diff --git a/app/src/androidTest/kotlin/com/wire/android/ui/common/textfield/WireTextFieldAutofillTest.kt b/app/src/androidTest/kotlin/com/wire/android/ui/common/textfield/WireTextFieldAutofillTest.kt new file mode 100644 index 00000000000..0c73ac6e3b2 --- /dev/null +++ b/app/src/androidTest/kotlin/com/wire/android/ui/common/textfield/WireTextFieldAutofillTest.kt @@ -0,0 +1,82 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.common.textfield + +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import com.wire.android.ui.WireTestTheme +import org.junit.Rule +import org.junit.Test + +class WireTextFieldAutofillTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun givenLoginAutofill_whenRendered_thenEmailAndUsernameContentTypeIsSet() { + composeTestRule.setContent { + WireTestTheme { + WireTextField( + textState = rememberTextFieldState(), + autoFillType = WireAutoFillType.Login, + testTag = FIELD_TAG, + ) + } + } + + composeTestRule + .onNodeWithTag(FIELD_TAG, useUnmergedTree = true) + .assert( + SemanticsMatcher.expectValue( + SemanticsProperties.ContentType, + requireNotNull(WireAutoFillType.Login.contentType), + ) + ) + } + + @Test + fun givenPasswordAutofill_whenRendered_thenPasswordContentTypeIsSet() { + composeTestRule.setContent { + WireTestTheme { + WirePasswordTextField( + textState = rememberTextFieldState(), + autoFill = true, + testTag = FIELD_TAG, + ) + } + } + + composeTestRule + .onNodeWithTag(FIELD_TAG, useUnmergedTree = true) + .assert( + SemanticsMatcher.expectValue( + SemanticsProperties.ContentType, + requireNotNull(WireAutoFillType.Password.contentType), + ) + ) + } + + private companion object { + const val FIELD_TAG = "autofillField" + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt index 879768685e7..c1da3882a8d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt @@ -62,7 +62,6 @@ import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.textfield.DefaultPassword import com.wire.android.ui.common.textfield.WirePasswordTextField import com.wire.android.ui.common.textfield.WireTextFieldState -import com.wire.android.ui.common.textfield.clearAutofillTree import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState @@ -87,7 +86,6 @@ fun RegisterDeviceScreen( viewModel: RegisterDeviceViewModel = registerDeviceViewModel(), clearSessionViewModel: ClearSessionViewModel = clearSessionViewModel(), ) { - clearAutofillTree() when (val flowState = viewModel.state.flowState) { is RegisterDeviceFlowState.Success -> { navigator.navigate( diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt index 674b4a303f6..d54bc39b2c1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt @@ -62,7 +62,6 @@ import com.wire.android.ui.common.dialogs.CancelLoginDialogState import com.wire.android.ui.common.divider.WireDivider import com.wire.android.ui.common.rememberTopBarElevationState import com.wire.android.ui.common.scaffold.WireScaffold -import com.wire.android.ui.common.textfield.clearAutofillTree import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.common.wireDialogPropertiesBuilder import com.ramcosta.composedestinations.generated.app.destinations.E2EIEnrollmentScreenDestination @@ -97,9 +96,6 @@ fun RemoveDeviceScreen( backStackMode = BackStackMode.CLEAR_WHOLE ) ) - - clearAutofillTree() - AnimatedContent( targetState = viewModel.secondFactorVerificationCodeState.isCodeInputNecessary, transitionSpec = { diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt index ae51e73e5ae..4741b51005e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt @@ -75,7 +75,6 @@ import com.wire.android.ui.common.textfield.WireAutoFillType import com.wire.android.ui.common.textfield.WirePasswordTextField import com.wire.android.ui.common.textfield.WireTextField import com.wire.android.ui.common.textfield.WireTextFieldState -import com.wire.android.ui.common.textfield.clearAutofillTree import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme @@ -96,9 +95,6 @@ fun LoginEmailScreen( fillMaxHeight: Boolean = true, ) { val scope = rememberCoroutineScope() - - clearAutofillTree() - LoginEmailContent( scrollState = scrollState, loginEmailState = loginEmailViewModel.loginState, diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/code/NewLoginVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/code/NewLoginVerificationCodeScreen.kt index d5345da35ab..654e7973947 100644 --- a/app/src/main/kotlin/com/wire/android/ui/newauthentication/code/NewLoginVerificationCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/code/NewLoginVerificationCodeScreen.kt @@ -39,7 +39,6 @@ import com.wire.android.ui.authentication.verificationcode.VerificationCode import com.wire.android.ui.authentication.verificationcode.VerificationCodeState import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.EdgeToEdgePreview -import com.wire.android.ui.common.textfield.clearAutofillTree import com.wire.android.ui.newauthentication.login.NewAuthContainer import com.wire.android.ui.newauthentication.login.NewAuthHeader import com.wire.android.ui.newauthentication.login.NewAuthSubtitle @@ -59,7 +58,6 @@ fun NewLoginVerificationCodeScreen( navigator: Navigator, loginEmailViewModel: LoginEmailViewModel, // provided in MainNavHost to reuse from NewLoginPasswordScreen, don't use wireViewModel() ) { - clearAutofillTree() LoginStateNavigationAndDialogs(loginEmailViewModel, navigator) LaunchedEffect(loginEmailViewModel) { diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt index 485075677d3..ca6964f3bc8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt @@ -83,7 +83,6 @@ import com.wire.android.ui.common.textfield.WireAutoFillType import com.wire.android.ui.common.textfield.WirePasswordTextField import com.wire.android.ui.common.textfield.WireTextField import com.wire.android.ui.common.textfield.WireTextFieldState -import com.wire.android.ui.common.textfield.clearAutofillTree import com.wire.android.ui.common.typography import com.wire.android.ui.common.visbility.rememberVisibilityState import com.ramcosta.composedestinations.generated.app.destinations.CreateAccountSelectorScreenDestination @@ -110,7 +109,6 @@ fun NewLoginPasswordScreen( navArgs: LoginNavArgs, loginEmailViewModel: LoginEmailViewModel = loginEmailViewModel(navArgs) ) { - clearAutofillTree() LoginStateNavigationAndDialogs(loginEmailViewModel, navigator) LaunchedEffect(loginEmailViewModel.secondFactorVerificationCodeState) { diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt index dec3e04116d..e32fd6c97c8 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WirePasswordTextField.kt @@ -105,7 +105,7 @@ fun WirePasswordTextField( shape = shape, colors = colors, trailingIcon = { VisibilityIconButton(passwordVisibility) { passwordVisibility = it } }, - modifier = modifier.then(autoFillModifier(autoFillType, textState::setTextAndPlaceCursorAtEnd)), + modifier = modifier, testTag = testTag, onTap = onTap, innerBasicTextField = { decorator, textFieldModifier -> @@ -119,7 +119,7 @@ fun WirePasswordTextField( enabled = state !is WireTextFieldState.Disabled, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), interactionSource = interactionSource, - modifier = textFieldModifier, + modifier = textFieldModifier.applyAutofill(autoFillType), decorator = decorator, ) } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt index 0011eeb3a8f..59ca64e29b6 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt @@ -129,12 +129,7 @@ fun WireTextField( inputMinHeight = inputMinHeight, shape = shape, colors = colors, - modifier = modifier.then( - autoFillModifier( - autoFillType, - textState::setTextAndPlaceCursorAtEnd - ) - ), + modifier = modifier, onInputSizeChanged = onInputSizeChanged, onTap = onTap, testTag = testTag, @@ -155,7 +150,9 @@ fun WireTextField( enabled = enabled, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), interactionSource = interactionSource, - modifier = textFieldModifier.then(inputModifier), + modifier = textFieldModifier + .then(inputModifier) + .applyAutofill(autoFillType), decorator = decorator, onTextLayout = onTextLayout( textState, diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldAutoFill.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldAutoFill.kt index 6f2de4cf788..4759f4f5ba7 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldAutoFill.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextFieldAutoFill.kt @@ -15,61 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -@file:OptIn(ExperimentalComposeUiApi::class) - package com.wire.android.ui.common.textfield -import androidx.compose.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.Autofill -import androidx.compose.ui.autofill.AutofillNode -import androidx.compose.ui.autofill.AutofillType -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalAutofill -import androidx.compose.ui.platform.LocalAutofillTree +import androidx.compose.ui.autofill.ContentType +import androidx.compose.ui.autofill.contentType import io.github.esentsov.PackagePrivate -@OptIn(ExperimentalComposeUiApi::class) @PackagePrivate -@Composable -internal fun autoFillModifier(type: WireAutoFillType, onFill: ((String) -> Unit)) = if (type.autoFillTypes.isNotEmpty()) { - val autofillNode = AutofillNode( - autofillTypes = type.autoFillTypes, - onFill = onFill, - ) - LocalAutofillTree.current += autofillNode - Modifier - .fillBounds(autofillNode) - .defaultOnFocusAutoFill(LocalAutofill.current, autofillNode) -} else { - Modifier -} - -@OptIn(ExperimentalComposeUiApi::class) -private fun Modifier.fillBounds(autofillNode: AutofillNode) = this.then( - Modifier.onGloballyPositioned { autofillNode.boundingBox = it.boundsInWindow() } -) - -private fun Modifier.defaultOnFocusAutoFill(autofill: Autofill?, autofillNode: AutofillNode): Modifier = - then( - Modifier.onFocusChanged { - focusState -> - if (focusState.isFocused) { - autofill?.requestAutofillForNode(autofillNode) - } else { - autofill?.cancelAutofillForNode(autofillNode) - } - } - ) - -@Composable -fun clearAutofillTree() = LocalAutofillTree.current.children.clear() +internal fun Modifier.applyAutofill(type: WireAutoFillType): Modifier = + type.contentType?.let { contentType(it) } ?: this -enum class WireAutoFillType(val autoFillTypes: List) { - None(emptyList()), - Login(listOf(AutofillType.EmailAddress, AutofillType.Username)), - Password(listOf(AutofillType.Password)), +enum class WireAutoFillType(val contentType: ContentType?) { + None(null), + Login(ContentType.EmailAddress + ContentType.Username), + Password(ContentType.Password), }