From 2b983e923e71b96793e3c85a0315b3a07fc981c8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Dec 2023 12:10:11 +0100 Subject: [PATCH] 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) } } }