Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -87,7 +86,6 @@ fun RegisterDeviceScreen(
viewModel: RegisterDeviceViewModel = registerDeviceViewModel(),
clearSessionViewModel: ClearSessionViewModel = clearSessionViewModel(),
) {
clearAutofillTree()
when (val flowState = viewModel.state.flowState) {
is RegisterDeviceFlowState.Success -> {
navigator.navigate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -97,9 +96,6 @@ fun RemoveDeviceScreen(
backStackMode = BackStackMode.CLEAR_WHOLE
)
)

clearAutofillTree()

AnimatedContent(
targetState = viewModel.secondFactorVerificationCodeState.isCodeInputNecessary,
transitionSpec = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -96,9 +95,6 @@ fun LoginEmailScreen(
fillMaxHeight: Boolean = true,
) {
val scope = rememberCoroutineScope()

clearAutofillTree()

LoginEmailContent(
scrollState = scrollState,
loginEmailState = loginEmailViewModel.loginState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -110,7 +109,6 @@ fun NewLoginPasswordScreen(
navArgs: LoginNavArgs,
loginEmailViewModel: LoginEmailViewModel = loginEmailViewModel(navArgs)
) {
clearAutofillTree()
LoginStateNavigationAndDialogs(loginEmailViewModel, navigator)

LaunchedEffect(loginEmailViewModel.secondFactorVerificationCodeState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand All @@ -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,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AutofillType>) {
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),
}
Loading