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 b572713d51..d8948a6b56 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 @@ -5,7 +5,6 @@ package org.signal.registration.screens.phonenumber -import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -25,9 +24,11 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField 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 @@ -37,10 +38,11 @@ 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.graphics.Color -import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -51,13 +53,13 @@ import org.signal.core.ui.compose.AllDevicePreviews import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Previews +import org.signal.core.util.E164Util import org.signal.registration.R import org.signal.registration.screens.phonenumber.PhoneNumberEntryState.OneTimeEvent import org.signal.registration.test.TestTags /** - * Phone number entry screen for the registration flow. - * Allows users to select their country and enter their phone number. + * Phone number entry screen */ @Composable fun PhoneNumberScreen( @@ -65,17 +67,28 @@ fun PhoneNumberScreen( onEvent: (PhoneNumberEntryScreenEvents) -> Unit, modifier: Modifier = Modifier ) { - val unableToSendSmsMsg = stringResource(R.string.VerificationCodeScreen__unable_to_send_sms) + val resources = LocalResources.current var simpleErrorMessage: String? by remember { mutableStateOf(null) } + if (state.showDialog) { + Dialogs.SimpleAlertDialog( + title = stringResource(R.string.RegistrationActivity_is_the_phone_number), + body = "${E164Util.formatAsE164WithCountryCodeForDisplay(state.countryCode, state.nationalNumber)}\n\n${stringResource(R.string.RegistrationActivity_a_verification_code)}", + confirm = stringResource(id = android.R.string.ok), + dismiss = stringResource(R.string.RegistrationActivity_edit_number), + onConfirm = { onEvent(PhoneNumberEntryScreenEvents.PhoneNumberSubmitted) }, + onDismiss = { onEvent(PhoneNumberEntryScreenEvents.PhoneNumberCancelled) } + ) + } + LaunchedEffect(state.oneTimeEvent) { onEvent(PhoneNumberEntryScreenEvents.ConsumeOneTimeEvent) when (state.oneTimeEvent) { - OneTimeEvent.NetworkError -> simpleErrorMessage = "Network error" - is OneTimeEvent.RateLimited -> simpleErrorMessage = "Rate limited" - OneTimeEvent.UnknownError -> simpleErrorMessage = "Unknown error" - OneTimeEvent.CouldNotRequestCodeWithSelectedTransport -> simpleErrorMessage = "Could not request code with selected transport" - OneTimeEvent.UnableToSendSms -> simpleErrorMessage = unableToSendSmsMsg + OneTimeEvent.NetworkError -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__network_error) + is OneTimeEvent.RateLimited -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__too_many_attempts_try_again_in_s, state.oneTimeEvent.retryAfter.toString()) + OneTimeEvent.UnknownError -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__an_unexpected_error_occurred) + OneTimeEvent.CouldNotRequestCodeWithSelectedTransport -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__could_not_send_code_via_selected_method) + OneTimeEvent.UnableToSendSms -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__unable_to_send_sms) null -> Unit } } @@ -83,12 +96,16 @@ fun PhoneNumberScreen( simpleErrorMessage?.let { message -> Dialogs.SimpleMessageDialog( message = message, - dismiss = "Ok", + dismiss = stringResource(android.R.string.ok), onDismiss = { simpleErrorMessage = null } ) } - Box(modifier = modifier.fillMaxSize().testTag(TestTags.PHONE_NUMBER_SCREEN)) { + Box( + modifier = modifier + .fillMaxSize() + .testTag(TestTags.PHONE_NUMBER_SCREEN) + ) { ScreenContent(state, onEvent) } } @@ -105,10 +122,8 @@ private fun ScreenContent(state: PhoneNumberEntryState, onEvent: (PhoneNumberEnt .fillMaxSize() .verticalScroll(scrollState) ) { - // Toolbar spacer (matching the Toolbar height in the XML) Spacer(modifier = Modifier.height(56.dp)) - // Title - "Phone number" Text( text = stringResource(R.string.RegistrationActivity_phone_number), style = MaterialTheme.typography.headlineMedium, @@ -119,7 +134,6 @@ private fun ScreenContent(state: PhoneNumberEntryState, onEvent: (PhoneNumberEnt Spacer(modifier = Modifier.height(16.dp)) - // Subtitle - "You will receive a verification code..." Text( text = stringResource(R.string.RegistrationActivity_you_will_receive_a_verification_code), style = MaterialTheme.typography.bodyLarge, @@ -131,7 +145,6 @@ private fun ScreenContent(state: PhoneNumberEntryState, onEvent: (PhoneNumberEnt Spacer(modifier = Modifier.height(36.dp)) - // Country Picker - styled with surfaceVariant background and outline bottom border CountryPicker( emoji = selectedCountryEmoji, country = selectedCountry, @@ -149,7 +162,7 @@ private fun ScreenContent(state: PhoneNumberEntryState, onEvent: (PhoneNumberEnt formattedNumber = state.formattedNumber, onCountryCodeChanged = { onEvent(PhoneNumberEntryScreenEvents.CountryCodeChanged(it)) }, onPhoneNumberChanged = { onEvent(PhoneNumberEntryScreenEvents.PhoneNumberChanged(it)) }, - onPhoneNumberSubmitted = { onEvent(PhoneNumberEntryScreenEvents.PhoneNumberSubmitted) }, + onPhoneNumberEntered = { onEvent(PhoneNumberEntryScreenEvents.PhoneNumberEntered) }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) @@ -157,7 +170,6 @@ private fun ScreenContent(state: PhoneNumberEntryState, onEvent: (PhoneNumberEnt Spacer(modifier = Modifier.weight(1f)) - // Bottom row with the next/spinner button aligned to end Row( modifier = Modifier .fillMaxWidth() @@ -165,18 +177,18 @@ private fun ScreenContent(state: PhoneNumberEntryState, onEvent: (PhoneNumberEnt horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically ) { - Buttons.LargeTonal( - onClick = { onEvent(PhoneNumberEntryScreenEvents.PhoneNumberSubmitted) }, - enabled = !state.showSpinner && state.countryCode.isNotEmpty() && state.nationalNumber.isNotEmpty(), - modifier = Modifier.testTag(TestTags.PHONE_NUMBER_NEXT_BUTTON) - ) { - if (state.showSpinner) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } else { + if (state.showSpinner) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 3.dp, + color = MaterialTheme.colorScheme.primary + ) + } else { + Buttons.LargeTonal( + onClick = { onEvent(PhoneNumberEntryScreenEvents.PhoneNumberEntered) }, + enabled = state.countryCode.isNotEmpty() && state.nationalNumber.isNotEmpty(), + modifier = Modifier.testTag(TestTags.PHONE_NUMBER_NEXT_BUTTON) + ) { Text(stringResource(R.string.RegistrationActivity_next)) } } @@ -184,12 +196,6 @@ private fun ScreenContent(state: PhoneNumberEntryState, onEvent: (PhoneNumberEnt } } -/** - * Country picker row styled to match the XML layout: - * - surfaceVariant background with outline bottom border - * - Rounded top corners (8dp outline, 4dp inner) - * - Country emoji, country name, and dropdown triangle - */ @Composable private fun CountryPicker( emoji: String, @@ -229,7 +235,9 @@ private fun CountryPicker( modifier = Modifier.weight(1f) ) - DropdownTriangle( + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.symbol_drop_down_24), + contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(24.dp) ) @@ -239,7 +247,6 @@ private fun CountryPicker( /** * Phone number input fields containing the country code and phone number text fields. - * Handles cursor position preservation when the formatted number changes. */ @Composable private fun PhoneNumberInputFields( @@ -247,17 +254,13 @@ private fun PhoneNumberInputFields( formattedNumber: String, onCountryCodeChanged: (String) -> Unit, onPhoneNumberChanged: (String) -> Unit, - onPhoneNumberSubmitted: () -> Unit, + onPhoneNumberEntered: () -> Unit, modifier: Modifier = Modifier ) { - // Track the phone number text field value with cursor position var phoneNumberTextFieldValue by remember { mutableStateOf(TextFieldValue(formattedNumber)) } - // Update the text field value when formattedNumber changes, preserving cursor position LaunchedEffect(formattedNumber) { if (phoneNumberTextFieldValue.text != formattedNumber) { - // Calculate cursor position: count digits before cursor in old text, - // then find position with same digit count in new text val oldText = phoneNumberTextFieldValue.text val oldCursorPos = phoneNumberTextFieldValue.selection.end val digitsBeforeCursor = oldText.take(oldCursorPos).count { it.isDigit() } @@ -286,8 +289,7 @@ private fun PhoneNumberInputFields( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.Bottom ) { - // Country code field - OutlinedTextField( + TextField( value = countryCode, onValueChange = onCountryCodeChanged, modifier = Modifier @@ -307,13 +309,16 @@ private fun PhoneNumberInputFields( singleLine = true, textStyle = MaterialTheme.typography.bodyLarge.copy( color = MaterialTheme.colorScheme.onSurfaceVariant + ), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, + focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant ) ) Spacer(modifier = Modifier.width(20.dp)) - // Phone number field - OutlinedTextField( + TextField( value = phoneNumberTextFieldValue, onValueChange = { newValue -> phoneNumberTextFieldValue = newValue @@ -330,40 +335,20 @@ private fun PhoneNumberInputFields( imeAction = ImeAction.Done ), keyboardActions = KeyboardActions( - onDone = { onPhoneNumberSubmitted() } + onDone = { onPhoneNumberEntered() } ), singleLine = true, textStyle = MaterialTheme.typography.bodyLarge.copy( color = MaterialTheme.colorScheme.onSurface + ), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, + focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant ) ) } } -/** - * Simple dropdown triangle icon matching the symbol_dropdown_triangle_24 vector drawable. - */ -@Composable -private fun DropdownTriangle( - tint: Color, - modifier: Modifier = Modifier -) { - Canvas(modifier = modifier) { - val w = size.width - val h = size.height - val path = Path().apply { - // Triangle pointing down, centered in the 18x24 viewport - val scaleX = w / 18f - val scaleY = h / 24f - moveTo(5.2f * scaleX, 9.5f * scaleY) - lineTo(12.8f * scaleX, 9.5f * scaleY) - lineTo(9f * scaleX, 14.95f * scaleY) - close() - } - drawPath(path, tint) - } -} - @AllDevicePreviews @Composable private fun PhoneNumberScreenPreview() { diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreenEvents.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreenEvents.kt index b88d912f61..66a063ad5f 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreenEvents.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreenEvents.kt @@ -12,6 +12,8 @@ sealed class PhoneNumberEntryScreenEvents : DebugLoggableModel() { data class CountryCodeChanged(val value: String) : PhoneNumberEntryScreenEvents() data class PhoneNumberChanged(val value: String) : PhoneNumberEntryScreenEvents() data class CountrySelected(val countryCode: Int, val regionCode: String, val countryName: String, val countryEmoji: String) : PhoneNumberEntryScreenEvents() + data object PhoneNumberEntered : PhoneNumberEntryScreenEvents() + data object PhoneNumberCancelled : PhoneNumberEntryScreenEvents() data object PhoneNumberSubmitted : PhoneNumberEntryScreenEvents() data object CountryPicker : PhoneNumberEntryScreenEvents() data class CaptchaCompleted(val token: String) : PhoneNumberEntryScreenEvents() { 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 10e99b370f..dcc9ccbc2c 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 @@ -23,6 +23,7 @@ data class PhoneNumberEntryState( val sessionE164: String? = null, val sessionMetadata: SessionMetadata? = null, val showSpinner: Boolean = false, + val showDialog: Boolean = false, val oneTimeEvent: OneTimeEvent? = null, val preExistingRegistrationData: PreExistingRegistrationData? = null, val restoredSvrCredentials: List = emptyList(), 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 c529f0d127..bb8588b308 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 @@ -79,6 +79,12 @@ class PhoneNumberEntryViewModel( is PhoneNumberEntryScreenEvents.PhoneNumberChanged -> { stateEmitter(applyPhoneNumberChanged(state, event.value)) } + is PhoneNumberEntryScreenEvents.PhoneNumberEntered -> { + stateEmitter(state.copy(showDialog = true)) + } + is PhoneNumberEntryScreenEvents.PhoneNumberCancelled -> { + stateEmitter(state.copy(showDialog = false)) + } is PhoneNumberEntryScreenEvents.PhoneNumberSubmitted -> { var localState = state.copy(showSpinner = true) stateEmitter(localState) diff --git a/feature/registration/src/main/res/drawable/symbol_drop_down_24.xml b/feature/registration/src/main/res/drawable/symbol_drop_down_24.xml new file mode 100644 index 0000000000..9974df1ba8 --- /dev/null +++ b/feature/registration/src/main/res/drawable/symbol_drop_down_24.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/feature/registration/src/main/res/values/strings.xml b/feature/registration/src/main/res/values/strings.xml index 04c6a190d1..8ecb6281e8 100644 --- a/feature/registration/src/main/res/values/strings.xml +++ b/feature/registration/src/main/res/values/strings.xml @@ -42,6 +42,12 @@ Select a country Phone number Next + + Is the phone number below correct? + + A verification code will be sent to this number. Carrier rates may apply. + + Edit number