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 9b53a3d154c..5d6fcdfa1c8 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 @@ -24,6 +24,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -33,8 +35,6 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination @@ -53,6 +53,7 @@ import com.wire.android.ui.common.dialogs.CancelLoginDialogContent import com.wire.android.ui.common.dialogs.CancelLoginDialogState import com.wire.android.ui.common.error.CoreFailureErrorDialog 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 @@ -63,18 +64,21 @@ import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.InitialSyncScreenDestination import com.wire.android.ui.destinations.RemoveDeviceScreenDestination +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes @RootNavGraph @Destination( style = PopUpNavigationAnimation::class, ) @Composable -fun RegisterDeviceScreen(navigator: Navigator) { - val viewModel: RegisterDeviceViewModel = hiltViewModel() - val clearSessionViewModel: ClearSessionViewModel = hiltViewModel() - val clearSessionState: ClearSessionState = clearSessionViewModel.state +fun RegisterDeviceScreen( + navigator: Navigator, + viewModel: RegisterDeviceViewModel = hiltViewModel(), + clearSessionViewModel: ClearSessionViewModel = hiltViewModel(), +) { clearAutofillTree() when (val flowState = viewModel.state.flowState) { is RegisterDeviceFlowState.Success -> { @@ -92,8 +96,8 @@ fun RegisterDeviceScreen(navigator: Navigator) { else -> RegisterDeviceContent( state = viewModel.state, - clearSessionState = clearSessionState, - onPasswordChange = viewModel::onPasswordChange, + passwordTextState = viewModel.passwordTextState, + clearSessionState = clearSessionViewModel.state, onContinuePressed = viewModel::onContinue, onErrorDismiss = viewModel::onErrorDismiss, onBackButtonClicked = clearSessionViewModel::onBackButtonClicked, @@ -106,13 +110,14 @@ fun RegisterDeviceScreen(navigator: Navigator) { @Composable private fun RegisterDeviceContent( state: RegisterDeviceState, + passwordTextState: TextFieldState, clearSessionState: ClearSessionState, - onPasswordChange: (TextFieldValue) -> Unit, onContinuePressed: () -> Unit, onErrorDismiss: () -> Unit, onBackButtonClicked: () -> Unit, onCancelLoginClicked: () -> Unit, - onProceedLoginClicked: () -> Unit + onProceedLoginClicked: () -> Unit, + modifier: Modifier = Modifier, ) { BackHandler { onBackButtonClicked() @@ -136,6 +141,7 @@ private fun RegisterDeviceContent( } WireScaffold( + modifier = modifier, topBar = { WireCenterAlignedTopAppBar( elevation = 0.dp, @@ -161,7 +167,7 @@ private fun RegisterDeviceContent( ) .testTag("registerText") ) - PasswordTextField(state = state, onPasswordChange = onPasswordChange) + PasswordTextField(state = state, passwordTextState = passwordTextState) Spacer(modifier = Modifier.weight(1f)) WirePrimaryButton( text = stringResource(R.string.label_add_device), @@ -182,28 +188,31 @@ private fun RegisterDeviceContent( } @Composable -private fun PasswordTextField(state: RegisterDeviceState, onPasswordChange: (TextFieldValue) -> Unit) { +private fun PasswordTextField( + state: RegisterDeviceState, + passwordTextState: TextFieldState, + modifier: Modifier = Modifier, +) { val keyboardController = LocalSoftwareKeyboardController.current WirePasswordTextField( - value = state.password, - onValueChange = onPasswordChange, + textState = passwordTextState, state = when (state.flowState) { is RegisterDeviceFlowState.Error.InvalidCredentialsError -> WireTextFieldState.Error(stringResource(id = R.string.remove_device_invalid_password)) else -> WireTextFieldState.Default }, - imeAction = ImeAction.Done, - onImeAction = { keyboardController?.hide() }, - modifier = Modifier + keyboardOptions = KeyboardOptions.DefaultPassword.copy(imeAction = ImeAction.Done), + onKeyboardAction = { keyboardController?.hide() }, + modifier = modifier .padding(horizontal = MaterialTheme.wireDimensions.spacing16x) .testTag("password field"), - autofill = true + autoFill = true ) } @Composable -@Preview -fun PreviewRegisterDeviceScreen() { - RegisterDeviceContent(RegisterDeviceState(), ClearSessionState(), {}, {}, {}, {}, {}, {}) +@PreviewMultipleThemes +fun PreviewRegisterDeviceScreen() = WireTheme { + RegisterDeviceContent(RegisterDeviceState(), TextFieldState(), ClearSessionState(), {}, {}, {}, {}, {}) } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt index 766959596da..2b91564d32f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt @@ -18,21 +18,19 @@ package com.wire.android.ui.authentication.devices.register -import androidx.compose.ui.text.input.TextFieldValue import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.user.UserId data class RegisterDeviceState( - val password: TextFieldValue = TextFieldValue(""), val continueEnabled: Boolean = false, val flowState: RegisterDeviceFlowState = RegisterDeviceFlowState.Default ) sealed class RegisterDeviceFlowState { - object Default : RegisterDeviceFlowState() - object Loading : RegisterDeviceFlowState() - object TooManyDevices : RegisterDeviceFlowState() + data object Default : RegisterDeviceFlowState() + data object Loading : RegisterDeviceFlowState() + data object TooManyDevices : RegisterDeviceFlowState() data class Success( val initialSyncCompleted: Boolean, val isE2EIRequired: Boolean, @@ -41,7 +39,7 @@ sealed class RegisterDeviceFlowState { ) : RegisterDeviceFlowState() sealed class Error : RegisterDeviceFlowState() { - object InvalidCredentialsError : Error() + data object InvalidCredentialsError : Error() data class GenericError(val coreFailure: CoreFailure) : Error() } } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt index 55336d9efe5..9f1145fef60 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt @@ -18,19 +18,22 @@ package com.wire.android.ui.authentication.devices.register +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.BuildConfig import com.wire.android.datastore.UserDataStore +import com.wire.android.ui.common.textfield.textAsFlow import com.wire.kalium.logic.feature.client.GetOrRegisterClientUseCase import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.client.RegisterClientUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -43,6 +46,7 @@ class RegisterDeviceViewModel @Inject constructor( private val userDataStore: UserDataStore, ) : ViewModel() { + val passwordTextState: TextFieldState = TextFieldState() var state: RegisterDeviceState by mutableStateOf(RegisterDeviceState()) private set @@ -62,11 +66,10 @@ class RegisterDeviceViewModel @Inject constructor( } } } - } - - fun onPasswordChange(newText: TextFieldValue) { - if (state.password != newText) { - state = state.copy(password = newText, flowState = RegisterDeviceFlowState.Default, continueEnabled = newText.text.isNotEmpty()) + viewModelScope.launch { + passwordTextState.textAsFlow().distinctUntilChanged().collectLatest { + state = state.copy(flowState = RegisterDeviceFlowState.Default, continueEnabled = it.isNotEmpty()) + } } } @@ -122,7 +125,7 @@ class RegisterDeviceViewModel @Inject constructor( fun onContinue() { viewModelScope.launch { - registerClient(state.password.text) + registerClient(passwordTextState.text.toString()) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceDialog.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceDialog.kt index 2063d3f6520..a6f09330ea3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceDialog.kt @@ -18,6 +18,8 @@ package com.wire.android.ui.authentication.devices.remove import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -30,12 +32,12 @@ import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue import com.wire.android.R import com.wire.android.ui.common.WireDialog import com.wire.android.ui.common.WireDialogButtonProperties import com.wire.android.ui.common.WireDialogButtonType import com.wire.android.ui.common.button.WireButtonState +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.theme.wireDimensions @@ -45,9 +47,10 @@ import com.wire.android.util.deviceDateTimeFormat fun RemoveDeviceDialog( errorState: RemoveDeviceError, state: RemoveDeviceDialogState.Visible, - onPasswordChange: (TextFieldValue) -> Unit, + passwordTextState: TextFieldState, onDialogDismiss: () -> Unit, - onRemoveConfirm: () -> Unit + onRemoveConfirm: () -> Unit, + modifier: Modifier = Modifier, ) { var keyboardController: SoftwareKeyboardController? = null val onDialogDismissHideKeyboard: () -> Unit = { @@ -55,6 +58,7 @@ fun RemoveDeviceDialog( onDialogDismiss() } WireDialog( + modifier = modifier, title = stringResource(R.string.remove_device_dialog_title), text = state.device.name.asString() + "\n" + stringResource( @@ -84,8 +88,7 @@ fun RemoveDeviceDialog( keyboardController = LocalSoftwareKeyboardController.current val focusRequester = remember { FocusRequester() } WirePasswordTextField( - value = state.password, - onValueChange = onPasswordChange, + textState = passwordTextState, state = when { errorState is RemoveDeviceError.InvalidCredentialsError -> WireTextFieldState.Error(stringResource(id = R.string.remove_device_invalid_password)) @@ -93,13 +96,13 @@ fun RemoveDeviceDialog( state.loading -> WireTextFieldState.Disabled else -> WireTextFieldState.Default }, - imeAction = ImeAction.Done, - onImeAction = { keyboardController?.hide() }, + keyboardOptions = KeyboardOptions.DefaultPassword.copy(imeAction = ImeAction.Done), + onKeyboardAction = { keyboardController?.hide() }, modifier = Modifier .focusRequester(focusRequester) .padding(bottom = MaterialTheme.wireDimensions.spacing8x) .testTag("remove device password field"), - autofill = true + autoFill = true ) LaunchedEffect(Unit) { // executed only once when showing the dialog focusRequester.requestFocus() 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 ae337f60e78..2469ff4e62e 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 @@ -26,15 +26,13 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.Icon -import com.wire.android.ui.common.scaffold.WireScaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph @@ -56,24 +54,26 @@ import com.wire.android.ui.common.dialogs.CancelLoginDialogContent 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.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.InitialSyncScreenDestination +import com.wire.android.ui.theme.WireTheme import com.wire.android.util.dialogErrorStrings +import com.wire.android.util.ui.PreviewMultipleThemes @RootNavGraph @Destination( style = PopUpNavigationAnimation::class, ) @Composable -fun RemoveDeviceScreen(navigator: Navigator) { - val viewModel: RemoveDeviceViewModel = hiltViewModel() - val clearSessionViewModel: ClearSessionViewModel = hiltViewModel() - val state: RemoveDeviceState = viewModel.state - val clearSessionState: ClearSessionState = clearSessionViewModel.state - +fun RemoveDeviceScreen( + navigator: Navigator, + viewModel: RemoveDeviceViewModel = hiltViewModel(), + clearSessionViewModel: ClearSessionViewModel = hiltViewModel(), +) { fun navigateAfterSuccess(initialSyncCompleted: Boolean, isE2EIRequired: Boolean) = navigator.navigate( NavigationCommand( destination = if (isE2EIRequired) E2EIEnrollmentScreenDestination @@ -85,10 +85,10 @@ fun RemoveDeviceScreen(navigator: Navigator) { clearAutofillTree() RemoveDeviceContent( - state = state, - clearSessionState = clearSessionState, + state = viewModel.state, + passwordTextState = viewModel.passwordTextState, + clearSessionState = clearSessionViewModel.state, onItemClicked = { viewModel.onItemClicked(it, ::navigateAfterSuccess) }, - onPasswordChange = viewModel::onPasswordChange, onRemoveConfirm = { viewModel.onRemoveConfirmed(::navigateAfterSuccess) }, onDialogDismiss = viewModel::onDialogDismissed, onErrorDialogDismiss = viewModel::clearDeleteClientError, @@ -101,15 +101,16 @@ fun RemoveDeviceScreen(navigator: Navigator) { @Composable private fun RemoveDeviceContent( state: RemoveDeviceState, + passwordTextState: TextFieldState, clearSessionState: ClearSessionState, onItemClicked: (Device) -> Unit, - onPasswordChange: (TextFieldValue) -> Unit, onRemoveConfirm: () -> Unit, onDialogDismiss: () -> Unit, onErrorDialogDismiss: () -> Unit, onBackButtonClicked: () -> Unit, onCancelLoginClicked: () -> Unit, - onProceedLoginClicked: () -> Unit + onProceedLoginClicked: () -> Unit, + modifier: Modifier = Modifier ) { BackHandler { onBackButtonClicked() @@ -133,12 +134,15 @@ private fun RemoveDeviceContent( } val lazyListState = rememberLazyListState() - WireScaffold(topBar = { - RemoveDeviceTopBar( - elevation = lazyListState.rememberTopBarElevationState().value, - onBackButtonClicked = onBackButtonClicked - ) - }) { internalPadding -> + WireScaffold( + modifier = modifier, + topBar = { + RemoveDeviceTopBar( + elevation = lazyListState.rememberTopBarElevationState().value, + onBackButtonClicked = onBackButtonClicked + ) + } + ) { internalPadding -> Box(modifier = Modifier.padding(internalPadding)) { when (state.isLoadingClientsList) { true -> RemoveDeviceItemsList(lazyListState, List(10) { Device() }, true, onItemClicked) @@ -150,7 +154,7 @@ private fun RemoveDeviceContent( RemoveDeviceDialog( errorState = state.error, state = state.removeDeviceDialogState, - onPasswordChange = onPasswordChange, + passwordTextState = passwordTextState, onDialogDismiss = onDialogDismiss, onRemoveConfirm = onRemoveConfirm ) @@ -203,18 +207,18 @@ private fun RemoveDeviceItemsList( } } -@Preview +@PreviewMultipleThemes @Composable -fun PreviewRemoveDeviceScreen() { +fun PreviewRemoveDeviceScreen() = WireTheme { RemoveDeviceContent( state = RemoveDeviceState( List(10) { Device() }, RemoveDeviceDialogState.Hidden, isLoadingClientsList = false ), + passwordTextState = TextFieldState(), clearSessionState = ClearSessionState(), onItemClicked = {}, - onPasswordChange = {}, onRemoveConfirm = {}, onDialogDismiss = {}, onErrorDialogDismiss = {}, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceState.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceState.kt index 930aff2e652..0713c8bf565 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceState.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.authentication.devices.remove -import androidx.compose.ui.text.input.TextFieldValue import com.wire.android.ui.authentication.devices.model.Device import com.wire.kalium.logic.CoreFailure @@ -30,18 +29,17 @@ data class RemoveDeviceState( ) sealed class RemoveDeviceDialogState { - object Hidden : RemoveDeviceDialogState() + data object Hidden : RemoveDeviceDialogState() data class Visible( val device: Device, - val password: TextFieldValue = TextFieldValue(""), val loading: Boolean = false, val removeEnabled: Boolean = false ) : RemoveDeviceDialogState() } sealed class RemoveDeviceError { - object None : RemoveDeviceError() - object InvalidCredentialsError : RemoveDeviceError() - object InitError : RemoveDeviceError() + data object None : RemoveDeviceError() + data object InvalidCredentialsError : RemoveDeviceError() + data object InitError : RemoveDeviceError() data class GenericError(val coreFailure: CoreFailure) : RemoveDeviceError() } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt index 8b032a358f5..6eaf697c97e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt @@ -18,15 +18,17 @@ package com.wire.android.ui.authentication.devices.remove +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.BuildConfig import com.wire.android.datastore.UserDataStore import com.wire.android.ui.authentication.devices.model.Device +import com.wire.android.ui.common.textfield.textAsFlow import com.wire.kalium.logic.data.client.ClientType import com.wire.kalium.logic.data.client.DeleteClientParam import com.wire.kalium.logic.feature.client.DeleteClientResult @@ -39,6 +41,8 @@ import com.wire.kalium.logic.feature.client.SelfClientsResult import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -52,6 +56,7 @@ class RemoveDeviceViewModel @Inject constructor( private val userDataStore: UserDataStore ) : ViewModel() { + val passwordTextState: TextFieldState = TextFieldState() var state: RemoveDeviceState by mutableStateOf( RemoveDeviceState(deviceList = listOf(), removeDeviceDialogState = RemoveDeviceDialogState.Hidden, isLoadingClientsList = true) ) @@ -59,6 +64,20 @@ class RemoveDeviceViewModel @Inject constructor( init { loadClientsList() + observePasswordTextChanges() + } + + private fun observePasswordTextChanges() { + viewModelScope.launch { + passwordTextState.textAsFlow().distinctUntilChanged().collectLatest { newPassword -> + updateStateIfDialogVisible { + state.copy( + removeDeviceDialogState = it.copy(removeEnabled = newPassword.isNotEmpty()), + error = RemoveDeviceError.None + ) + } + } + } } private fun loadClientsList() { @@ -75,17 +94,8 @@ class RemoveDeviceViewModel @Inject constructor( } } - fun onPasswordChange(newText: TextFieldValue) { - updateStateIfDialogVisible { - if (it.password == newText) state - else state.copy( - removeDeviceDialogState = it.copy(password = newText, removeEnabled = newText.text.isNotEmpty()), - error = RemoveDeviceError.None - ) - } - } - fun onDialogDismissed() { + passwordTextState.clearText() updateStateIfDialogVisible { state.copy(removeDeviceDialogState = RemoveDeviceDialogState.Hidden) } } @@ -160,13 +170,14 @@ class RemoveDeviceViewModel @Inject constructor( (state.removeDeviceDialogState as? RemoveDeviceDialogState.Visible)?.let { dialogStateVisible -> updateStateIfDialogVisible { state.copy(removeDeviceDialogState = it.copy(loading = true, removeEnabled = false)) } viewModelScope.launch { - deleteClient(dialogStateVisible.password.text, dialogStateVisible.device, onCompleted) + deleteClient(passwordTextState.text.toString(), dialogStateVisible.device, onCompleted) updateStateIfDialogVisible { state.copy(removeDeviceDialogState = it.copy(loading = false)) } } } } private fun showDeleteClientDialog(device: Device) { + passwordTextState.clearText() state = state.copy( error = RemoveDeviceError.None, removeDeviceDialogState = RemoveDeviceDialogState.Visible( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeResetDeviceDialog.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeResetDeviceDialog.kt index f654c8ede74..b329cd34057 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeResetDeviceDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeResetDeviceDialog.kt @@ -18,19 +18,17 @@ package com.wire.android.ui.home.appLock.forgot import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.window.DialogProperties import com.wire.android.R import com.wire.android.ui.common.WireDialog @@ -39,6 +37,7 @@ import com.wire.android.ui.common.WireDialogButtonType import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions +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.theme.WireTheme @@ -48,21 +47,22 @@ import com.wire.android.util.ui.stringWithStyledArgs @Composable fun ForgotLockCodeResetDeviceDialog( + passwordTextState: TextFieldState, username: String, isPasswordRequired: Boolean, isPasswordValid: Boolean, isResetDeviceEnabled: Boolean, - onPasswordChanged: (TextFieldValue) -> Unit, onResetDeviceClicked: () -> Unit, - onDialogDismissed: () -> Unit + onDialogDismissed: () -> Unit, + modifier: Modifier = Modifier, ) { - var backupPassword by remember { mutableStateOf(TextFieldValue("")) } var keyboardController: SoftwareKeyboardController? = null val onDialogDismissHideKeyboard: () -> Unit = { keyboardController?.hide() onDialogDismissed() } WireDialog( + modifier = modifier, title = stringResource(R.string.settings_forgot_lock_screen_reset_device), text = if (isPasswordRequired) { LocalContext.current.resources.stringWithStyledArgs( @@ -98,17 +98,14 @@ fun ForgotLockCodeResetDeviceDialog( // to the dialog's content and use keyboard controller from there keyboardController = LocalSoftwareKeyboardController.current WirePasswordTextField( + textState = passwordTextState, state = when { !isPasswordValid -> WireTextFieldState.Error(stringResource(id = R.string.remove_device_invalid_password)) else -> WireTextFieldState.Default }, - value = backupPassword, - onValueChange = { - backupPassword = it - onPasswordChanged(it) - }, - autofill = false, - onImeAction = { keyboardController?.hide() }, + autoFill = false, + keyboardOptions = KeyboardOptions.DefaultPassword.copy(imeAction = ImeAction.Done), + onKeyboardAction = { keyboardController?.hide() }, modifier = Modifier.padding(bottom = dimensions().spacing16x) ) } @@ -129,7 +126,7 @@ fun ForgotLockCodeResettingDeviceDialog() { @Composable fun PreviewForgotLockCodeResetDeviceDialog() { WireTheme { - ForgotLockCodeResetDeviceDialog("Username", false, true, true, {}, {}, {}) + ForgotLockCodeResetDeviceDialog(TextFieldState(), "Username", false, true, true, {}, {}) } } @@ -137,7 +134,7 @@ fun PreviewForgotLockCodeResetDeviceDialog() { @Composable fun PreviewForgotLockCodeResetDeviceWithoutPasswordDialog() { WireTheme { - ForgotLockCodeResetDeviceDialog("Username", true, true, true, {}, {}, {}) + ForgotLockCodeResetDeviceDialog(TextFieldState(), "Username", true, true, true, {}, {}) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt index 23add6063a8..b0f79361086 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt @@ -83,16 +83,19 @@ fun ForgotLockCodeScreen( onResetDevice = viewModel::onResetDevice, ) if (dialogState is ForgotLockCodeDialogState.Visible) { - if (dialogState.loading) ForgotLockCodeResettingDeviceDialog() - else ForgotLockCodeResetDeviceDialog( - username = dialogState.username, - isPasswordRequired = dialogState.passwordRequired, - isPasswordValid = dialogState.passwordValid, - isResetDeviceEnabled = dialogState.resetDeviceEnabled, - onPasswordChanged = viewModel::onPasswordChanged, - onResetDeviceClicked = viewModel::onResetDeviceConfirmed, - onDialogDismissed = viewModel::onDialogDismissed, - ) + if (dialogState.loading) { + ForgotLockCodeResettingDeviceDialog() + } else { + ForgotLockCodeResetDeviceDialog( + passwordTextState = viewModel.passwordTextState, + username = dialogState.username, + isPasswordRequired = dialogState.passwordRequired, + isPasswordValid = dialogState.passwordValid, + isResetDeviceEnabled = dialogState.resetDeviceEnabled, + onResetDeviceClicked = viewModel::onResetDeviceConfirmed, + onDialogDismissed = viewModel::onDialogDismissed, + ) + } } if (error != null) { val (title, message) = error.dialogErrorStrings(LocalContext.current.resources) @@ -115,10 +118,11 @@ fun ForgotLockCodeScreen( fun ForgotLockCodeScreenContent( scrollState: ScrollState, onResetDevice: () -> Unit, + modifier: Modifier = Modifier, ) { WireScaffold { internalPadding -> Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .padding(internalPadding) ) { @@ -183,9 +187,9 @@ fun ForgotLockCodeScreenContent( @Composable private fun ContinueButton( - modifier: Modifier = Modifier.fillMaxWidth(), enabled: Boolean, - onContinue: () -> Unit + onContinue: () -> Unit, + modifier: Modifier = Modifier.fillMaxWidth(), ) { val interactionSource = remember { MutableInteractionSource() } Column(modifier = modifier) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeViewState.kt index 64e5da07567..8439787349c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeViewState.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.home.appLock.forgot -import androidx.compose.ui.text.input.TextFieldValue import com.wire.kalium.logic.CoreFailure data class ForgotLockCodeViewState( @@ -30,7 +29,6 @@ sealed class ForgotLockCodeDialogState { data object Hidden : ForgotLockCodeDialogState() data class Visible( val username: String, - val password: TextFieldValue = TextFieldValue(""), val passwordRequired: Boolean = false, val passwordValid: Boolean = true, val resetDeviceEnabled: Boolean = false, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt index 60d2e6e500d..c863378b3a2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt @@ -18,10 +18,11 @@ package com.wire.android.ui.home.appLock.forgot import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger @@ -30,8 +31,8 @@ import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.KaliumCoreLogic import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.SwitchAccountParam -import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager +import com.wire.android.ui.common.textfield.textAsFlow import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.auth.AccountInfo @@ -49,6 +50,7 @@ import com.wire.kalium.logic.feature.session.GetSessionsUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull @@ -62,7 +64,6 @@ class ForgotLockScreenViewModel @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, private val globalDataStore: GlobalDataStore, private val userDataStoreProvider: UserDataStoreProvider, - private val notificationChannelsManager: NotificationChannelsManager, private val notificationManager: WireNotificationManager, private val getSelf: GetSelfUserUseCase, private val isPasswordRequired: IsPasswordRequiredUseCase, @@ -75,20 +76,26 @@ class ForgotLockScreenViewModel @Inject constructor( private val accountSwitch: AccountSwitchUseCase, ) : ViewModel() { + val passwordTextState: TextFieldState = TextFieldState() var state: ForgotLockCodeViewState by mutableStateOf(ForgotLockCodeViewState()) private set + init { + viewModelScope.launch { + passwordTextState.textAsFlow().collectLatest { newPassword -> + updateIfDialogStateVisible { it.copy(resetDeviceEnabled = newPassword.isNotBlank()) } + } + } + } + private fun updateIfDialogStateVisible(update: (ForgotLockCodeDialogState.Visible) -> ForgotLockCodeDialogState) { (state.dialogState as? ForgotLockCodeDialogState.Visible)?.let { dialogStateVisible -> state = state.copy(dialogState = update(dialogStateVisible)) } } - fun onPasswordChanged(password: TextFieldValue) { - updateIfDialogStateVisible { it.copy(password = password, resetDeviceEnabled = password.text.isNotBlank()) } - } - fun onResetDevice() { + passwordTextState.clearText() viewModelScope.launch { state = when (val isPasswordRequiredResult = isPasswordRequired()) { is IsPasswordRequiredUseCase.Result.Success -> { @@ -109,6 +116,7 @@ class ForgotLockScreenViewModel @Inject constructor( } fun onDialogDismissed() { + passwordTextState.clearText() state = state.copy(dialogState = ForgotLockCodeDialogState.Hidden) } @@ -120,7 +128,7 @@ class ForgotLockScreenViewModel @Inject constructor( (state.dialogState as? ForgotLockCodeDialogState.Visible)?.let { dialogStateVisible -> updateIfDialogStateVisible { it.copy(resetDeviceEnabled = false) } viewModelScope.launch { - validatePasswordIfNeeded(dialogStateVisible.password.text) + validatePasswordIfNeeded(passwordTextState.text.toString()) .flatMapIfSuccess { validatedPassword -> updateIfDialogStateVisible { it.copy(loading = true) } deleteCurrentClient(validatedPassword) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeScreen.kt index 8890c22670e..a3afe0a4dc5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeScreen.kt @@ -29,6 +29,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -46,7 +48,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph @@ -60,6 +61,7 @@ import com.wire.android.ui.common.rememberBottomBarElevationState import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.spacers.HorizontalSpace import com.wire.android.ui.common.spacers.VerticalSpace +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.topappbar.NavigationIconType @@ -77,14 +79,14 @@ import java.util.Locale @Destination @Composable fun SetLockCodeScreen( + navigator: Navigator, viewModel: SetLockScreenViewModel = hiltViewModel(), - navigator: Navigator ) { SetLockCodeScreenContent( navigator = navigator, state = viewModel.state, + passwordTextState = viewModel.passwordTextState, scrollState = rememberScrollState(), - onPasswordChanged = viewModel::onPasswordChanged, onBackPress = navigator::navigateBack, onContinue = viewModel::onContinue ) @@ -95,10 +97,11 @@ fun SetLockCodeScreen( fun SetLockCodeScreenContent( navigator: Navigator, state: SetLockCodeViewState, + passwordTextState: TextFieldState, scrollState: ScrollState, - onPasswordChanged: (TextFieldValue) -> Unit, - onBackPress: () -> Unit = {}, - onContinue: () -> Unit + onBackPress: () -> Unit, + onContinue: () -> Unit, + modifier: Modifier = Modifier, ) { LaunchedEffect(state.done) { if (state.done) { @@ -107,6 +110,7 @@ fun SetLockCodeScreenContent( } WireScaffold( + modifier = modifier, snackbarHost = {}, topBar = { WireCenterAlignedTopAppBar( @@ -115,7 +119,8 @@ fun SetLockCodeScreenContent( elevation = dimensions().spacing0x, title = stringResource(id = R.string.settings_set_lock_screen_title) ) - }) { internalPadding -> + } + ) { internalPadding -> Column( modifier = Modifier .fillMaxSize() @@ -144,14 +149,13 @@ fun SetLockCodeScreenContent( .testTag("registerText") ) WirePasswordTextField( - value = state.password, - onValueChange = onPasswordChanged, + textState = passwordTextState, labelMandatoryIcon = true, - imeAction = ImeAction.Done, + keyboardOptions = KeyboardOptions.DefaultPassword.copy(imeAction = ImeAction.Done), modifier = Modifier .testTag("password"), state = WireTextFieldState.Default, - autofill = false, + autoFill = false, placeholderText = stringResource(R.string.settings_set_lock_screen_passcode_label), labelText = stringResource(R.string.settings_set_lock_screen_passcode_label).uppercase(Locale.getDefault()) ) @@ -168,7 +172,7 @@ fun SetLockCodeScreenContent( } ) { Box(modifier = Modifier.padding(MaterialTheme.wireDimensions.spacing16x)) { - val enabled = state.password.text.isNotBlank() && state.passwordValidation.isValid + val enabled = passwordTextState.text.isNotBlank() && state.passwordValidation.isValid && !state.loading ContinueButton( enabled = enabled, onContinue = onContinue @@ -232,9 +236,9 @@ private fun PasswordVerificationItem(isInvalid: Boolean, text: String) { @Composable private fun ContinueButton( - modifier: Modifier = Modifier.fillMaxWidth(), enabled: Boolean, - onContinue: () -> Unit + onContinue: () -> Unit, + modifier: Modifier = Modifier.fillMaxWidth(), ) { val interactionSource = remember { MutableInteractionSource() } Column(modifier = modifier) { @@ -273,8 +277,8 @@ fun PreviewSetLockCodeScreen() { SetLockCodeScreenContent( navigator = rememberNavigator {}, state = SetLockCodeViewState(), + passwordTextState = TextFieldState(), scrollState = rememberScrollState(), - onPasswordChanged = {}, onBackPress = {}, onContinue = {} ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeViewState.kt index d49b94e7d09..7408272ce34 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeViewState.kt @@ -17,15 +17,12 @@ */ package com.wire.android.ui.home.appLock.set -import androidx.compose.ui.text.input.TextFieldValue import com.wire.android.feature.ObserveAppLockConfigUseCase - import com.wire.kalium.logic.feature.auth.ValidatePasswordResult import kotlin.time.Duration data class SetLockCodeViewState( - val continueEnabled: Boolean = false, - val password: TextFieldValue = TextFieldValue(), + val loading: Boolean = false, val passwordValidation: ValidatePasswordResult = ValidatePasswordResult.Invalid(), val timeout: Duration = ObserveAppLockConfigUseCase.DEFAULT_APP_LOCK_TIMEOUT, val done: Boolean = false, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt index 2d8be056713..dbd8927ebac 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt @@ -17,15 +17,16 @@ */ package com.wire.android.ui.home.appLock.set +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.AppLockSource import com.wire.android.feature.ObserveAppLockConfigUseCase +import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase @@ -47,10 +48,16 @@ class SetLockScreenViewModel @Inject constructor( private val markTeamAppLockStatusAsNotified: MarkTeamAppLockStatusAsNotifiedUseCase ) : ViewModel() { + val passwordTextState: TextFieldState = TextFieldState() var state: SetLockCodeViewState by mutableStateOf(SetLockCodeViewState()) private set init { + viewModelScope.launch { + passwordTextState.textAsFlow().collect { + state = state.copy(passwordValidation = validatePassword(it.toString())) + } + } viewModelScope.launch { combine( observeAppLockConfig(), @@ -64,23 +71,11 @@ class SetLockScreenViewModel @Inject constructor( } } - fun onPasswordChanged(password: TextFieldValue) { - state = state.copy( - password = password - ) - validatePassword(password.text).let { - state = state.copy( - continueEnabled = it.isValid, - passwordValidation = it - ) - } - } - fun onContinue() { - state = state.copy(continueEnabled = false) + state = state.copy(loading = true) // the continue button is enabled iff the password is valid // this check is for safety only - validatePassword(state.password.text).let { + validatePassword(passwordTextState.text.toString()).let { state = state.copy(passwordValidation = it) if (it.isValid) { viewModelScope.launch { @@ -92,7 +87,7 @@ class SetLockScreenViewModel @Inject constructor( AppLockSource.TeamEnforced } - setUserAppLock(state.password.text, source) + setUserAppLock(passwordTextState.text.toString(), source) // TODO(bug): this does not take into account which account enforced the app lock markTeamAppLockStatusAsNotified() @@ -103,6 +98,7 @@ class SetLockScreenViewModel @Inject constructor( } } } + state = state.copy(loading = false) } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt index 6a56a3707a9..23fbc6f80e7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt @@ -27,6 +27,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -45,7 +47,6 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph @@ -58,6 +59,7 @@ import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.button.WireTertiaryButton import com.wire.android.ui.common.rememberBottomBarElevationState 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.destinations.AppUnlockWithBiometricsScreenDestination @@ -73,13 +75,13 @@ import java.util.Locale @Destination @Composable fun EnterLockCodeScreen( + navigator: Navigator, viewModel: EnterLockScreenViewModel = hiltViewModel(), - navigator: Navigator ) { EnterLockCodeScreenContent( state = viewModel.state, + passwordTextState = viewModel.passwordTextState, scrollState = rememberScrollState(), - onPasswordChanged = viewModel::onPasswordChanged, onContinue = viewModel::onContinue, onForgotCodeClicked = { navigator.navigate(NavigationCommand(ForgotLockCodeScreenDestination)) } ) @@ -97,14 +99,15 @@ fun EnterLockCodeScreen( @Composable fun EnterLockCodeScreenContent( state: EnterLockCodeViewState, + passwordTextState: TextFieldState, scrollState: ScrollState, - onPasswordChanged: (TextFieldValue) -> Unit, onContinue: () -> Unit, onForgotCodeClicked: () -> Unit, + modifier: Modifier = Modifier, ) { WireScaffold { internalPadding -> Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .padding(internalPadding) ) { @@ -136,10 +139,9 @@ fun EnterLockCodeScreenContent( ) WirePasswordTextField( - value = state.password, - onValueChange = onPasswordChanged, + textState = passwordTextState, labelMandatoryIcon = true, - imeAction = ImeAction.Done, + keyboardOptions = KeyboardOptions.DefaultPassword.copy(imeAction = ImeAction.Done), modifier = Modifier .testTag("password"), state = when (state.error) { @@ -149,7 +151,7 @@ fun EnterLockCodeScreenContent( EnterLockCodeError.None -> WireTextFieldState.Default }, - autofill = false, + autoFill = false, placeholderText = stringResource(R.string.settings_set_lock_screen_passcode_label), labelText = stringResource(R.string.settings_set_lock_screen_passcode_label).uppercase( Locale.getDefault() @@ -173,7 +175,7 @@ fun EnterLockCodeScreenContent( } ) { Box(modifier = Modifier.padding(MaterialTheme.wireDimensions.spacing16x)) { - val enabled = state.password.text.isNotBlank() && state.isUnlockEnabled + val enabled = passwordTextState.text.isNotBlank() && state.isUnlockEnabled && !state.loading ContinueButton( enabled = enabled, onContinue = onContinue @@ -186,9 +188,9 @@ fun EnterLockCodeScreenContent( @Composable private fun ContinueButton( - modifier: Modifier = Modifier.fillMaxWidth(), enabled: Boolean, - onContinue: () -> Unit + onContinue: () -> Unit, + modifier: Modifier = Modifier.fillMaxWidth(), ) { val interactionSource = remember { MutableInteractionSource() } Column(modifier = modifier) { @@ -210,8 +212,8 @@ fun PreviewEnterLockCodeScreen() { WireTheme { EnterLockCodeScreenContent( state = EnterLockCodeViewState(), + passwordTextState = TextFieldState(), scrollState = rememberScrollState(), - onPasswordChanged = {}, onContinue = {}, onForgotCodeClicked = {} ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeViewState.kt index e645db0239e..1267383f00b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeViewState.kt @@ -17,11 +17,8 @@ */ package com.wire.android.ui.home.appLock.unlock -import androidx.compose.ui.text.input.TextFieldValue - data class EnterLockCodeViewState( - val continueEnabled: Boolean = false, - val password: TextFieldValue = TextFieldValue(), + val loading: Boolean = false, val isUnlockEnabled: Boolean = false, val error: EnterLockCodeError = EnterLockCodeError.None, val done: Boolean = false diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModel.kt index ff53fa1e6b8..25f1bc2a37a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModel.kt @@ -17,18 +17,20 @@ */ package com.wire.android.ui.home.appLock.unlock +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.datastore.GlobalDataStore +import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.ui.home.appLock.LockCodeTimeManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.sha256 import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -42,37 +44,31 @@ class EnterLockScreenViewModel @Inject constructor( private val lockCodeTimeManager: LockCodeTimeManager, ) : ViewModel() { + val passwordTextState: TextFieldState = TextFieldState() var state: EnterLockCodeViewState by mutableStateOf(EnterLockCodeViewState()) private set - fun onPasswordChanged(password: TextFieldValue) { - state = state.copy( - error = EnterLockCodeError.None, - password = password - ) - state = if (validatePassword(password.text).isValid) { - state.copy( - continueEnabled = true, - isUnlockEnabled = true - ) - } else { - state.copy( - isUnlockEnabled = false - ) + init { + viewModelScope.launch { + passwordTextState.textAsFlow().collectLatest { + state = state.copy( + isUnlockEnabled = validatePassword(it.toString()).isValid + ) + } } } fun onContinue() { - state = state.copy(continueEnabled = false) - // the continue button is enabled iff the password is valid + state = state.copy(loading = true) + // the continue button is enabled if the password is valid // this check is for safety only - if (!validatePassword(state.password.text).isValid) { + if (!validatePassword(passwordTextState.text.toString()).isValid) { state = state.copy(isUnlockEnabled = false) } else { viewModelScope.launch { val storedPasscode = withContext(dispatchers.io()) { globalDataStore.getAppLockPasscodeFlow().firstOrNull() } withContext(dispatchers.main()) { - state = if (storedPasscode == state.password.text.sha256()) { + state = if (storedPasscode == passwordTextState.text.toString().sha256()) { lockCodeTimeManager.appUnlocked() state.copy(done = true) } else { @@ -81,5 +77,6 @@ class EnterLockScreenViewModel @Inject constructor( } } } + state = state.copy(loading = false) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index 1e9a71c9e19..9fc8c7138d1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.settings.devices -import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -30,6 +29,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -41,10 +41,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination @@ -79,14 +77,16 @@ import com.wire.android.ui.home.E2EISuccessDialog import com.wire.android.ui.home.E2EIUpdateErrorWithDismissDialog import com.wire.android.ui.home.conversationslist.common.FolderHeader import com.wire.android.ui.settings.devices.model.DeviceDetailsState +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.CustomTabsHelper +import com.wire.android.util.deviceDateTimeFormat import com.wire.android.util.dialogErrorStrings import com.wire.android.util.extension.formatAsFingerPrint import com.wire.android.util.extension.formatAsString -import com.wire.android.util.deviceDateTimeFormat +import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.conversation.ClientId @@ -106,8 +106,8 @@ fun DeviceDetailsScreen( else { DeviceDetailsContent( state = viewModel.state, + passwordTextState = viewModel.passwordTextState, onDeleteDevice = { viewModel.removeDevice(navigator::navigateBack) }, - onPasswordChange = viewModel::onPasswordChange, onRemoveConfirm = { viewModel.onRemoveConfirmed(navigator::navigateBack) }, onDialogDismiss = viewModel::onDialogDismissed, onErrorDialogDismiss = viewModel::clearDeleteClientError, @@ -129,21 +129,23 @@ fun DeviceDetailsScreen( @Composable fun DeviceDetailsContent( state: DeviceDetailsState, + passwordTextState: TextFieldState, + handleE2EIEnrollmentResult: (Either) -> Unit, + modifier: Modifier = Modifier, onDeleteDevice: () -> Unit = {}, onNavigateBack: () -> Unit = {}, onNavigateToE2eiCertificateDetailsScreen: (String) -> Unit = {}, - onPasswordChange: (TextFieldValue) -> Unit = {}, onRemoveConfirm: () -> Unit = {}, onDialogDismiss: () -> Unit = {}, onErrorDialogDismiss: () -> Unit = {}, enrollE2eiCertificate: () -> Unit = {}, - handleE2EIEnrollmentResult: (Either) -> Unit, onUpdateClientVerification: (Boolean) -> Unit = {}, onEnrollE2EIErrorDismiss: () -> Unit = {}, onEnrollE2EISuccessDismiss: () -> Unit = {} ) { val screenState = rememberConversationScreenState() WireScaffold( + modifier = modifier, topBar = { DeviceDetailsTopBar(onNavigateBack, state.device, state.isCurrentDevice, state.isE2EIEnabled) }, bottomBar = { Column( @@ -258,7 +260,7 @@ fun DeviceDetailsContent( RemoveDeviceDialog( errorState = state.error, state = state.removeDeviceDialogState, - onPasswordChange = onPasswordChange, + passwordTextState = passwordTextState, onDialogDismiss = onDialogDismiss, onRemoveConfirm = onRemoveConfirm ) @@ -372,9 +374,10 @@ fun DeviceKeyFingerprintItem( fun DeviceMLSSignatureItem( mlsThumbprint: String, mlsProtocolType: String, - onCopy: (String) -> Unit + onCopy: (String) -> Unit, + modifier: Modifier = Modifier, ) { - + Column(modifier = modifier) { FolderHeader( name = stringResource(id = R.string.label_mls_signature, mlsProtocolType).uppercase(), modifier = Modifier @@ -392,6 +395,7 @@ fun DeviceMLSSignatureItem( ) } ) + } } @Composable @@ -401,34 +405,39 @@ fun DeviceVerificationItem( isSelfClient: Boolean, userName: String?, onStatusChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, ) { - @StringRes - val subTitle = if (state) { - R.string.label_client_verified - } else { - R.string.label_client_unverified + Column(modifier = modifier) { + DeviceDetailSectionContent( + sectionTitle = stringResource(id = R.string.title_device_key_fingerprint), + sectionText = AnnotatedString( + stringResource( + id = when (state) { + true -> R.string.label_client_verified + false -> R.string.label_client_unverified + } + ) + ), + titleTrailingItem = { + WireSwitch( + checked = state, + onCheckedChange = onStatusChange, + enabled = enabled + ) + } + ) + VerificationDescription(isSelfClient, userName) } - DeviceDetailSectionContent( - stringResource(id = R.string.title_device_key_fingerprint), - AnnotatedString(stringResource(id = subTitle)), - titleTrailingItem = { - WireSwitch( - checked = state, - onCheckedChange = onStatusChange, - enabled = enabled - ) - } - ) - VerificationDescription(isSelfClient, userName) } @Composable private fun VerificationDescription( isSelfClient: Boolean, - userName: String? + userName: String?, + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding( start = dimensions().spacing16x, @@ -527,12 +536,13 @@ private fun DescriptionText( private fun DeviceDetailSectionContent( sectionTitle: String, sectionText: AnnotatedString, + modifier: Modifier = Modifier, enabled: Boolean = true, titleTrailingItem: (@Composable () -> Unit)? = null ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier + modifier = modifier .padding( top = MaterialTheme.wireDimensions.spacing12x, bottom = MaterialTheme.wireDimensions.spacing12x, @@ -567,10 +577,11 @@ private fun DeviceDetailSectionContent( } } -@Preview +@PreviewMultipleThemes @Composable -fun PreviewDeviceDetailsScreen() { +fun PreviewDeviceDetailsScreen() = WireTheme { DeviceDetailsContent( + passwordTextState = TextFieldState(), state = DeviceDetailsState( device = Device( clientId = ClientId(""), @@ -580,11 +591,16 @@ fun PreviewDeviceDetailsScreen() { ), isCurrentDevice = false ), - onPasswordChange = { }, - enrollE2eiCertificate = { }, + enrollE2eiCertificate = {}, handleE2EIEnrollmentResult = {}, - onRemoveConfirm = { }, - onDialogDismiss = { }, - onErrorDialogDismiss = { } + onRemoveConfirm = {}, + onDialogDismiss = {}, + onErrorDialogDismiss = {}, + onNavigateBack = {}, + onNavigateToE2eiCertificateDetailsScreen = {}, + onUpdateClientVerification = {}, + onEnrollE2EIErrorDismiss = {}, + onEnrollE2EISuccessDismiss = {}, + onDeleteDevice = {}, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt index 84ff017311e..545fe2334ce 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt @@ -17,10 +17,11 @@ */ package com.wire.android.ui.settings.devices +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.appLogger @@ -29,6 +30,7 @@ import com.wire.android.navigation.SavedStateViewModel import com.wire.android.ui.authentication.devices.model.Device import com.wire.android.ui.authentication.devices.remove.RemoveDeviceDialogState import com.wire.android.ui.authentication.devices.remove.RemoveDeviceError +import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.ui.navArgs import com.wire.android.ui.settings.devices.model.DeviceDetailsState import com.wire.kalium.logic.CoreFailure @@ -53,6 +55,8 @@ import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.fold import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import javax.inject.Inject @@ -76,6 +80,7 @@ class DeviceDetailsViewModel @Inject constructor( private val deviceId: ClientId = deviceDetailsNavArgs.clientId private val userId: UserId = deviceDetailsNavArgs.userId + val passwordTextState: TextFieldState = TextFieldState() var state: DeviceDetailsState by mutableStateOf( DeviceDetailsState( isSelfClient = isSelfClient, @@ -89,11 +94,25 @@ class DeviceDetailsViewModel @Inject constructor( getClientFingerPrint() observeUserName() getE2eiCertificate() + observePasswordTextChanges() } private val isSelfClient: Boolean get() = currentUserId == userId + private fun observePasswordTextChanges() { + viewModelScope.launch { + passwordTextState.textAsFlow().distinctUntilChanged().collectLatest { newPassword -> + updateStateIfDialogVisible { + state.copy( + removeDeviceDialogState = it.copy(removeEnabled = newPassword.isNotEmpty()), + error = RemoveDeviceError.None + ) + } + } + } + } + private fun observeUserName() { if (!isSelfClient) { viewModelScope.launch { @@ -204,6 +223,7 @@ class DeviceDetailsViewModel @Inject constructor( } private fun showDeleteClientDialog(device: Device) { + passwordTextState.clearText() state = device.let { RemoveDeviceDialogState.Visible(it) }.let { state.copy( error = RemoveDeviceError.None, @@ -227,22 +247,6 @@ class DeviceDetailsViewModel @Inject constructor( } } - fun onPasswordChange(newText: TextFieldValue) { - updateStateIfDialogVisible { - if (it.password == newText) { - state - } else { - state.copy( - removeDeviceDialogState = it.copy( - password = newText, - removeEnabled = newText.text.isNotEmpty() - ), - error = RemoveDeviceError.None - ) - } - } - } - fun onRemoveConfirmed(onSuccess: () -> Unit) { (state.removeDeviceDialogState as? RemoveDeviceDialogState.Visible)?.let { dialogStateVisible -> updateStateIfDialogVisible { @@ -251,7 +255,7 @@ class DeviceDetailsViewModel @Inject constructor( ) } viewModelScope.launch { - deleteDevice(dialogStateVisible.password.text, onSuccess) + deleteDevice(passwordTextState.text.toString(), onSuccess) updateStateIfDialogVisible { state.copy(removeDeviceDialogState = it.copy(loading = false)) } } } @@ -270,6 +274,7 @@ class DeviceDetailsViewModel @Inject constructor( } fun onDialogDismissed() { + passwordTextState.clearText() state = state.copy(removeDeviceDialogState = RemoveDeviceDialogState.Hidden) } diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelTest.kt index 2ef85aa58dc..23652e5d067 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelTest.kt @@ -18,8 +18,9 @@ package com.wire.android.ui.authentication.devices.register -import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import com.wire.android.config.CoroutineTestExtension +import com.wire.android.config.SnapshotExtension import com.wire.android.config.mockUri import com.wire.android.datastore.UserDataStore import com.wire.android.framework.TestClient @@ -45,7 +46,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(CoroutineTestExtension::class) +@ExtendWith(CoroutineTestExtension::class, SnapshotExtension::class) class RegisterDeviceViewModelTest { @MockK @@ -73,15 +74,15 @@ class RegisterDeviceViewModelTest { } @Test - fun `given empty string, when entering the password to register, then button is disabled`() { - registerDeviceViewModel.onPasswordChange(TextFieldValue(String.EMPTY)) + fun `given empty string, when entering the password to register, then button is disabled`() = runTest { + registerDeviceViewModel.passwordTextState.setTextAndPlaceCursorAtEnd(String.EMPTY) registerDeviceViewModel.state.continueEnabled shouldBeEqualTo false registerDeviceViewModel.state.flowState shouldBeInstanceOf RegisterDeviceFlowState.Default::class } @Test fun `given non-empty string, when entering the password to register, then button is disabled`() { - registerDeviceViewModel.onPasswordChange(TextFieldValue("abc")) + registerDeviceViewModel.passwordTextState.setTextAndPlaceCursorAtEnd("abc") registerDeviceViewModel.state.continueEnabled shouldBeEqualTo true registerDeviceViewModel.state.flowState shouldBeInstanceOf RegisterDeviceFlowState.Default::class } @@ -95,7 +96,7 @@ class RegisterDeviceViewModelTest { ) } returns RegisterClientResult.Success(CLIENT) - registerDeviceViewModel.onPasswordChange(TextFieldValue(password)) + registerDeviceViewModel.passwordTextState.setTextAndPlaceCursorAtEnd(password) registerDeviceViewModel.onContinue() advanceUntilIdle() @@ -111,7 +112,7 @@ class RegisterDeviceViewModelTest { coEvery { registerClientUseCase(any()) } returns RegisterClientResult.Failure.TooManyClients - registerDeviceViewModel.onPasswordChange(TextFieldValue(password)) + registerDeviceViewModel.passwordTextState.setTextAndPlaceCursorAtEnd(password) registerDeviceViewModel.onContinue() advanceUntilIdle() diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelTest.kt index 582005956f1..7bdab55d6f5 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelTest.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.home.appLock.forgot import com.wire.android.config.CoroutineTestExtension +import com.wire.android.config.SnapshotExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.GlobalDataStore import com.wire.android.datastore.UserDataStore @@ -25,7 +26,6 @@ import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.SwitchAccountParam import com.wire.android.feature.SwitchAccountResult -import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.StorageFailure @@ -63,7 +63,7 @@ import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -@ExtendWith(CoroutineTestExtension::class) +@ExtendWith(CoroutineTestExtension::class, SnapshotExtension::class) class ForgotLockScreenViewModelTest { private val dispatcher = TestDispatcherProvider() @@ -216,7 +216,6 @@ class ForgotLockScreenViewModelTest { @MockK lateinit var globalDataStore: GlobalDataStore @MockK lateinit var userDataStoreProvider: UserDataStoreProvider @MockK lateinit var userDataStore: UserDataStore - @MockK lateinit var notificationChannelsManager: NotificationChannelsManager @MockK lateinit var notificationManager: WireNotificationManager @MockK lateinit var getSelfUserUseCase: GetSelfUserUseCase @MockK lateinit var isPasswordRequiredUseCase: IsPasswordRequiredUseCase @@ -230,7 +229,7 @@ class ForgotLockScreenViewModelTest { private val viewModel: ForgotLockScreenViewModel by lazy { ForgotLockScreenViewModel( - coreLogic, globalDataStore, userDataStoreProvider, notificationChannelsManager, notificationManager, getSelfUserUseCase, + coreLogic, globalDataStore, userDataStoreProvider, notificationManager, getSelfUserUseCase, isPasswordRequiredUseCase, validatePasswordUseCase, observeCurrentClientIdUseCase, deleteClientUseCase, getSessionsUseCase, observeEstablishedCallsUseCase, endCallUseCase, accountSwitchUseCase ) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModelTest.kt index b1df6468d69..13f68906563 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModelTest.kt @@ -17,8 +17,9 @@ */ package com.wire.android.ui.home.appLock.set -import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import com.wire.android.config.CoroutineTestExtension +import com.wire.android.config.SnapshotExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.AppLockConfig @@ -34,10 +35,11 @@ import io.mockk.impl.annotations.MockK import io.mockk.verify import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -@ExtendWith(CoroutineTestExtension::class) +@ExtendWith(CoroutineTestExtension::class, SnapshotExtension::class) class SetLockScreenViewModelTest { @Test @@ -46,10 +48,10 @@ class SetLockScreenViewModelTest { .withValidPassword() .arrange() - viewModel.onPasswordChanged(TextFieldValue("password")) + viewModel.passwordTextState.setTextAndPlaceCursorAtEnd("password") - assert(viewModel.state.password.text == "password") - assert(viewModel.state.passwordValidation.isValid) + assertEquals("password", viewModel.passwordTextState.text.toString()) + assertEquals(true, viewModel.state.passwordValidation.isValid) verify(exactly = 1) { arrangement.validatePassword("password") } } @@ -60,10 +62,10 @@ class SetLockScreenViewModelTest { .withInvalidPassword() .arrange() - viewModel.onPasswordChanged(TextFieldValue("password")) + viewModel.passwordTextState.setTextAndPlaceCursorAtEnd("password") - assert(viewModel.state.password.text == "password") - assert(!viewModel.state.passwordValidation.isValid) + assertEquals("password", viewModel.passwordTextState.text.toString()) + assertEquals(false, viewModel.state.passwordValidation.isValid) verify(exactly = 1) { arrangement.validatePassword("password") } } @@ -97,6 +99,9 @@ class SetLockScreenViewModelTest { fun withValidPassword() = apply { every { validatePassword(any()) } returns ValidatePasswordResult.Valid + coEvery { validatePassword(any()) } returns ValidatePasswordResult.Valid + every { validatePassword.invoke(any()) } returns ValidatePasswordResult.Valid + coEvery { validatePassword.invoke(any()) } returns ValidatePasswordResult.Valid } fun withInvalidPassword() = apply { @@ -107,14 +112,16 @@ class SetLockScreenViewModelTest { coEvery { observeIsAppLockEditableUseCase() } returns flowOf(result) } - private val viewModel = SetLockScreenViewModel( - validatePassword, - globalDataStore, - TestDispatcherProvider(), - observeAppLockConfig, - observeIsAppLockEditableUseCase, - markTeamAppLockStatusAsNotified - ) + private val viewModel by lazy { + SetLockScreenViewModel( + validatePassword, + globalDataStore, + TestDispatcherProvider(), + observeAppLockConfig, + observeIsAppLockEditableUseCase, + markTeamAppLockStatusAsNotified + ) + } fun arrange() = this to viewModel }