Add more fixes to the country picker.

This commit is contained in:
Michelle Tang
2025-02-06 16:19:43 -05:00
committed by GitHub
parent 254b0dacc3
commit 5173916699
24 changed files with 425 additions and 355 deletions

View File

@@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import kotlin.time.Duration
@@ -53,8 +52,7 @@ data class RegistrationState(
val networkError: Throwable? = null,
val sessionCreationError: RegistrationSessionResult? = null,
val sessionStateError: VerificationCodeRequestResult? = null,
val registerAccountError: RegisterAccountResult? = null,
val country: Country? = null
val registerAccountError: RegisterAccountResult? = null
) {
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }

View File

@@ -61,7 +61,6 @@ import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequ
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.Success
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.TokenNotAccepted
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.UnknownError
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.util.RemoteConfig
@@ -113,9 +112,6 @@ class RegistrationViewModel : ViewModel() {
val phoneNumber: Phonenumber.PhoneNumber?
get() = store.value.phoneNumber
val country: Country?
get() = store.value.country
fun maybePrefillE164(context: Context) {
Log.v(TAG, "maybePrefillE164()")
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
@@ -198,18 +194,6 @@ class RegistrationViewModel : ViewModel() {
}
}
fun setCurrentCountryPicked(country: Country) {
store.update {
it.copy(country = country)
}
}
fun clearCountry() {
store.update {
it.copy(country = null)
}
}
fun fetchFcmToken(context: Context) {
viewModelScope.launch(context = coroutineExceptionHandler) {
val fcmToken = RegistrationRepository.getFcmToken(context)

View File

@@ -5,7 +5,12 @@
package org.thoughtcrime.securesms.registration.ui.countrycode
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Data class string describing useful characteristics of countries when selecting one. Used in the [CountryCodeState]
* An example is: Country(emoji=🇺🇸, name = "United States", countryCode = 1, regionCode= "US")
*/
data class Country(val emoji: String, val name: String, val countryCode: String)
@Parcelize
data class Country(val emoji: String, val name: String, val countryCode: Int, val regionCode: String) : Parcelable

View File

@@ -4,9 +4,10 @@ package org.thoughtcrime.securesms.registration.ui.countrycode
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -14,6 +15,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CircularProgressIndicator
@@ -25,9 +27,11 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -45,19 +49,21 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.fragment.app.activityViewModels
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch
import org.signal.core.ui.Dividers
import org.signal.core.ui.IconButtons.IconButton
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
/**
* Country picker fragment used in registration V1
@@ -66,10 +72,12 @@ class CountryCodeFragment : ComposeFragment() {
companion object {
private val TAG = Log.tag(CountryCodeFragment::class.java)
const val REQUEST_KEY_COUNTRY = "request_key_country"
const val REQUEST_COUNTRY = "country"
const val RESULT_COUNTRY = "country"
}
private val viewModel: CountryCodeViewModel by viewModels()
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
@Composable
override fun FragmentContent() {
@@ -77,10 +85,16 @@ class CountryCodeFragment : ComposeFragment() {
Screen(
state = state,
title = stringResource(R.string.CountryCodeFragment__your_country),
onSearch = { search -> viewModel.filterCountries(search) },
onDismissed = { findNavController().popBackStack() },
onClick = { country ->
sharedViewModel.setCurrentCountryPicked(country)
setFragmentResult(
REQUEST_KEY_COUNTRY,
bundleOf(
RESULT_COUNTRY to country
)
)
findNavController().popBackStack()
}
)
@@ -89,14 +103,16 @@ class CountryCodeFragment : ComposeFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.loadCountries()
val initialCountry = arguments?.getParcelableCompat(REQUEST_COUNTRY, Country::class.java)
viewModel.loadCountries(initialCountry)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
private fun Screen(
state: CountryCodeState,
title: String,
onSearch: (String) -> Unit = {},
onDismissed: () -> Unit = {},
onClick: (Country) -> Unit = {}
@@ -104,7 +120,7 @@ private fun Screen(
Scaffold(
topBar = {
Scaffolds.DefaultTopAppBar(
title = stringResource(R.string.CountryCodeFragment__your_country),
title = title,
titleContent = { _, title ->
Text(text = title, style = MaterialTheme.typography.titleLarge)
},
@@ -113,16 +129,19 @@ private fun Screen(
)
}
) { padding ->
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
state = listState,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(padding)
) {
item {
stickyHeader {
SearchBar(
text = state.query,
onSearch = onSearch
)
Spacer(modifier = Modifier.size(18.dp))
}
if (state.countryList.isEmpty()) {
@@ -149,6 +168,12 @@ private fun Screen(
}
}
}
LaunchedEffect(state.startingIndex) {
coroutineScope.launch {
listState.scrollToItem(index = state.startingIndex)
}
}
}
}
@@ -160,7 +185,7 @@ fun CountryItem(
) {
val emoji = country.emoji
val name = country.name
val code = country.countryCode
val code = "+${country.countryCode}"
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@@ -253,21 +278,30 @@ fun SearchBar(
onValueChange = { onSearch(it) },
placeholder = { Text(hint) },
trailingIcon = {
IconButton(onClick = {
showKeyboard = !showKeyboard
focusRequester.requestFocus()
}) {
if (showKeyboard) {
if (text.isNotEmpty()) {
IconButton(onClick = { onSearch("") }) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_keyboard_24),
contentDescription = null
)
} else {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_number_pad_24),
imageVector = ImageVector.vectorResource(R.drawable.symbol_x_24),
contentDescription = null
)
}
} else {
IconButton(onClick = {
showKeyboard = !showKeyboard
focusRequester.requestFocus()
}) {
if (showKeyboard) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_keyboard_24),
contentDescription = null
)
} else {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_number_pad_24),
contentDescription = null
)
}
}
}
},
keyboardOptions = KeyboardOptions(
@@ -279,10 +313,11 @@ fun SearchBar(
),
shape = RoundedCornerShape(32.dp),
modifier = modifier
.background(MaterialTheme.colorScheme.background)
.padding(bottom = 18.dp, start = 16.dp, end = 16.dp)
.fillMaxWidth()
.height(54.dp)
.focusRequester(focusRequester)
.padding(horizontal = 16.dp),
.focusRequester(focusRequester),
visualTransformation = VisualTransformation.None,
colors = TextFieldDefaults.colors(
// TODO move to SignalTheme
@@ -302,15 +337,16 @@ private fun ScreenPreview() {
Screen(
state = CountryCodeState(
countryList = mutableListOf(
Country("\uD83C\uDDFA\uD83C\uDDF8", "United States", "+1"),
Country("\uD83C\uDDE8\uD83C\uDDE6", "Canada", "+2"),
Country("\uD83C\uDDF2\uD83C\uDDFD", "Mexico", "+3")
Country("\uD83C\uDDFA\uD83C\uDDF8", "United States", 1, "US"),
Country("\uD83C\uDDE8\uD83C\uDDE6", "Canada", 2, "CA"),
Country("\uD83C\uDDF2\uD83C\uDDFD", "Mexico", 3, "MX")
),
commonCountryList = mutableListOf(
Country("\uD83C\uDDFA\uD83C\uDDF8", "United States", "+4"),
Country("\uD83C\uDDE8\uD83C\uDDE6", "Canada", "+5")
Country("\uD83C\uDDFA\uD83C\uDDF8", "United States", 4, "US"),
Country("\uD83C\uDDE8\uD83C\uDDE6", "Canada", 5, "CA")
)
)
),
title = "Your country"
)
}
}
@@ -322,7 +358,8 @@ private fun LoadingScreenPreview() {
Screen(
state = CountryCodeState(
countryList = emptyList()
)
),
title = "Your country"
)
}
}

View File

@@ -12,5 +12,6 @@ data class CountryCodeState(
val query: String = "",
val countryList: List<Country> = emptyList(),
val commonCountryList: List<Country> = emptyList(),
val filteredList: List<Country> = emptyList()
val filteredList: List<Country> = emptyList(),
val startingIndex: Int = 0
)

View File

@@ -7,15 +7,12 @@ package org.thoughtcrime.securesms.registration.ui.countrycode
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.i18n.phonenumbers.PhoneNumberUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.signal.core.util.logging.Log
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter
import java.util.Locale
/**
* View model to support [CountryCodeFragment] and track the countries
@@ -42,54 +39,30 @@ class CountryCodeViewModel : ViewModel() {
it.copy(
query = filterBy,
filteredList = state.value.countryList.filter { country: Country ->
country.name.contains(filterBy, ignoreCase = true) || country.countryCode.contains(filterBy)
country.name.contains(filterBy, ignoreCase = true) ||
country.countryCode.toString().contains(filterBy) ||
(filterBy.equals("usa", ignoreCase = true) && country.name.equals("United States", ignoreCase = true))
}
)
}
}
}
fun loadCountries() {
loadCommonCountryList()
fun loadCountries(initialCountry: Country? = null) {
viewModelScope.launch(Dispatchers.IO) {
val regions = PhoneNumberUtil.getInstance().supportedRegions
val countries = mutableListOf<Country>()
for (region in regions) {
val c = Country(
name = PhoneNumberFormatter.getRegionDisplayName(region).orElse(""),
emoji = CountryUtils.countryToEmoji(region),
countryCode = "+" + PhoneNumberUtil.getInstance().getCountryCodeForRegion(region)
)
countries.add(c)
}
val sortedCountries = countries.sortedWith { lhs, rhs ->
lhs.name.lowercase(Locale.getDefault()).compareTo(rhs.name.lowercase(Locale.getDefault()))
val countryList = CountryUtils.getCountries()
val commonCountryList = CountryUtils.getCommonCountries()
val startingIndex = if (initialCountry == null || commonCountryList.contains(initialCountry)) {
0
} else {
countryList.indexOf(initialCountry) + commonCountryList.size
}
internalState.update {
it.copy(
countryList = sortedCountries
)
}
}
}
private fun loadCommonCountryList() {
viewModelScope.launch(Dispatchers.IO) {
val countries = mutableListOf<Country>()
for (region in CountryUtils.COMMON_COUNTRIES) {
val c = Country(
name = PhoneNumberFormatter.getRegionDisplayName(region).orElse(""),
emoji = CountryUtils.countryToEmoji(region),
countryCode = "+" + PhoneNumberUtil.getInstance().getCountryCodeForRegion(region)
)
countries.add(c)
}
internalState.update {
it.copy(
commonCountryList = countries
countryList = countryList,
commonCountryList = commonCountryList,
startingIndex = startingIndex
)
}
}

View File

@@ -5,6 +5,9 @@
package org.thoughtcrime.securesms.registration.ui.countrycode
import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter
import java.text.Collator
import java.util.Locale
/**
@@ -12,6 +15,38 @@ import java.util.Locale
*/
object CountryUtils {
/** A hardcoded list of countries to suggest during registration. Can change at any time. */
private val COMMON_COUNTRIES = listOf("US", "DE", "IN", "NL", "UA")
fun getCountries(): List<Country> {
val collator = Collator.getInstance(Locale.getDefault())
collator.strength = Collator.PRIMARY
return PhoneNumberUtil.getInstance().supportedRegions
.map { region ->
Country(
name = PhoneNumberFormatter.getRegionDisplayName(region).orElse(""),
emoji = countryToEmoji(region),
countryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(region),
regionCode = region
)
}.sortedWith { lhs, rhs ->
collator.compare(lhs.name.lowercase(Locale.getDefault()), rhs.name.lowercase(Locale.getDefault()))
}
}
fun getCommonCountries(): List<Country> {
return COMMON_COUNTRIES
.map { region ->
Country(
name = PhoneNumberFormatter.getRegionDisplayName(region).orElse(""),
emoji = countryToEmoji(region),
countryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(region),
regionCode = region
)
}
}
fun countryToEmoji(countryCode: String): String {
return if (countryCode.isNotEmpty()) {
countryCode
@@ -23,7 +58,4 @@ object CountryUtils {
""
}
}
/** A hardcoded list of countries to suggest during registration. Can change at any time. */
val COMMON_COUNTRIES = listOf("US", "DE", "IN", "NL", "UA")
}

View File

@@ -37,6 +37,7 @@ import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import org.signal.core.util.ThreadUtil
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
@@ -58,6 +59,7 @@ import org.thoughtcrime.securesms.registration.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.ui.RegistrationState
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeFragment
import org.thoughtcrime.securesms.registration.ui.toE164
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.util.CommunicationActions
@@ -109,6 +111,14 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
moveToCountryPickerScreen()
}
parentFragmentManager.setFragmentResultListener(
CountryCodeFragment.REQUEST_KEY_COUNTRY,
this
) { _, bundle ->
val country: Country = bundle.getParcelableCompat(CountryCodeFragment.RESULT_COUNTRY, Country::class.java)!!
fragmentViewModel.setCountry(country.countryCode, country)
}
spinnerAdapter = ArrayAdapter<CountryPrefix>(
requireContext(),
R.layout.registration_country_code_dropdown_item,
@@ -162,9 +172,11 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
.map { it.phoneNumberRegionCode }
.distinctUntilChanged()
.observe(viewLifecycleOwner) { regionCode ->
currentPhoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
reformatText(phoneNumberInputLayout.text)
phoneNumberInputLayout.requestFocus()
if (regionCode.isNotNullOrBlank()) {
currentPhoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
reformatText(phoneNumberInputLayout.text)
phoneNumberInputLayout.requestFocus()
}
}
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
@@ -184,18 +196,12 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
initializeInputFields()
val existingPhoneNumber = sharedViewModel.phoneNumber
val country = sharedViewModel.country
if (country != null) {
binding.countryEmoji.text = country.emoji
binding.country.text = country.name
spinnerView.setText(country.countryCode)
} else if (existingPhoneNumber != null) {
if (existingPhoneNumber != null) {
fragmentViewModel.restoreState(existingPhoneNumber)
spinnerView.setText(existingPhoneNumber.countryCode.toString())
phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString())
} else {
spinnerView.setText(fragmentViewModel.countryPrefix().toString())
} else if (spinnerView.text?.isEmpty() == true) {
spinnerView.setText(fragmentViewModel.getDefaultCountryCode(requireContext()).toString())
}
ViewUtil.focusAndShowKeyboard(phoneNumberInputLayout)
@@ -203,8 +209,15 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
private fun updateCountrySelection(country: Country?) {
if (country != null) {
binding.countryEmoji.visible = true
binding.countryEmoji.text = country.emoji
binding.country.text = country.name
if (spinnerView.text.toString() != country.countryCode.toString()) {
spinnerView.setText(country.countryCode.toString())
}
} else {
binding.countryEmoji.visible = false
binding.country.text = getString(R.string.RegistrationActivity_select_a_country)
}
}
@@ -242,21 +255,13 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
private fun initializeInputFields() {
binding.countryCode.editText?.addTextChangedListener { s ->
if (s.isNotNullOrBlank()) {
val formatted = s.toString().replace("+", "").let { "+$it" }
if (formatted != s.toString()) {
s.replace(0, s.length, formatted)
}
}
val sanitized = s.toString().filter { c -> c.isDigit() }
if (sanitized.isNotNullOrBlank()) {
val countryCode: Int = sanitized.toInt()
if (sharedViewModel.country != null) {
fragmentViewModel.setCountry(countryCode, sharedViewModel.country)
sharedViewModel.clearCountry()
} else {
fragmentViewModel.setCountry(countryCode)
}
fragmentViewModel.setCountry(countryCode)
} else {
binding.countryCode.editText?.setHint(R.string.RegistrationActivity_default_country_code)
fragmentViewModel.clearCountry()
}
}
@@ -662,7 +667,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
}
private fun moveToCountryPickerScreen() {
findNavController().safeNavigate(R.id.action_countryPicker)
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionCountryPicker(fragmentViewModel.country))
}
private fun popBackStack() {

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.registration.ui.phonenumber
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.google.i18n.phonenumbers.NumberParseException
@@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter
/**
@@ -54,25 +56,73 @@ class EnterPhoneNumberViewModel : ViewModel() {
it.copy(mode = value)
}
fun countryPrefix(): CountryPrefix {
return supportedCountryPrefixes[store.value.countryPrefixIndex]
fun getDefaultCountryCode(context: Context): Int {
val existingCountry = store.value.country
val maybeRegionCode = Util.getNetworkCountryIso(context)
val regionCode = if (maybeRegionCode != null && supportedCountryPrefixes.any { it.regionCode == maybeRegionCode }) {
maybeRegionCode
} else {
Log.w(TAG, "Could not find region code")
"US"
}
val countryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(regionCode)
store.update {
it.copy(
country = existingCountry ?: Country(
name = PhoneNumberFormatter.getRegionDisplayName(regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(regionCode),
countryCode = countryCode,
regionCode = regionCode
)
)
}
return existingCountry?.countryCode ?: countryCode
}
val country: Country?
get() = store.value.country
fun setPhoneNumber(phoneNumber: String?) {
store.update { it.copy(phoneNumber = phoneNumber ?: "") }
}
fun clearCountry() {
store.update {
it.copy(
country = null,
phoneNumberRegionCode = "",
countryPrefixIndex = 0
)
}
}
fun setCountry(digits: Int, country: Country? = null) {
val matchingIndex = countryCodeToAdapterIndex(digits)
if (matchingIndex == -1) {
Log.d(TAG, "Invalid country code specified $digits")
if (country == null && digits == store.value.country?.countryCode) {
return
}
val matchingIndex = countryCodeToAdapterIndex(digits)
if (matchingIndex == -1) {
Log.d(TAG, "Invalid country code specified $digits")
store.update {
it.copy(
country = null,
phoneNumberRegionCode = "",
countryPrefixIndex = 0
)
}
return
}
val regionCode = supportedCountryPrefixes[matchingIndex].regionCode
val matchedCountry = Country(
name = PhoneNumberFormatter.getRegionDisplayName(supportedCountryPrefixes[matchingIndex].regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(supportedCountryPrefixes[matchingIndex].regionCode),
countryCode = digits.toString()
name = PhoneNumberFormatter.getRegionDisplayName(regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(regionCode),
countryCode = digits,
regionCode = regionCode
)
store.update {
@@ -90,7 +140,8 @@ class EnterPhoneNumberViewModel : ViewModel() {
fun isEnteredNumberPossible(state: EnterPhoneNumberState): Boolean {
return try {
PhoneNumberUtil.getInstance().isPossibleNumber(parsePhoneNumber(state))
state.country != null &&
PhoneNumberUtil.getInstance().isPossibleNumber(parsePhoneNumber(state))
} catch (ex: NumberParseException) {
false
}

View File

@@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registrationv3.ui.countrycode.Country
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import kotlin.time.Duration
@@ -54,8 +53,7 @@ data class RegistrationState(
val networkError: Throwable? = null,
val sessionCreationError: RegistrationSessionResult? = null,
val sessionStateError: VerificationCodeRequestResult? = null,
val registerAccountError: RegisterAccountResult? = null,
val country: Country? = null
val registerAccountError: RegisterAccountResult? = null
) {
val challengesRemaining: List<Challenge> = challengesRequested.filterNot { it in challengesPresented }

View File

@@ -75,7 +75,6 @@ import org.thoughtcrime.securesms.registration.ui.toE164
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.registrationv3.ui.countrycode.Country
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
@@ -129,9 +128,6 @@ class RegistrationViewModel : ViewModel() {
val phoneNumber: Phonenumber.PhoneNumber?
get() = store.value.phoneNumber
val country: Country?
get() = store.value.country
fun maybePrefillE164(context: Context) {
Log.v(TAG, "maybePrefillE164()")
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
@@ -1046,18 +1042,6 @@ class RegistrationViewModel : ViewModel() {
.firstOrNull()
}
fun setCurrentCountryPicked(country: Country) {
store.update {
it.copy(country = country)
}
}
fun clearCountry() {
store.update {
it.copy(country = null)
}
}
companion object {
private val TAG = Log.tag(RegistrationViewModel::class.java)

View File

@@ -1,11 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registrationv3.ui.countrycode
/**
* Data class string describing useful characteristics of countries when selecting one. Used in the [CountryCodeState]
*/
data class Country(val emoji: String, val name: String, val countryCode: String)

View File

@@ -4,9 +4,10 @@ package org.thoughtcrime.securesms.registrationv3.ui.countrycode
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -14,6 +15,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CircularProgressIndicator
@@ -25,9 +27,11 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -45,19 +49,24 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.fragment.app.activityViewModels
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch
import org.signal.core.ui.Dividers
import org.signal.core.ui.IconButtons.IconButton
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeState
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeViewModel
/**
* Country picker fragment used in registration V3
@@ -66,10 +75,12 @@ class CountryCodeFragment : ComposeFragment() {
companion object {
private val TAG = Log.tag(CountryCodeFragment::class.java)
const val REQUEST_KEY_COUNTRY = "request_key_country"
const val REQUEST_COUNTRY = "country"
const val RESULT_COUNTRY = "country"
}
private val viewModel: CountryCodeViewModel by viewModels()
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
@Composable
override fun FragmentContent() {
@@ -77,10 +88,16 @@ class CountryCodeFragment : ComposeFragment() {
Screen(
state = state,
title = stringResource(R.string.CountryCodeFragment__your_country),
onSearch = { search -> viewModel.filterCountries(search) },
onDismissed = { findNavController().popBackStack() },
onClick = { country ->
sharedViewModel.setCurrentCountryPicked(country)
setFragmentResult(
REQUEST_KEY_COUNTRY,
bundleOf(
RESULT_COUNTRY to country
)
)
findNavController().popBackStack()
}
)
@@ -89,14 +106,16 @@ class CountryCodeFragment : ComposeFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.loadCountries()
val initialCountry = arguments?.getParcelableCompat(REQUEST_COUNTRY, Country::class.java)
viewModel.loadCountries(initialCountry)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
private fun Screen(
fun Screen(
state: CountryCodeState,
title: String,
onSearch: (String) -> Unit = {},
onDismissed: () -> Unit = {},
onClick: (Country) -> Unit = {}
@@ -104,7 +123,7 @@ private fun Screen(
Scaffold(
topBar = {
Scaffolds.DefaultTopAppBar(
title = stringResource(R.string.CountryCodeFragment__your_country),
title = title,
titleContent = { _, title ->
Text(text = title, style = MaterialTheme.typography.titleLarge)
},
@@ -113,16 +132,19 @@ private fun Screen(
)
}
) { padding ->
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
state = listState,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(padding)
) {
item {
stickyHeader {
SearchBar(
text = state.query,
onSearch = onSearch
)
Spacer(modifier = Modifier.size(18.dp))
}
if (state.countryList.isEmpty()) {
@@ -132,12 +154,14 @@ private fun Screen(
)
}
} else if (state.query.isEmpty()) {
items(state.commonCountryList) { country ->
CountryItem(country, onClick)
}
if (state.commonCountryList.isNotEmpty()) {
items(state.commonCountryList) { country ->
CountryItem(country, onClick)
}
item {
Dividers.Default()
item {
Dividers.Default()
}
}
items(state.countryList) { country ->
@@ -149,6 +173,12 @@ private fun Screen(
}
}
}
LaunchedEffect(state.startingIndex) {
coroutineScope.launch {
listState.scrollToItem(index = state.startingIndex)
}
}
}
}
@@ -160,7 +190,7 @@ fun CountryItem(
) {
val emoji = country.emoji
val name = country.name
val code = country.countryCode
val code = "+${country.countryCode}"
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@@ -253,21 +283,30 @@ fun SearchBar(
onValueChange = { onSearch(it) },
placeholder = { Text(hint) },
trailingIcon = {
IconButton(onClick = {
showKeyboard = !showKeyboard
focusRequester.requestFocus()
}) {
if (showKeyboard) {
if (text.isNotEmpty()) {
IconButton(onClick = { onSearch("") }) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_keyboard_24),
contentDescription = null
)
} else {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_number_pad_24),
imageVector = ImageVector.vectorResource(R.drawable.symbol_x_24),
contentDescription = null
)
}
} else {
IconButton(onClick = {
showKeyboard = !showKeyboard
focusRequester.requestFocus()
}) {
if (showKeyboard) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_keyboard_24),
contentDescription = null
)
} else {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_number_pad_24),
contentDescription = null
)
}
}
}
},
keyboardOptions = KeyboardOptions(
@@ -279,10 +318,11 @@ fun SearchBar(
),
shape = RoundedCornerShape(32.dp),
modifier = modifier
.background(MaterialTheme.colorScheme.background)
.padding(bottom = 18.dp, start = 16.dp, end = 16.dp)
.fillMaxWidth()
.height(54.dp)
.focusRequester(focusRequester)
.padding(horizontal = 16.dp),
.focusRequester(focusRequester),
visualTransformation = VisualTransformation.None,
colors = TextFieldDefaults.colors(
// TODO move to SignalTheme
@@ -302,15 +342,16 @@ private fun ScreenPreview() {
Screen(
state = CountryCodeState(
countryList = mutableListOf(
Country("\uD83C\uDDFA\uD83C\uDDF8", "United States", "+1"),
Country("\uD83C\uDDE8\uD83C\uDDE6", "Canada", "+2"),
Country("\uD83C\uDDF2\uD83C\uDDFD", "Mexico", "+3")
Country("\uD83C\uDDFA\uD83C\uDDF8", "United States", 1, "US"),
Country("\uD83C\uDDE8\uD83C\uDDE6", "Canada", 2, "CA"),
Country("\uD83C\uDDF2\uD83C\uDDFD", "Mexico", 3, "MX")
),
commonCountryList = mutableListOf(
Country("\uD83C\uDDFA\uD83C\uDDF8", "United States", "+4"),
Country("\uD83C\uDDE8\uD83C\uDDE6", "Canada", "+5")
Country("\uD83C\uDDFA\uD83C\uDDF8", "United States", 4, "US"),
Country("\uD83C\uDDE8\uD83C\uDDE6", "Canada", 5, "CA")
)
)
),
title = "Your country"
)
}
}
@@ -322,7 +363,8 @@ private fun LoadingScreenPreview() {
Screen(
state = CountryCodeState(
countryList = emptyList()
)
),
title = "Your country"
)
}
}

View File

@@ -1,16 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registrationv3.ui.countrycode
/**
* State managed by [CountryCodeViewModel]. Includes country list and allows for searching
*/
data class CountryCodeState(
val query: String = "",
val countryList: List<Country> = emptyList(),
val commonCountryList: List<Country> = emptyList(),
val filteredList: List<Country> = emptyList()
)

View File

@@ -1,98 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registrationv3.ui.countrycode
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.i18n.phonenumbers.PhoneNumberUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter
import java.util.Locale
/**
* View model to support [CountryCodeFragment] and track the countries
*/
class CountryCodeViewModel : ViewModel() {
companion object {
private val TAG = Log.tag(CountryCodeViewModel::class.java)
}
private val internalState = MutableStateFlow(CountryCodeState())
val state = internalState.asStateFlow()
fun filterCountries(filterBy: String) {
if (filterBy.isEmpty()) {
internalState.update {
it.copy(
query = filterBy,
filteredList = emptyList()
)
}
} else {
internalState.update {
it.copy(
query = filterBy,
filteredList = state.value.countryList.filter { country: Country ->
country.name.contains(filterBy, ignoreCase = true) || country.countryCode.contains(filterBy)
}
)
}
}
}
fun loadCountries() {
loadCommonCountryList()
viewModelScope.launch(Dispatchers.IO) {
val regions = PhoneNumberUtil.getInstance().supportedRegions
val countries = mutableListOf<Country>()
for (region in regions) {
val c = Country(
name = PhoneNumberFormatter.getRegionDisplayName(region).orElse(""),
emoji = CountryUtils.countryToEmoji(region),
countryCode = "+" + PhoneNumberUtil.getInstance().getCountryCodeForRegion(region)
)
countries.add(c)
}
val sortedCountries = countries.sortedWith { lhs, rhs ->
lhs.name.lowercase(Locale.getDefault()).compareTo(rhs.name.lowercase(Locale.getDefault()))
}
internalState.update {
it.copy(
countryList = sortedCountries
)
}
}
}
private fun loadCommonCountryList() {
viewModelScope.launch(Dispatchers.IO) {
val countries = mutableListOf<Country>()
for (region in CountryUtils.COMMON_COUNTRIES) {
val c = Country(
name = PhoneNumberFormatter.getRegionDisplayName(region).orElse(""),
emoji = CountryUtils.countryToEmoji(region),
countryCode = "+" + PhoneNumberUtil.getInstance().getCountryCodeForRegion(region)
)
countries.add(c)
}
internalState.update {
it.copy(
commonCountryList = countries
)
}
}
}
}

View File

@@ -38,6 +38,7 @@ import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import org.signal.core.util.ThreadUtil
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
@@ -54,13 +55,14 @@ import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionC
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeFragment
import org.thoughtcrime.securesms.registration.ui.toE164
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationState
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registrationv3.ui.countrycode.Country
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.Dialogs
import org.thoughtcrime.securesms.util.PlayServicesUtil
@@ -114,6 +116,14 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
moveToCountryPickerScreen()
}
parentFragmentManager.setFragmentResultListener(
CountryCodeFragment.REQUEST_KEY_COUNTRY,
this
) { _, bundle ->
val country: Country = bundle.getParcelableCompat(CountryCodeFragment.RESULT_COUNTRY, Country::class.java)!!
fragmentViewModel.setCountry(country.countryCode, country)
}
spinnerAdapter = ArrayAdapter<CountryPrefix>(
requireContext(),
R.layout.registration_country_code_dropdown_item,
@@ -167,9 +177,11 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
.map { it.phoneNumberRegionCode }
.distinctUntilChanged()
.observe(viewLifecycleOwner) { regionCode ->
currentPhoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
reformatText(phoneNumberInputLayout.text)
phoneNumberInputLayout.requestFocus()
if (regionCode.isNotNullOrBlank()) {
currentPhoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
reformatText(phoneNumberInputLayout.text)
phoneNumberInputLayout.requestFocus()
}
}
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
@@ -189,18 +201,12 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
initializeInputFields()
val existingPhoneNumber = sharedViewModel.phoneNumber
val country = sharedViewModel.country
if (country != null) {
binding.countryEmoji.text = country.emoji
binding.country.text = country.name
spinnerView.setText(country.countryCode)
} else if (existingPhoneNumber != null) {
if (existingPhoneNumber != null) {
fragmentViewModel.restoreState(existingPhoneNumber)
spinnerView.setText(existingPhoneNumber.countryCode.toString())
phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString())
} else {
spinnerView.setText(fragmentViewModel.countryPrefix().toString())
} else if (spinnerView.text?.isEmpty() == true) {
spinnerView.setText(fragmentViewModel.getDefaultCountryCode(requireContext()).toString())
}
if (enterPhoneNumberMode == EnterPhoneNumberMode.RESTART_AFTER_COLLECTION && (savedInstanceState == null && !processedResumeMode)) {
@@ -213,8 +219,15 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
private fun updateCountrySelection(country: Country?) {
if (country != null) {
binding.countryEmoji.visible = true
binding.countryEmoji.text = country.emoji
binding.country.text = country.name
if (spinnerView.text.toString() != country.countryCode.toString()) {
spinnerView.setText(country.countryCode.toString())
}
} else {
binding.countryEmoji.visible = false
binding.country.text = getString(R.string.RegistrationActivity_select_a_country)
}
}
@@ -252,21 +265,13 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
private fun initializeInputFields() {
binding.countryCode.editText?.addTextChangedListener { s ->
if (s.isNotNullOrBlank()) {
val formatted = s.toString().replace("+", "").let { "+$it" }
if (formatted != s.toString()) {
s.replace(0, s.length, formatted)
}
}
val sanitized = s.toString().filter { c -> c.isDigit() }
if (sanitized.isNotNullOrBlank()) {
val countryCode: Int = sanitized.toInt()
if (sharedViewModel.country != null) {
fragmentViewModel.setCountry(countryCode, sharedViewModel.country)
sharedViewModel.clearCountry()
} else {
fragmentViewModel.setCountry(countryCode)
}
fragmentViewModel.setCountry(countryCode)
} else {
binding.countryCode.editText?.setHint(R.string.RegistrationActivity_default_country_code)
fragmentViewModel.clearCountry()
}
}
@@ -681,7 +686,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
}
private fun moveToCountryPickerScreen() {
findNavController().safeNavigate(R.id.action_countryPicker)
findNavController().safeNavigate(EnterPhoneNumberFragmentDirections.actionCountryPicker(fragmentViewModel.country))
}
private fun popBackStack() {

View File

@@ -5,8 +5,8 @@
package org.thoughtcrime.securesms.registrationv3.ui.phonenumber
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.registrationv3.ui.countrycode.Country
/**
* State holder for the phone number entry screen, including phone number and Play Services errors.

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.registrationv3.ui.phonenumber
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.google.i18n.phonenumbers.NumberParseException
@@ -13,10 +14,11 @@ import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryUtils
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.registrationv3.ui.countrycode.Country
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter
/**
@@ -54,23 +56,73 @@ class EnterPhoneNumberViewModel : ViewModel() {
it.copy(mode = value)
}
fun countryPrefix(): CountryPrefix = supportedCountryPrefixes[store.value.countryPrefixIndex]
fun getDefaultCountryCode(context: Context): Int {
val existingCountry = store.value.country
val maybeRegionCode = Util.getNetworkCountryIso(context)
val regionCode = if (maybeRegionCode != null && supportedCountryPrefixes.any { it.regionCode == maybeRegionCode }) {
maybeRegionCode
} else {
Log.w(TAG, "Could not find region code")
"US"
}
val countryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(regionCode)
store.update {
it.copy(
country = existingCountry ?: Country(
name = PhoneNumberFormatter.getRegionDisplayName(regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(regionCode),
countryCode = countryCode,
regionCode = regionCode
)
)
}
return existingCountry?.countryCode ?: countryCode
}
val country: Country?
get() = store.value.country
fun setPhoneNumber(phoneNumber: String?) {
store.update { it.copy(phoneNumber = phoneNumber ?: "") }
}
fun clearCountry() {
store.update {
it.copy(
country = null,
phoneNumberRegionCode = "",
countryPrefixIndex = 0
)
}
}
fun setCountry(digits: Int, country: Country? = null) {
val matchingIndex = countryCodeToAdapterIndex(digits)
if (matchingIndex == -1) {
Log.d(TAG, "Invalid country code specified $digits")
if (country == null && digits == store.value.country?.countryCode) {
return
}
val matchingIndex = countryCodeToAdapterIndex(digits)
if (matchingIndex == -1) {
Log.d(TAG, "Invalid country code specified $digits")
store.update {
it.copy(
country = null,
phoneNumberRegionCode = "",
countryPrefixIndex = 0
)
}
return
}
val regionCode = supportedCountryPrefixes[matchingIndex].regionCode
val matchedCountry = Country(
name = PhoneNumberFormatter.getRegionDisplayName(supportedCountryPrefixes[matchingIndex].regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(supportedCountryPrefixes[matchingIndex].regionCode),
countryCode = digits.toString()
name = PhoneNumberFormatter.getRegionDisplayName(regionCode).orElse(""),
emoji = CountryUtils.countryToEmoji(regionCode),
countryCode = digits,
regionCode = regionCode
)
store.update {
@@ -88,7 +140,8 @@ class EnterPhoneNumberViewModel : ViewModel() {
fun isEnteredNumberPossible(state: EnterPhoneNumberState): Boolean {
return try {
PhoneNumberUtil.getInstance().isPossibleNumber(parsePhoneNumber(state))
state.country != null &&
PhoneNumberUtil.getInstance().isPossibleNumber(parsePhoneNumber(state))
} catch (ex: NumberParseException) {
false
}

View File

@@ -232,6 +232,11 @@ public class Util {
return Optional.ofNullable(simCountryIso != null ? simCountryIso.toUpperCase() : null);
}
public static @Nullable String getNetworkCountryIso(Context context) {
String networkCountryIso = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getNetworkCountryIso();
return networkCountryIso == null ? null : networkCountryIso.toUpperCase();
}
public static @NonNull <T> T firstNonNull(@Nullable T optional, @NonNull T fallback) {
return optional != null ? optional : fallback;
}

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="9dp" android:viewportHeight="9" android:viewportWidth="9" android:width="9dp">
<path android:fillAlpha="0.6" android:fillColor="#44464D" android:pathData="M8.4063,3.8984V5.2578H0.6094V3.8984H8.4063ZM5.2344,0.5781V8.8594H3.7891V0.5781H5.2344Z"/>
</vector>

View File

@@ -43,13 +43,18 @@
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:inputType="number"
android:digits="+1234567890"
android:maxLength="4"
android:digits="1234567890"
android:maxLength="3"
android:padding="0dp"
android:minHeight="56dp"
android:minWidth="76dp"
android:maxLines="1"
android:singleLine="true"
android:textAlignment="center"
android:textColor="@color/signal_colorOnSurfaceVariant"
android:text="@string/RegistrationActivity_default_country_code" />
android:textAppearance="@style/Signal.Text.BodyLarge"
android:drawableStart="@drawable/symbol_plus_9"
android:drawablePadding="4dp"
android:gravity="start|center_vertical"
android:textColor="@color/signal_colorOnSurfaceVariant" />
</com.google.android.material.textfield.TextInputLayout>
@@ -108,8 +113,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:layout_marginHorizontal="24dp"
android:paddingVertical="16dp"
android:background="@drawable/country_picker_background"
android:minHeight="56dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/verify_subheader">

View File

@@ -61,7 +61,13 @@
<fragment
android:id="@+id/countryPickerFragment"
android:name="org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeFragment"
tools:layout="@layout/fragment_registration_country_picker" />
tools:layout="@layout/fragment_registration_country_picker">
<argument
android:name="country"
app:argType="org.thoughtcrime.securesms.registration.ui.countrycode.Country"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/enterPhoneNumberFragment"

View File

@@ -118,7 +118,13 @@
<fragment
android:id="@+id/countryPickerFragment"
android:name="org.thoughtcrime.securesms.registrationv3.ui.countrycode.CountryCodeFragment"
tools:layout="@layout/fragment_registration_country_picker"/>
tools:layout="@layout/fragment_registration_country_picker">
<argument
android:name="country"
app:argType="org.thoughtcrime.securesms.registration.ui.countrycode.Country"
app:nullable="true" />
</fragment>
<fragment

View File

@@ -2556,7 +2556,7 @@
<!-- Hint text to select a country -->
<string name="RegistrationActivity_select_a_country">Select a country</string>
<!-- Hint text explaining that this is where the country code should go -->
<string name="RegistrationActivity_default_country_code">+0</string>
<string name="RegistrationActivity_default_country_code">0</string>
<string name="RegistrationActivity_you_will_receive_a_call_to_verify_this_number">You\'ll receive a call to verify this number.</string>
<string name="RegistrationActivity_edit_number">Edit number</string>
<string name="RegistrationActivity_missing_google_play_services">Missing Google Play Services</string>