From 951c980eedd76874997b5383b3af17df05eeff80 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 15 Jun 2026 16:57:34 -0400 Subject: [PATCH] feat(phone): add IME autofill hints for phone number and OTP fields Add contentType parameter to TextInput and set PhoneNumber on PhoneInputField and SmsOtpCode on OtpInputField so the keyboard can suggest the user phone number and auto-read SMS OTP codes. Signed-off-by: Brandon McAnsh --- .../phone/PhoneVerificationViewModel.kt | 17 ++++++++++++++++- .../app/phone/components/OtpInputField.kt | 2 ++ .../app/phone/components/PhoneInputField.kt | 2 ++ .../com/getcode/ui/components/TextInput.kt | 15 +++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/internal/phone/PhoneVerificationViewModel.kt b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/internal/phone/PhoneVerificationViewModel.kt index 3b7aa8018..d5400125c 100644 --- a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/internal/phone/PhoneVerificationViewModel.kt +++ b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/internal/phone/PhoneVerificationViewModel.kt @@ -109,8 +109,23 @@ internal class PhoneVerificationViewModel @Inject constructor( .flatMapLatest { ts -> snapshotFlow { ts.text } } .distinctUntilChanged() .onEach { enteredNumber -> + val raw = enteredNumber.toString() + + // Handle autofilled international numbers (e.g. "+15551234567") + if (raw.contains("+")) { + val result = phoneUtils.parseInternationalNumber(raw) + if (result != null) { + val (locale, nationalNumber) = result + dispatchEvent(Event.OnCountrySelected(locale)) + stateFlow.value.numberTextFieldState.edit { + replace(0, length, nationalNumber) + } + return@onEach + } + } + val countryCode = stateFlow.value.selectedLocale.phoneCode.toString() - val phoneInputFiltered = enteredNumber.toString().replace("+$countryCode", "") + val phoneInputFiltered = raw.replace("+$countryCode", "") val phoneNumber = "+$countryCode$phoneInputFiltered" val formattedNumber = phoneUtils.formatNumber( number = phoneNumber, diff --git a/apps/flipcash/shared/phone/src/main/kotlin/com/flipcash/app/phone/components/OtpInputField.kt b/apps/flipcash/shared/phone/src/main/kotlin/com/flipcash/app/phone/components/OtpInputField.kt index abba90565..5fde12d4b 100644 --- a/apps/flipcash/shared/phone/src/main/kotlin/com/flipcash/app/phone/components/OtpInputField.kt +++ b/apps/flipcash/shared/phone/src/main/kotlin/com/flipcash/app/phone/components/OtpInputField.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import com.flipcash.app.theme.FlipcashPreview +import androidx.compose.ui.autofill.ContentType import com.getcode.theme.CodeTheme import com.getcode.theme.WindowSizeClass import com.getcode.ui.components.TextInput @@ -73,6 +74,7 @@ fun OtpInputField( } }, state = state, + contentType = ContentType.SmsOtpCode, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), ) diff --git a/apps/flipcash/shared/phone/src/main/kotlin/com/flipcash/app/phone/components/PhoneInputField.kt b/apps/flipcash/shared/phone/src/main/kotlin/com/flipcash/app/phone/components/PhoneInputField.kt index 5f13fb883..4a46c47b4 100644 --- a/apps/flipcash/shared/phone/src/main/kotlin/com/flipcash/app/phone/components/PhoneInputField.kt +++ b/apps/flipcash/shared/phone/src/main/kotlin/com/flipcash/app/phone/components/PhoneInputField.kt @@ -41,6 +41,7 @@ import com.flipcash.shared.phone.R import com.getcode.opencode.compose.ExchangeStub import com.getcode.opencode.compose.LocalExchange import com.getcode.theme.CodeTheme +import androidx.compose.ui.autofill.ContentType import com.getcode.ui.components.TextInput import com.getcode.ui.components.VerticalDivider import com.getcode.ui.core.rememberAnimationScale @@ -78,6 +79,7 @@ fun PhoneInputField( onKeyboardAction = { onSubmit() }, maxLines = 1, placeholder = placeholder, + contentType = ContentType.PhoneNumber + ContentType.PhoneNumberDevice, outputTransformation = outputTransformation, leadingIcon = { Row { diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/TextInput.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/TextInput.kt index 6c816040a..7beac47cd 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/TextInput.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/TextInput.kt @@ -42,6 +42,9 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.unit.Dp +import androidx.compose.ui.autofill.ContentType +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.getcode.theme.CodeTheme import com.getcode.theme.DesignSystem @@ -80,6 +83,7 @@ fun TextInput( constraintMode: ConstraintMode = ConstraintMode.Free, leadingIcon: (@Composable () -> Unit)? = null, trailingIcon: (@Composable () -> Unit)? = null, + contentType: ContentType? = null, scrollState: ScrollState = rememberScrollState(), ) { val backgroundColor by colors.backgroundColor(enabled = enabled) @@ -93,9 +97,20 @@ fun TextInput( var textSize by remember(style.fontSize) { mutableStateOf(style.fontSize) } + // Capture outside semantics lambda to avoid shadowing by + // SemanticsPropertyReceiver.contentType extension property. + val autofillContentType = contentType + Box(modifier = modifier) { BasicTextField( modifier = Modifier + .then( + if (autofillContentType != null) { + Modifier.semantics { this.contentType = autofillContentType } + } else { + Modifier + } + ) .background(backgroundColor, shape) .defaultMinSize(minHeight = minHeight) .constrain(