From 7b7acec2611b5b8a7d481206e71415e114e425af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:16:04 +0100 Subject: [PATCH 1/2] fix: reset passcode dialog state when empty input or sso user [WPB-5094] (#2447) Co-authored-by: Yamil Medina --- .../forgot/ForgotLockCodeResetDeviceDialog.kt | 68 +++++---- .../appLock/forgot/ForgotLockCodeScreen.kt | 1 + .../appLock/forgot/ForgotLockCodeViewState.kt | 3 +- .../forgot/ForgotLockScreenViewModel.kt | 86 ++++++++---- app/src/main/res/values/strings.xml | 1 + .../forgot/ForgotLockScreenViewModelTest.kt | 131 +++++++++++------- 6 files changed, 185 insertions(+), 105 deletions(-) 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 7121dee9ca1..f8279fb9e05 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 @@ -31,6 +31,7 @@ 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.window.DialogProperties import com.wire.android.R @@ -51,6 +52,7 @@ import com.wire.android.util.ui.stringWithStyledArgs @Composable fun ForgotLockCodeResetDeviceDialog( username: String, + isPasswordRequired: Boolean, isPasswordValid: Boolean, isResetDeviceEnabled: Boolean, onPasswordChanged: (TextFieldValue) -> Unit, @@ -65,14 +67,18 @@ fun ForgotLockCodeResetDeviceDialog( } WireDialog( title = stringResource(R.string.settings_forgot_lock_screen_reset_device), - text = LocalContext.current.resources.stringWithStyledArgs( - R.string.settings_forgot_lock_screen_reset_device_description, - MaterialTheme.wireTypography.body01, - MaterialTheme.wireTypography.body02, - colorsScheme().onBackground, - colorsScheme().onBackground, - username - ), + text = if (isPasswordRequired) { + LocalContext.current.resources.stringWithStyledArgs( + R.string.settings_forgot_lock_screen_reset_device_description, + MaterialTheme.wireTypography.body01, + MaterialTheme.wireTypography.body02, + colorsScheme().onBackground, + colorsScheme().onBackground, + username + ) + } else { + AnnotatedString(stringResource(id = R.string.settings_forgot_lock_screen_reset_device_without_password_description)) + }, onDismiss = onDialogDismissHideKeyboard, buttonsHorizontalAlignment = false, dismissButtonProperties = WireDialogButtonProperties( @@ -90,23 +96,25 @@ fun ForgotLockCodeResetDeviceDialog( state = if (!isResetDeviceEnabled) WireButtonState.Disabled else WireButtonState.Error ) ) { - // keyboard controller from outside the Dialog doesn't work inside its content so we have to pass the state - // to the dialog's content and use keyboard controller from there - keyboardController = LocalSoftwareKeyboardController.current - WirePasswordTextField( - 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, - keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }), - modifier = Modifier.padding(bottom = dimensions().spacing16x) - ) + if (isPasswordRequired) { + // keyboard controller from outside the Dialog doesn't work inside its content so we have to pass the state + // to the dialog's content and use keyboard controller from there + keyboardController = LocalSoftwareKeyboardController.current + WirePasswordTextField( + 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, + keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }), + modifier = Modifier.padding(bottom = dimensions().spacing16x) + ) + } } } @@ -124,7 +132,15 @@ fun ForgotLockCodeResettingDeviceDialog() { @Composable fun PreviewForgotLockCodeResetDeviceDialog() { WireTheme { - ForgotLockCodeResetDeviceDialog("Username", true, true, {}, {}, {}) + ForgotLockCodeResetDeviceDialog("Username", false, true, true, {}, {}, {}) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewForgotLockCodeResetDeviceWithoutPasswordDialog() { + WireTheme { + ForgotLockCodeResetDeviceDialog("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 430c5524367..321288898ad 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 @@ -86,6 +86,7 @@ fun ForgotLockCodeScreen( if (dialogState.loading) ForgotLockCodeResettingDeviceDialog() else ForgotLockCodeResetDeviceDialog( username = dialogState.username, + isPasswordRequired = dialogState.passwordRequired, isPasswordValid = dialogState.passwordValid, isResetDeviceEnabled = dialogState.resetDeviceEnabled, onPasswordChanged = viewModel::onPasswordChanged, 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 e0506aaa30a..f7e0e680791 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 @@ -31,8 +31,9 @@ sealed class ForgotLockCodeDialogState { data class Visible( val username: String, val password: TextFieldValue = TextFieldValue(""), + val passwordRequired: Boolean = false, val passwordValid: Boolean = true, - val resetDeviceEnabled: Boolean = true, + val resetDeviceEnabled: Boolean = false, val loading: Boolean = false, ) : ForgotLockCodeDialogState() } 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 460646dc76d..44031ca5f9d 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 @@ -48,9 +48,6 @@ import com.wire.kalium.logic.feature.session.GetAllSessionsResult 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 com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.flatMap -import com.wire.kalium.logic.functional.fold import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first @@ -88,12 +85,26 @@ class ForgotLockScreenViewModel @Inject constructor( } fun onPasswordChanged(password: TextFieldValue) { - updateIfDialogStateVisible { it.copy(password = password, resetDeviceEnabled = true) } + updateIfDialogStateVisible { it.copy(password = password, resetDeviceEnabled = password.text.isNotBlank()) } } fun onResetDevice() { viewModelScope.launch { - state = state.copy(dialogState = ForgotLockCodeDialogState.Visible(username = getSelf().firstOrNull()?.name ?: "")) + state = when (val isPasswordRequiredResult = isPasswordRequired()) { + is IsPasswordRequiredUseCase.Result.Success -> { + state.copy( + dialogState = ForgotLockCodeDialogState.Visible( + username = getSelf().firstOrNull()?.name ?: "", + passwordRequired = isPasswordRequiredResult.value, + resetDeviceEnabled = !isPasswordRequiredResult.value, + ) + ) + } + is IsPasswordRequiredUseCase.Result.Failure -> { + appLogger.e("$TAG Failed to check if password is required when opening reset passcode dialog") + state.copy(error = isPasswordRequiredResult.cause) + } + } } } @@ -110,40 +121,45 @@ class ForgotLockScreenViewModel @Inject constructor( updateIfDialogStateVisible { it.copy(resetDeviceEnabled = false) } viewModelScope.launch { validatePasswordIfNeeded(dialogStateVisible.password.text) - .flatMapIfSuccess { + .flatMapIfSuccess { validatedPassword -> updateIfDialogStateVisible { it.copy(loading = true) } - deleteCurrentClient(dialogStateVisible.password.text) - .flatMapIfSuccess { hardLogoutAllAccounts() } + deleteCurrentClient(validatedPassword) } - .fold({ error -> - state = state.copy(error = error) - updateIfDialogStateVisible { it.copy(loading = false, resetDeviceEnabled = true) } - }, { result -> + .flatMapIfSuccess { hardLogoutAllAccounts() } + .let { result -> when (result) { - Result.InvalidPassword -> updateIfDialogStateVisible { it.copy(passwordValid = false, loading = false) } - Result.Success -> state = state.copy(completed = true, dialogState = ForgotLockCodeDialogState.Hidden) + is Result.Failure.Generic -> { + state = state.copy(error = result.cause) + updateIfDialogStateVisible { it.copy(loading = false, resetDeviceEnabled = true) } + } + Result.Failure.PasswordRequired -> + updateIfDialogStateVisible { it.copy(passwordRequired = true, passwordValid = false, loading = false) } + Result.Failure.InvalidPassword -> + updateIfDialogStateVisible { it.copy(passwordValid = false, loading = false) } + Result.Success -> + state = state.copy(completed = true, dialogState = ForgotLockCodeDialogState.Hidden) } } - ) } } } @VisibleForTesting - internal suspend fun validatePasswordIfNeeded(password: String): Either = + internal suspend fun validatePasswordIfNeeded(password: String): Pair = when (val isPasswordRequiredResult = isPasswordRequired()) { is IsPasswordRequiredUseCase.Result.Failure -> { appLogger.e("$TAG Failed to check if password is required when resetting passcode") - Either.Left(isPasswordRequiredResult.cause) + Result.Failure.Generic(isPasswordRequiredResult.cause) to password } - is IsPasswordRequiredUseCase.Result.Success -> { - if (!isPasswordRequiredResult.value || validatePassword(password).isValid) Either.Right(Result.Success) - else Either.Right(Result.InvalidPassword) + is IsPasswordRequiredUseCase.Result.Success -> when { + isPasswordRequiredResult.value && password.isBlank() -> Result.Failure.PasswordRequired to password + isPasswordRequiredResult.value && !validatePassword(password).isValid -> Result.Failure.InvalidPassword to password + else -> Result.Success to if (isPasswordRequiredResult.value) password else "" } } @VisibleForTesting - internal suspend fun deleteCurrentClient(password: String): Either = + internal suspend fun deleteCurrentClient(password: String): Result = observeCurrentClientId() .filterNotNull() .first() @@ -151,19 +167,19 @@ class ForgotLockScreenViewModel @Inject constructor( when (val deleteClientResult = deleteClient(DeleteClientParam(password, clientId))) { is DeleteClientResult.Failure.Generic -> { appLogger.e("$TAG Failed to delete current client when resetting passcode") - Either.Left(deleteClientResult.genericFailure) + Result.Failure.Generic(deleteClientResult.genericFailure) } - DeleteClientResult.Success -> Either.Right(Result.Success) - else -> Either.Right(Result.InvalidPassword) + DeleteClientResult.Success -> Result.Success + else -> Result.Failure.InvalidPassword } } @VisibleForTesting - internal suspend fun hardLogoutAllAccounts(): Either = + internal suspend fun hardLogoutAllAccounts(): Result = when (val getAllSessionsResult = getSessions()) { is GetAllSessionsResult.Failure.Generic -> { appLogger.e("$TAG Failed to get all sessions when resetting passcode") - Either.Left(getAllSessionsResult.genericFailure) + Result.Failure.Generic(getAllSessionsResult.genericFailure) } is GetAllSessionsResult.Failure.NoSessionFound, is GetAllSessionsResult.Success -> { @@ -178,7 +194,7 @@ class ForgotLockScreenViewModel @Inject constructor( }.joinAll() // wait until all accounts are logged out globalDataStore.clearAppLockPasscode() accountSwitch(SwitchAccountParam.Clear) - Either.Right(Result.Success) + Result.Success } } @@ -190,10 +206,20 @@ class ForgotLockScreenViewModel @Inject constructor( userDataStoreProvider.getOrCreate(userId).clear() } - internal enum class Result { InvalidPassword, Success; } + internal sealed class Result { + sealed class Failure : Result() { + data object InvalidPassword : Failure() + data object PasswordRequired : Failure() + data class Generic(val cause: CoreFailure) : Failure() + } + data object Success : Result() + } + + private inline fun Result.flatMapIfSuccess(block: () -> Result): Result = + if (this is Result.Success) block() else this - private inline fun Either.flatMapIfSuccess(block: () -> Either): Either = - this.flatMap { if (it == Result.Success) block() else Either.Right(it) } + private inline fun Pair.flatMapIfSuccess(block: (T) -> Result): Result = + if (this.first is Result.Success) block(this.second) else this.first companion object { const val TAG = "ForgotLockResetPasscode" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fef837e1d8b..b8d32c6dfc7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -963,6 +963,7 @@ If you remove your device, you lose all local data and messages for all accounts on this device permanently. Remove Device Enter your password for the account %s to confirm the deletion of all data for all accounts on this device. After removing your device, you can log in with your account credentials. + Confirm the deletion of all data for all accounts on this device. After removing your device, you can log in with your account credentials. Please wait... Your Devices 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 4adabf37d7e..55563124db7 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 @@ -48,9 +48,6 @@ import com.wire.kalium.logic.feature.session.GetAllSessionsResult 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 com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.isLeft -import com.wire.kalium.logic.functional.isRight import io.mockk.Called import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -62,6 +59,7 @@ import io.mockk.verify import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -71,24 +69,26 @@ class ForgotLockScreenViewModelTest { // password validation @Test - fun `given password not required, when validating password, then return Success`() = + fun `given password not required, when validating password, then return Success with empty password`() = runTest(dispatcher.default()) { val (arrangement, viewModel) = Arrangement() .withIsPasswordRequiredResult(IsPasswordRequiredUseCase.Result.Success(false)) .arrange() - val result = viewModel.validatePasswordIfNeeded("password") - assert(result.isRight() && (result as Either.Right).value == ForgotLockScreenViewModel.Result.Success) + val (result, resultPassword) = viewModel.validatePasswordIfNeeded("password") + assert(result is ForgotLockScreenViewModel.Result.Success) + assertEquals("", resultPassword) verify { arrangement.validatePasswordUseCase(any()) wasNot Called } } @Test - fun `given password required and valid, when validating password, then return Success`() = + fun `given password required and valid, when validating password, then return Success with given password`() = runTest(dispatcher.default()) { val (_, viewModel) = Arrangement() .withIsPasswordRequiredResult(IsPasswordRequiredUseCase.Result.Success(true)) .withValidatePasswordResult(ValidatePasswordResult.Valid) .arrange() - val result = viewModel.validatePasswordIfNeeded("password") - assert(result.isRight() && (result as Either.Right).value == ForgotLockScreenViewModel.Result.Success) + val (result, resultPassword) = viewModel.validatePasswordIfNeeded("password") + assert(result is ForgotLockScreenViewModel.Result.Success) + assertEquals("password", resultPassword) } @Test fun `given password required but invalid, when validating password, then return InvalidPassword`() = @@ -97,83 +97,118 @@ class ForgotLockScreenViewModelTest { .withIsPasswordRequiredResult(IsPasswordRequiredUseCase.Result.Success(true)) .withValidatePasswordResult(ValidatePasswordResult.Invalid()) .arrange() - val result = viewModel.validatePasswordIfNeeded("password") - assert(result.isRight() && (result as Either.Right).value == ForgotLockScreenViewModel.Result.InvalidPassword) + val (result, _) = viewModel.validatePasswordIfNeeded("password") + assert(result is ForgotLockScreenViewModel.Result.Failure.InvalidPassword) } @Test - fun `given password required returns failure, when validating password, then return failure`() = + fun `given password required but not provided, when validating password, then return PasswordRequired`() = runTest(dispatcher.default()) { val (_, viewModel) = Arrangement() + .withIsPasswordRequiredResult(IsPasswordRequiredUseCase.Result.Success(true)) + .withValidatePasswordResult(ValidatePasswordResult.Invalid()) + .arrange() + val (result, _) = viewModel.validatePasswordIfNeeded("") + assert(result is ForgotLockScreenViewModel.Result.Failure.PasswordRequired) + } + @Test + fun `given password required returns failure, when validating password, then return failure`() = + runTest(dispatcher.default()) { + val (arrangement, viewModel) = Arrangement() .withIsPasswordRequiredResult(IsPasswordRequiredUseCase.Result.Failure(StorageFailure.DataNotFound)) .arrange() - val result = viewModel.validatePasswordIfNeeded("password") - assert(result.isLeft()) + val (result, _) = viewModel.validatePasswordIfNeeded("password") + assert(result is ForgotLockScreenViewModel.Result.Failure) + verify { arrangement.validatePasswordUseCase(any()) wasNot Called } } // current client deletion - private fun testSuccessfulClientDelete(deleteClientResult: DeleteClientResult, actionResult: ForgotLockScreenViewModel.Result) = + private fun testClientDelete(deleteClientResult: DeleteClientResult, expected: ForgotLockScreenViewModel.Result) = runTest(dispatcher.default()) { val (_, viewModel) = Arrangement() .withDeleteClientResult(deleteClientResult) .arrange() val result = viewModel.deleteCurrentClient("password") - assert(result.isRight() && (result as Either.Right).value == actionResult) + assertEquals(expected, result) } @Test fun `given deleting client returns success, when deleting current client, then return Success`() = - testSuccessfulClientDelete(DeleteClientResult.Success, ForgotLockScreenViewModel.Result.Success) + testClientDelete( + deleteClientResult = DeleteClientResult.Success, + expected = ForgotLockScreenViewModel.Result.Success + ) @Test fun `given deleting client returns invalid credentials, when deleting current client, then return InvalidPassword`() = - testSuccessfulClientDelete(DeleteClientResult.Failure.InvalidCredentials, ForgotLockScreenViewModel.Result.InvalidPassword) + testClientDelete( + deleteClientResult = DeleteClientResult.Failure.InvalidCredentials, + expected = ForgotLockScreenViewModel.Result.Failure.InvalidPassword + ) @Test fun `given deleting client returns failure, when deleting current client, then return failure`() = - runTest(dispatcher.default()) { - val (_, viewModel) = Arrangement() - .withDeleteClientResult(DeleteClientResult.Failure.Generic(StorageFailure.DataNotFound)) - .arrange() - val result = viewModel.deleteCurrentClient("password") - assert(result.isLeft()) - } + testClientDelete( + deleteClientResult = DeleteClientResult.Failure.Generic(StorageFailure.DataNotFound), + expected = ForgotLockScreenViewModel.Result.Failure.Generic(StorageFailure.DataNotFound) + ) // sessions hard logout - private fun Arrangement.verifyHardLogoutActions(logoutCalled: Boolean) { - coVerify { observeEstablishedCallsUseCase() } - coVerify { endCallUseCase(any()) } - coVerify { globalDataStore.clearAppLockPasscode() } - coVerify { accountSwitchUseCase(SwitchAccountParam.Clear) } - val (atLeast, atMost) = if (logoutCalled) 1 to 1 else 0 to 0 - coVerify(atLeast = atLeast, atMost = atMost) { logoutUseCase(any(), any()) } - coVerify(atLeast = atLeast, atMost = atMost) { notificationManager.stopObservingOnLogout(any()) } - coVerify(atLeast = atLeast, atMost = atMost) { notificationChannelsManager.deleteChannelGroup(any()) } - coVerify(atLeast = atLeast, atMost = atMost) { userDataStore.clear() } + private fun Arrangement.verifyHardLogoutActions(successActionsCalled: Boolean, userLogoutActionsCalled: Boolean) { + val successActionsCalledExactly = if (successActionsCalled) 1 else 0 + coVerify(exactly = successActionsCalledExactly) { observeEstablishedCallsUseCase() } + coVerify(exactly = successActionsCalledExactly) { endCallUseCase(any()) } + coVerify(exactly = successActionsCalledExactly) { globalDataStore.clearAppLockPasscode() } + coVerify(exactly = successActionsCalledExactly) { accountSwitchUseCase(SwitchAccountParam.Clear) } + val logoutActionsCalledExactly = if (userLogoutActionsCalled) 1 else 0 + coVerify(exactly = logoutActionsCalledExactly) { logoutUseCase(any(), any()) } + coVerify(exactly = logoutActionsCalledExactly) { notificationManager.stopObservingOnLogout(any()) } + coVerify(exactly = logoutActionsCalledExactly) { notificationChannelsManager.deleteChannelGroup(any()) } + coVerify(exactly = logoutActionsCalledExactly) { userDataStore.clear() } } - private fun testSuccessfulLoggingOut(getSessionsResult: GetAllSessionsResult, logoutCalled: Boolean) = runTest(dispatcher.default()) { + private fun testLoggingOut( + getSessionsResult: GetAllSessionsResult, + expected: ForgotLockScreenViewModel.Result, + successActionsCalled: Boolean, + userLogoutActionsCalled: Boolean + ) = + runTest(dispatcher.default()) { val (arrangement, viewModel) = Arrangement() .withGetSessionsResult(getSessionsResult) .arrange() val result = viewModel.hardLogoutAllAccounts() advanceUntilIdle() - assert(result.isRight() && (result as Either.Right).value == ForgotLockScreenViewModel.Result.Success) - arrangement.verifyHardLogoutActions(logoutCalled = logoutCalled) + assertEquals(expected, result) + arrangement.verifyHardLogoutActions(successActionsCalled, userLogoutActionsCalled) } @Test fun `given no sessions, when logging out, then make all required actions other than logout and return success`() = - testSuccessfulLoggingOut(getSessionsResult = GetAllSessionsResult.Failure.NoSessionFound, logoutCalled = false) + testLoggingOut( + getSessionsResult = GetAllSessionsResult.Failure.NoSessionFound, + expected = ForgotLockScreenViewModel.Result.Success, + successActionsCalled = true, + userLogoutActionsCalled = false + ) @Test fun `given no valid sessions, when logging out, then make all required actions other than logout and return success`() = - testSuccessfulLoggingOut(getSessionsResult = GetAllSessionsResult.Success(listOf(INVALID_SESSION)), logoutCalled = false) + testLoggingOut( + getSessionsResult = GetAllSessionsResult.Success(listOf(INVALID_SESSION)), + expected = ForgotLockScreenViewModel.Result.Success, + successActionsCalled = true, + userLogoutActionsCalled = false + ) @Test fun `given valid sessions, when logging out, then make all required actions with logout and return success`() = - testSuccessfulLoggingOut(getSessionsResult = GetAllSessionsResult.Success(listOf(VALID_SESSION)), logoutCalled = true) + testLoggingOut( + getSessionsResult = GetAllSessionsResult.Success(listOf(VALID_SESSION)), + expected = ForgotLockScreenViewModel.Result.Success, + successActionsCalled = true, + userLogoutActionsCalled = true + ) @Test fun `given sessions return failure, when hard-logging out sessions, then return failure`() = - runTest(dispatcher.default()) { - val (_, viewModel) = Arrangement() - .withGetSessionsResult(GetAllSessionsResult.Failure.Generic(StorageFailure.DataNotFound)) - .arrange() - val result = viewModel.hardLogoutAllAccounts() - assert(result.isLeft()) - } + testLoggingOut( + getSessionsResult = GetAllSessionsResult.Failure.Generic(StorageFailure.DataNotFound), + expected = ForgotLockScreenViewModel.Result.Failure.Generic(StorageFailure.DataNotFound), + successActionsCalled = false, + userLogoutActionsCalled = false + ) class Arrangement { @MockK lateinit var coreLogic: CoreLogic From d0ace3ec7f96dafbc9beb1473a3178f7fdcbf329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Fri, 24 Nov 2023 09:43:48 +0100 Subject: [PATCH 2/2] fix: closing keyboard or back pressing when editing a message (#2463) --- .../android/ui/home/messagecomposer/EnabledMessageComposer.kt | 3 +++ .../android/ui/home/messagecomposer/MessageComposerInput.kt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt index 900e5a2f26b..e2de04840ec 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt @@ -282,6 +282,9 @@ fun EnabledMessageComposer( } } + BackHandler(inputStateHolder.inputType is MessageCompositionType.Editing) { + cancelEdit() + } BackHandler(isImeVisible || inputStateHolder.optionsVisible) { inputStateHolder.handleBackPressed( isImeVisible, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt index 7981a6849ff..5cd0d3f7b1d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt @@ -200,7 +200,7 @@ private fun InputContent( onPlusClick: () -> Unit, modifier: Modifier, ) { - if (!showOptions) { + if (!showOptions && inputType is MessageCompositionType.Composing) { AdditionalOptionButton( isSelected = false, onClick = {