From 2b983e923e71b96793e3c85a0315b3a07fc981c8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Dec 2023 12:10:11 +0100 Subject: [PATCH 01/10] Sign out: direct flow if not last session and if not currently backing up keys #2072 Extract some stuff from existing Logout to avoid duplication. --- .../logout/api/direct/DirectLogoutEvents.kt | 22 ++++ .../api/direct/DirectLogoutPresenter.kt | 21 ++++ .../logout/api/direct/DirectLogoutState.kt | 26 ++++ .../logout/api/direct/DirectLogoutView.kt | 27 +++++ .../features/logout/impl/LogoutView.kt | 56 +++------ .../direct/DefaultDirectLogoutPresenter.kt | 113 ++++++++++++++++++ .../impl/direct/DefaultDirectLogoutView.kt | 62 ++++++++++ .../features/logout/impl/tools/Extensions.kt | 31 +++++ .../logout/impl/ui/LogoutActionDialog.kt | 53 ++++++++ .../impl/ui/LogoutConfirmationDialog.kt | 37 ++++++ .../impl/root/PreferencesRootNode.kt | 19 ++- .../impl/root/PreferencesRootPresenter.kt | 5 + .../impl/root/PreferencesRootState.kt | 2 + .../impl/root/PreferencesRootStateProvider.kt | 10 ++ .../impl/root/PreferencesRootView.kt | 13 +- .../impl/root/PreferencesRootPresenterTest.kt | 32 +++-- 16 files changed, 477 insertions(+), 52 deletions(-) create mode 100644 features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutEvents.kt create mode 100644 features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutPresenter.kt create mode 100644 features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt create mode 100644 features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/tools/Extensions.kt create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutConfirmationDialog.kt diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutEvents.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutEvents.kt new file mode 100644 index 0000000000..ab5f40c321 --- /dev/null +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutEvents.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.api.direct + +sealed interface DirectLogoutEvents { + data class Logout(val ignoreSdkError: Boolean) : DirectLogoutEvents + data object CloseDialogs : DirectLogoutEvents +} diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutPresenter.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutPresenter.kt new file mode 100644 index 0000000000..bdd501cde6 --- /dev/null +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutPresenter.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.api.direct + +import io.element.android.libraries.architecture.Presenter + +interface DirectLogoutPresenter : Presenter diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt new file mode 100644 index 0000000000..71e97a856d --- /dev/null +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.api.direct + +import io.element.android.libraries.architecture.Async + +data class DirectLogoutState( + val canDoDirectSignOut: Boolean, + val showConfirmationDialog: Boolean, + val logoutAction: Async, + val eventSink: (DirectLogoutEvents) -> Unit, +) diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt new file mode 100644 index 0000000000..91c4dbcdda --- /dev/null +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.api.direct + +import androidx.compose.runtime.Composable + +interface DirectLogoutView { + @Composable + fun render( + state: DirectLogoutState, + onSuccessLogout: (logoutUrlResult: String?) -> Unit + ) +} diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt index d3d4805f82..e025f4825a 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -31,10 +30,11 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.features.logout.impl.tools.isBackingUp +import io.element.android.features.logout.impl.ui.LogoutActionDialog +import io.element.android.features.logout.impl.ui.LogoutConfirmationDialog import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage -import io.element.android.libraries.designsystem.components.ProgressDialog -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button @@ -81,10 +81,7 @@ fun LogoutView( // Log out confirmation dialog if (state.showConfirmationDialog) { - ConfirmationDialog( - title = stringResource(id = CommonStrings.action_signout), - content = stringResource(id = R.string.screen_signout_confirmation_dialog_content), - submitText = stringResource(id = CommonStrings.action_signout), + LogoutConfirmationDialog( onSubmitClicked = { eventSink(LogoutEvents.Logout(ignoreSdkError = false)) }, @@ -94,28 +91,18 @@ fun LogoutView( ) } - when (state.logoutAction) { - is Async.Loading -> - ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) - is Async.Failure -> - ConfirmationDialog( - title = stringResource(id = CommonStrings.dialog_title_error), - content = stringResource(id = CommonStrings.error_unknown), - submitText = stringResource(id = CommonStrings.action_signout_anyway), - onSubmitClicked = { - eventSink(LogoutEvents.Logout(ignoreSdkError = true)) - }, - onDismiss = { - eventSink(LogoutEvents.CloseDialogs) - } - ) - Async.Uninitialized -> - Unit - is Async.Success -> - LaunchedEffect(state.logoutAction) { - onSuccessLogout(state.logoutAction.data) - } - } + LogoutActionDialog( + state.logoutAction, + onForceLogoutClicked = { + eventSink(LogoutEvents.Logout(ignoreSdkError = true)) + }, + onDismissError = { + eventSink(LogoutEvents.CloseDialogs) + }, + onSuccessLogout = { + onSuccessLogout(it) + }, + ) } @Composable @@ -146,17 +133,6 @@ private fun subtitle(state: LogoutState): String? { } } -private fun BackupUploadState.isBackingUp(): Boolean { - return when (this) { - BackupUploadState.Waiting, - is BackupUploadState.Uploading -> true - is BackupUploadState.SteadyException -> exception is SteadyStateException.Connection - BackupUploadState.Unknown, - BackupUploadState.Done, - BackupUploadState.Error -> false - } -} - @Composable private fun ColumnScope.Buttons( state: LogoutState, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt new file mode 100644 index 0000000000..70827e8ab0 --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.impl.direct + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.logout.api.direct.DirectLogoutEvents +import io.element.android.features.logout.api.direct.DirectLogoutPresenter +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.logout.impl.tools.isBackingUp +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +class DefaultDirectLogoutPresenter @Inject constructor( + private val matrixClient: MatrixClient, + private val encryptionService: EncryptionService, + private val featureFlagService: FeatureFlagService, +) : DirectLogoutPresenter { + @Composable + override fun present(): DirectLogoutState { + val localCoroutineScope = rememberCoroutineScope() + + val logoutAction: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + + val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) + .collectAsState(initial = null) + + val backupUploadState: BackupUploadState by remember(secureStorageFlag) { + when (secureStorageFlag) { + true -> encryptionService.waitForBackupUploadSteadyState() + false -> flowOf(BackupUploadState.Done) + else -> emptyFlow() + } + } + .collectAsState(initial = BackupUploadState.Unknown) + + var showLogoutDialog by remember { mutableStateOf(false) } + var isLastSession by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + isLastSession = encryptionService.isLastDevice().getOrNull() ?: false + } + + fun handleEvents(event: DirectLogoutEvents) { + when (event) { + is DirectLogoutEvents.Logout -> { + if (showLogoutDialog || event.ignoreSdkError) { + showLogoutDialog = false + localCoroutineScope.logout(logoutAction, event.ignoreSdkError) + } else { + showLogoutDialog = true + } + } + DirectLogoutEvents.CloseDialogs -> { + logoutAction.value = Async.Uninitialized + showLogoutDialog = false + } + } + } + + return DirectLogoutState( + canDoDirectSignOut = !isLastSession && + !backupUploadState.isBackingUp(), + showConfirmationDialog = showLogoutDialog, + logoutAction = logoutAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.logout( + logoutAction: MutableState>, + ignoreSdkError: Boolean, + ) = launch { + suspend { + matrixClient.logout(ignoreSdkError) + }.runCatchingUpdatingState(logoutAction) + } +} diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt new file mode 100644 index 0000000000..8667f2200e --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.impl.direct + +import androidx.compose.runtime.Composable +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.logout.api.direct.DirectLogoutEvents +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.logout.api.direct.DirectLogoutView +import io.element.android.features.logout.impl.ui.LogoutActionDialog +import io.element.android.features.logout.impl.ui.LogoutConfirmationDialog +import io.element.android.libraries.di.SessionScope +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView { + @Composable + override fun render( + state: DirectLogoutState, + onSuccessLogout: (logoutUrlResult: String?) -> Unit, + ) { + val eventSink = state.eventSink + // Log out confirmation dialog + if (state.showConfirmationDialog) { + LogoutConfirmationDialog( + onSubmitClicked = { + eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false)) + }, + onDismiss = { + eventSink(DirectLogoutEvents.CloseDialogs) + } + ) + } + + LogoutActionDialog( + state.logoutAction, + onForceLogoutClicked = { + eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true)) + }, + onDismissError = { + eventSink(DirectLogoutEvents.CloseDialogs) + }, + onSuccessLogout = { + onSuccessLogout(it) + }, + ) + } +} diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/tools/Extensions.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/tools/Extensions.kt new file mode 100644 index 0000000000..ba71b2a045 --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/tools/Extensions.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.impl.tools + +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.matrix.api.encryption.SteadyStateException + +internal fun BackupUploadState.isBackingUp(): Boolean { + return when (this) { + BackupUploadState.Waiting, + is BackupUploadState.Uploading -> true + is BackupUploadState.SteadyException -> exception is SteadyStateException.Connection + BackupUploadState.Unknown, + BackupUploadState.Done, + BackupUploadState.Error -> false + } +} diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt new file mode 100644 index 0000000000..c5a3e41e73 --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.res.stringResource +import io.element.android.features.logout.impl.R +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun LogoutActionDialog( + state: Async, + onForceLogoutClicked: () -> Unit, + onDismissError: () -> Unit, + onSuccessLogout: (String?) -> Unit, +) { + when (state) { + is Async.Loading -> + ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) + is Async.Failure -> + ConfirmationDialog( + title = stringResource(id = CommonStrings.dialog_title_error), + content = stringResource(id = CommonStrings.error_unknown), + submitText = stringResource(id = CommonStrings.action_signout_anyway), + onSubmitClicked = onForceLogoutClicked, + onDismiss = onDismissError, + ) + Async.Uninitialized -> + Unit + is Async.Success -> + LaunchedEffect(state) { + onSuccessLogout(state.data) + } + } +} diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutConfirmationDialog.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutConfirmationDialog.kt new file mode 100644 index 0000000000..caf04a2752 --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutConfirmationDialog.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.features.logout.impl.R +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun LogoutConfirmationDialog( + onSubmitClicked: () -> Unit, + onDismiss: () -> Unit, +) { + ConfirmationDialog( + title = stringResource(id = CommonStrings.action_signout), + content = stringResource(id = R.string.screen_signout_confirmation_dialog_content), + submitText = stringResource(id = CommonStrings.action_signout), + onSubmitClicked = onSubmitClicked, + onDismiss = onDismiss, + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index ecb2f7f070..6fa8a65928 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -27,16 +27,19 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.logout.api.direct.DirectLogoutView import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.compound.theme.ElementTheme +import timber.log.Timber @ContributesNode(SessionScope::class) class PreferencesRootNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: PreferencesRootPresenter, + private val directLogoutView: DirectLogoutView, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { @@ -95,6 +98,13 @@ class PreferencesRootNode @AssistedInject constructor( } } + private fun onSuccessLogout(activity: Activity, url: String?) { + Timber.d("Success (direct) logout with result url: $url") + url?.let { + activity.openUrlInChromeCustomTab(null, false, it) + } + } + private fun onOpenNotificationSettings() { plugins().forEach { it.onOpenNotificationSettings() } } @@ -133,5 +143,12 @@ class PreferencesRootNode @AssistedInject constructor( onOpenUserProfile = this::onOpenUserProfile, onSignOutClicked = this::onSignOutClicked, ) + + directLogoutView.render( + state = state.directLogoutState, + onSuccessLogout = { + onSuccessLogout(activity, it) + } + ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 3295e3a59a..7472f6df7a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.features.logout.api.direct.DirectLogoutPresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -50,6 +51,7 @@ class PreferencesRootPresenter @Inject constructor( private val snackbarDispatcher: SnackbarDispatcher, private val featureFlagService: FeatureFlagService, private val indicatorService: IndicatorService, + private val directLogoutPresenter: DirectLogoutPresenter, ) : Presenter { @Composable @@ -88,6 +90,8 @@ class PreferencesRootPresenter @Inject constructor( mutableStateOf(null) } + val directLogoutState = directLogoutPresenter.present() + LaunchedEffect(Unit) { initAccountManagementUrl(accountManagementUrl, devicesManagementUrl) } @@ -105,6 +109,7 @@ class PreferencesRootPresenter @Inject constructor( showDeveloperSettings = showDeveloperSettings, showNotificationSettings = showNotificationSettings.value, showLockScreenSettings = showLockScreenSettings.value, + directLogoutState = directLogoutState, snackbarMessage = snackbarMessage, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index fec09f150f..0da537f26a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -16,6 +16,7 @@ package io.element.android.features.preferences.impl.root +import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.user.MatrixUser @@ -31,5 +32,6 @@ data class PreferencesRootState( val showDeveloperSettings: Boolean, val showLockScreenSettings: Boolean, val showNotificationSettings: Boolean, + val directLogoutState: DirectLogoutState, val snackbarMessage: SnackbarMessage?, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt index 74d8b0f0c9..db8c5b97b9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -16,6 +16,8 @@ package io.element.android.features.preferences.impl.root +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.ui.strings.CommonStrings @@ -32,4 +34,12 @@ fun aPreferencesRootState() = PreferencesRootState( showNotificationSettings = true, showLockScreenSettings = true, snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete), + directLogoutState = aDirectLogoutState(), +) + +fun aDirectLogoutState() = DirectLogoutState( + canDoDirectSignOut = true, + showConfirmationDialog = false, + logoutAction = Async.Uninitialized, + eventSink = {}, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 230648a96a..784202a892 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.user.UserPreferences import io.element.android.libraries.designsystem.components.list.ListItemContent @@ -57,7 +58,7 @@ fun PreferencesRootView( onManageAccountClicked: (url: String) -> Unit, onOpenAnalytics: () -> Unit, onOpenRageShake: () -> Unit, - onOpenLockScreenSettings: ()->Unit, + onOpenLockScreenSettings: () -> Unit, onOpenAbout: () -> Unit, onOpenDeveloperSettings: () -> Unit, onOpenAdvancedSettings: () -> Unit, @@ -91,7 +92,7 @@ fun PreferencesRootView( if (state.showSecureBackup) { ListItem( headlineContent = { Text(stringResource(id = CommonStrings.common_chat_backup)) }, - leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_key_filled),), + leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_key_filled)), trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge }, onClick = onSecureBackupClicked, ) @@ -162,7 +163,13 @@ fun PreferencesRootView( headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) }, leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_sign_out)), style = ListItemStyle.Destructive, - onClick = onSignOutClicked, + onClick = { + if (state.directLogoutState.canDoDirectSignOut) { + state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false)) + } else { + onSignOutClicked() + } + }, ) Text( modifier = Modifier diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index f914c21100..8655fb5df5 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -16,10 +16,14 @@ package io.element.android.features.preferences.impl.root +import androidx.compose.runtime.Composable import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.logout.api.direct.DirectLogoutPresenter +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.test.FakeFeatureFlagService @@ -41,23 +45,34 @@ class PreferencesRootPresenterTest { @get:Rule val warmUpRule = WarmUpRule() + private val aDirectLogoutState = DirectLogoutState( + canDoDirectSignOut = true, + showConfirmationDialog = false, + logoutAction = Async.Uninitialized, + eventSink = {}, + ) + @Test fun `present - initial state`() = runTest { val matrixClient = FakeMatrixClient() val sessionVerificationService = FakeSessionVerificationService() val presenter = PreferencesRootPresenter( - matrixClient, - sessionVerificationService, - FakeAnalyticsService(), - BuildType.DEBUG, - FakeVersionFormatter(), - SnackbarDispatcher(), - FakeFeatureFlagService(), - DefaultIndicatorService( + matrixClient = matrixClient, + sessionVerificationService = sessionVerificationService, + analyticsService = FakeAnalyticsService(), + buildType = BuildType.DEBUG, + versionFormatter = FakeVersionFormatter(), + snackbarDispatcher = SnackbarDispatcher(), + featureFlagService = FakeFeatureFlagService(), + indicatorService = DefaultIndicatorService( sessionVerificationService = sessionVerificationService, encryptionService = FakeEncryptionService(), featureFlagService = FakeFeatureFlagService(), ), + directLogoutPresenter = object : DirectLogoutPresenter { + @Composable + override fun present() = aDirectLogoutState + } ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -77,6 +92,7 @@ class PreferencesRootPresenterTest { assertThat(loadedState.showAnalyticsSettings).isFalse() assertThat(loadedState.accountManagementUrl).isNull() assertThat(loadedState.devicesManagementUrl).isNull() + assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState) } } } From 25e4e0424d87241880c0e2e4d42c5003966c7adc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Dec 2023 12:20:19 +0100 Subject: [PATCH 02/10] Test more fields --- .../impl/root/PreferencesRootPresenterTest.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 8655fb5df5..fc657bfbd7 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -88,11 +88,18 @@ class PreferencesRootPresenterTest { avatarUrl = AN_AVATAR_URL ) ) - assertThat(loadedState.showDeveloperSettings).isTrue() - assertThat(loadedState.showAnalyticsSettings).isFalse() + assertThat(initialState.version).isEqualTo("A Version") + assertThat(loadedState.showCompleteVerification).isTrue() + assertThat(loadedState.showSecureBackup).isFalse() + assertThat(loadedState.showSecureBackupBadge).isTrue() assertThat(loadedState.accountManagementUrl).isNull() assertThat(loadedState.devicesManagementUrl).isNull() + assertThat(loadedState.showAnalyticsSettings).isFalse() + assertThat(loadedState.showDeveloperSettings).isTrue() + assertThat(loadedState.showLockScreenSettings).isTrue() + assertThat(loadedState.showNotificationSettings).isTrue() assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState) + assertThat(loadedState.snackbarMessage).isNull() } } } From c871dc2492c0a5fd448ce84996445c15abdd6021 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Dec 2023 12:32:36 +0100 Subject: [PATCH 03/10] Add test for `DefaultDirectLogoutPresenter` --- .../DefaultDirectLogoutPresenterTest.kt | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt new file mode 100644 index 0000000000..d9cd793105 --- /dev/null +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.logout.impl.direct + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.logout.api.direct.DirectLogoutEvents +import io.element.android.features.logout.api.direct.DirectLogoutPresenter +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class DefaultDirectLogoutPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createDefaultDirectLogoutPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitLastSequentialItem() + assertThat(initialState.canDoDirectSignOut).isTrue() + assertThat(initialState.showConfirmationDialog).isFalse() + assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - initial state - last session`() = runTest { + val presenter = createDefaultDirectLogoutPresenter( + encryptionService = FakeEncryptionService().apply { + givenIsLastDevice(true) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(2) + val initialState = awaitItem() + assertThat(initialState.canDoDirectSignOut).isFalse() + assertThat(initialState.showConfirmationDialog).isFalse() + assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - initial state - backing up`() = runTest { + val encryptionService = FakeEncryptionService() + encryptionService.givenWaitForBackupUploadSteadyStateFlow( + flow { + emit(BackupUploadState.Waiting) + } + ) + val presenter = createDefaultDirectLogoutPresenter( + encryptionService = encryptionService + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(2) + val initialState = awaitItem() + assertThat(initialState.canDoDirectSignOut).isFalse() + assertThat(initialState.showConfirmationDialog).isFalse() + assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - logout then cancel`() = runTest { + val presenter = createDefaultDirectLogoutPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitLastSequentialItem() + initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + val confirmationState = awaitItem() + assertThat(confirmationState.showConfirmationDialog).isTrue() + initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs) + val finalState = awaitItem() + assertThat(finalState.showConfirmationDialog).isFalse() + } + } + + @Test + fun `present - logout then confirm`() = runTest { + val presenter = createDefaultDirectLogoutPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitLastSequentialItem() + initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + val confirmationState = awaitItem() + assertThat(confirmationState.showConfirmationDialog).isTrue() + confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + skipItems(1) + val loadingState = awaitItem() + assertThat(loadingState.showConfirmationDialog).isFalse() + assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java) + val successState = awaitItem() + assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java) + } + } + + @Test + fun `present - logout with error then cancel`() = runTest { + val matrixClient = FakeMatrixClient().apply { + givenLogoutError(A_THROWABLE) + } + val presenter = createDefaultDirectLogoutPresenter( + matrixClient, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + val confirmationState = awaitItem() + assertThat(confirmationState.showConfirmationDialog).isTrue() + confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + skipItems(1) + val loadingState = awaitItem() + assertThat(loadingState.showConfirmationDialog).isFalse() + assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java) + val errorState = awaitItem() + assertThat(errorState.logoutAction).isEqualTo(Async.Failure(A_THROWABLE)) + errorState.eventSink.invoke(DirectLogoutEvents.CloseDialogs) + val finalState = awaitItem() + assertThat(finalState.logoutAction).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - logout with error then force`() = runTest { + val matrixClient = FakeMatrixClient().apply { + givenLogoutError(A_THROWABLE) + } + val presenter = createDefaultDirectLogoutPresenter( + matrixClient, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + val confirmationState = awaitItem() + assertThat(confirmationState.showConfirmationDialog).isTrue() + confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + skipItems(1) + val loadingState = awaitItem() + assertThat(loadingState.showConfirmationDialog).isFalse() + assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java) + val errorState = awaitItem() + assertThat(errorState.logoutAction).isEqualTo(Async.Failure(A_THROWABLE)) + errorState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = true)) + val loadingState2 = awaitItem() + assertThat(loadingState2.showConfirmationDialog).isFalse() + assertThat(loadingState2.logoutAction).isInstanceOf(Async.Loading::class.java) + val successState = awaitItem() + assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java) + } + } + + private fun createDefaultDirectLogoutPresenter( + matrixClient: MatrixClient = FakeMatrixClient(), + encryptionService: EncryptionService = FakeEncryptionService(), + ): DefaultDirectLogoutPresenter = DefaultDirectLogoutPresenter( + matrixClient = matrixClient, + encryptionService = encryptionService, + featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), + ) +} + From 1f2b2df46999bff4a15ab0b0761af8615c9d47b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Dec 2023 13:24:59 +0100 Subject: [PATCH 04/10] Quality --- .../android/features/logout/api/direct/DirectLogoutView.kt | 2 +- .../features/logout/impl/direct/DefaultDirectLogoutView.kt | 2 +- .../logout/impl/direct/DefaultDirectLogoutPresenterTest.kt | 1 - .../features/preferences/impl/root/PreferencesRootNode.kt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt index 91c4dbcdda..0fd4c04cf2 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt @@ -20,7 +20,7 @@ import androidx.compose.runtime.Composable interface DirectLogoutView { @Composable - fun render( + fun Render( state: DirectLogoutState, onSuccessLogout: (logoutUrlResult: String?) -> Unit ) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt index 8667f2200e..bd00975864 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt @@ -29,7 +29,7 @@ import javax.inject.Inject @ContributesBinding(SessionScope::class) class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView { @Composable - override fun render( + override fun Render( state: DirectLogoutState, onSuccessLogout: (logoutUrlResult: String?) -> Unit, ) { diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt index d9cd793105..5341a8c19e 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt @@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.logout.api.direct.DirectLogoutEvents -import io.element.android.features.logout.api.direct.DirectLogoutPresenter import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.libraries.architecture.Async import io.element.android.libraries.featureflag.api.FeatureFlags diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index 6fa8a65928..ee79de0cce 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -144,7 +144,7 @@ class PreferencesRootNode @AssistedInject constructor( onSignOutClicked = this::onSignOutClicked, ) - directLogoutView.render( + directLogoutView.Render( state = state.directLogoutState, onSuccessLogout = { onSuccessLogout(activity, it) From 59993bf6cf53d0344700af2fff7a24d7a844f441 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Dec 2023 13:26:01 +0100 Subject: [PATCH 05/10] towncrier file --- changelog.d/2072.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2072.misc diff --git a/changelog.d/2072.misc b/changelog.d/2072.misc new file mode 100644 index 0000000000..7ae9d0be44 --- /dev/null +++ b/changelog.d/2072.misc @@ -0,0 +1 @@ + Remove extra logout screen. From 6a3084d8c6e20fa5978b5fcd160e0537b6cb8e41 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 26 Dec 2023 13:01:40 +0000 Subject: [PATCH 06/10] Update screenshots --- ...null_CreateRoomRootView-Day-4_4_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_CreateRoomRootView-Night-4_5_null_0,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Day-4_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Day-4_4_null_0,NEXUS_5,1.0,en].png index 3b6d8bc1b0..c5f3236dde 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Day-4_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Day-4_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e055da42d13817ccd4f6de3a45ac24db3a64aaf58e6f43e66f362ce391873100 -size 23794 +oid sha256:bb9b7d6711afd4c21acd3a5a1c93807c4add46cba94ede30aca92bfbc533a2da +size 23879 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Night-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Night-4_5_null_0,NEXUS_5,1.0,en].png index 55017d72a8..d0d90c769b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Night-4_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Night-4_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ab61e6f09be66482c0afb7f0667ed249b2b1e67bb6b41cfc4618237e9013bef -size 22278 +oid sha256:14554d6b72e1daa3e7385fbc26e9f377d281f6f6bfa8d9c4b8048bf0f8fd104b +size 22339 From 481d80d1f0d0042ab2f89271780621ca6c73614d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Dec 2023 15:39:56 +0100 Subject: [PATCH 07/10] Add comment to clarify the `SteadyException` case. --- .../io/element/android/features/logout/impl/tools/Extensions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/tools/Extensions.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/tools/Extensions.kt index ba71b2a045..5d9858782d 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/tools/Extensions.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/tools/Extensions.kt @@ -23,6 +23,7 @@ internal fun BackupUploadState.isBackingUp(): Boolean { return when (this) { BackupUploadState.Waiting, is BackupUploadState.Uploading -> true + // The backup is in progress, but there have been a network issue, so we have to warn the user. is BackupUploadState.SteadyException -> exception is SteadyStateException.Connection BackupUploadState.Unknown, BackupUploadState.Done, From 7db7a32d4361a1a73dcefa325813fedb2f41edec Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Dec 2023 15:42:44 +0100 Subject: [PATCH 08/10] Use a RetryDialog here, it does not change the rendering, but is more appropriate. --- .../android/features/logout/impl/ui/LogoutActionDialog.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt index c5a3e41e73..9baad1d1f3 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/ui/LogoutActionDialog.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.res.stringResource import io.element.android.features.logout.impl.R import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.ProgressDialog -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -36,11 +36,11 @@ fun LogoutActionDialog( is Async.Loading -> ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) is Async.Failure -> - ConfirmationDialog( + RetryDialog( title = stringResource(id = CommonStrings.dialog_title_error), content = stringResource(id = CommonStrings.error_unknown), - submitText = stringResource(id = CommonStrings.action_signout_anyway), - onSubmitClicked = onForceLogoutClicked, + retryText = stringResource(id = CommonStrings.action_signout_anyway), + onRetry = onForceLogoutClicked, onDismiss = onDismissError, ) Async.Uninitialized -> From d05cf6d64943dd32b6042cc4fc93f0cb001a6105 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Dec 2023 15:47:42 +0100 Subject: [PATCH 09/10] Move the logic to the Node, which handles navigation. --- .../preferences/impl/root/PreferencesRootNode.kt | 9 ++++++++- .../preferences/impl/root/PreferencesRootView.kt | 9 +-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index ee79de0cce..05d11b85f1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -28,6 +28,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.compound.theme.ElementTheme +import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutView import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.SessionScope @@ -141,7 +142,13 @@ class PreferencesRootNode @AssistedInject constructor( onOpenNotificationSettings = this::onOpenNotificationSettings, onOpenLockScreenSettings = this::onOpenLockScreenSettings, onOpenUserProfile = this::onOpenUserProfile, - onSignOutClicked = this::onSignOutClicked, + onSignOutClicked = { + if (state.directLogoutState.canDoDirectSignOut) { + state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false)) + } else { + onSignOutClicked() + } + }, ) directLogoutView.Render( diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 784202a892..f25cb515e9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.user.UserPreferences import io.element.android.libraries.designsystem.components.list.ListItemContent @@ -163,13 +162,7 @@ fun PreferencesRootView( headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) }, leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_sign_out)), style = ListItemStyle.Destructive, - onClick = { - if (state.directLogoutState.canDoDirectSignOut) { - state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false)) - } else { - onSignOutClicked() - } - }, + onClick = onSignOutClicked, ) Text( modifier = Modifier From 076b487a0a49870b092ca45e6db1a76796cab3d8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Dec 2023 16:55:58 +0100 Subject: [PATCH 10/10] Fix Maestro --- .maestro/tests/account/logout.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.maestro/tests/account/logout.yaml b/.maestro/tests/account/logout.yaml index f10d5ef364..3019f1d2c3 100644 --- a/.maestro/tests/account/logout.yaml +++ b/.maestro/tests/account/logout.yaml @@ -6,12 +6,10 @@ appId: ${APP_ID} - takeScreenshot: build/maestro/900-SignOutScreen - back - tapOn: "Sign out" -- tapOn: - id: "sign-out-submit" # Ensure cancel cancels -- tapOn: "Cancel" - tapOn: - id: "sign-out-submit" + id: "dialog-negative" +- tapOn: "Sign out" - tapOn: id: "dialog-positive" - runFlow: ../assertions/assertInitDisplayed.yaml