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.
This commit is contained in:
parent
43f87b206f
commit
c65f5ed560
16 changed files with 477 additions and 52 deletions
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<DirectLogoutState>
|
||||||
|
|
@ -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<String?>,
|
||||||
|
val eventSink: (DirectLogoutEvents) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
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.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
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.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
|
@ -81,10 +81,7 @@ fun LogoutView(
|
||||||
|
|
||||||
// Log out confirmation dialog
|
// Log out confirmation dialog
|
||||||
if (state.showConfirmationDialog) {
|
if (state.showConfirmationDialog) {
|
||||||
ConfirmationDialog(
|
LogoutConfirmationDialog(
|
||||||
title = stringResource(id = CommonStrings.action_signout),
|
|
||||||
content = stringResource(id = R.string.screen_signout_confirmation_dialog_content),
|
|
||||||
submitText = stringResource(id = CommonStrings.action_signout),
|
|
||||||
onSubmitClicked = {
|
onSubmitClicked = {
|
||||||
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
|
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
|
||||||
},
|
},
|
||||||
|
|
@ -94,28 +91,18 @@ fun LogoutView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (state.logoutAction) {
|
LogoutActionDialog(
|
||||||
is Async.Loading ->
|
state.logoutAction,
|
||||||
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
|
onForceLogoutClicked = {
|
||||||
is Async.Failure ->
|
eventSink(LogoutEvents.Logout(ignoreSdkError = true))
|
||||||
ConfirmationDialog(
|
},
|
||||||
title = stringResource(id = CommonStrings.dialog_title_error),
|
onDismissError = {
|
||||||
content = stringResource(id = CommonStrings.error_unknown),
|
eventSink(LogoutEvents.CloseDialogs)
|
||||||
submitText = stringResource(id = CommonStrings.action_signout_anyway),
|
},
|
||||||
onSubmitClicked = {
|
onSuccessLogout = {
|
||||||
eventSink(LogoutEvents.Logout(ignoreSdkError = true))
|
onSuccessLogout(it)
|
||||||
},
|
},
|
||||||
onDismiss = {
|
)
|
||||||
eventSink(LogoutEvents.CloseDialogs)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Async.Uninitialized ->
|
|
||||||
Unit
|
|
||||||
is Async.Success ->
|
|
||||||
LaunchedEffect(state.logoutAction) {
|
|
||||||
onSuccessLogout(state.logoutAction.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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
|
@Composable
|
||||||
private fun ColumnScope.Buttons(
|
private fun ColumnScope.Buttons(
|
||||||
state: LogoutState,
|
state: LogoutState,
|
||||||
|
|
|
||||||
|
|
@ -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<Async<String?>> = 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<Async<String?>>,
|
||||||
|
ignoreSdkError: Boolean,
|
||||||
|
) = launch {
|
||||||
|
suspend {
|
||||||
|
matrixClient.logout(ignoreSdkError)
|
||||||
|
}.runCatchingUpdatingState(logoutAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String?>,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -27,16 +27,19 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
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.androidutils.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class PreferencesRootNode @AssistedInject constructor(
|
class PreferencesRootNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: PreferencesRootPresenter,
|
private val presenter: PreferencesRootPresenter,
|
||||||
|
private val directLogoutView: DirectLogoutView,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
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() {
|
private fun onOpenNotificationSettings() {
|
||||||
plugins<Callback>().forEach { it.onOpenNotificationSettings() }
|
plugins<Callback>().forEach { it.onOpenNotificationSettings() }
|
||||||
}
|
}
|
||||||
|
|
@ -133,5 +143,12 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
onOpenUserProfile = this::onOpenUserProfile,
|
onOpenUserProfile = this::onOpenUserProfile,
|
||||||
onSignOutClicked = this::onSignOutClicked,
|
onSignOutClicked = this::onSignOutClicked,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
directLogoutView.render(
|
||||||
|
state = state.directLogoutState,
|
||||||
|
onSuccessLogout = {
|
||||||
|
onSuccessLogout(activity, it)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
|
@ -50,6 +51,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val indicatorService: IndicatorService,
|
private val indicatorService: IndicatorService,
|
||||||
|
private val directLogoutPresenter: DirectLogoutPresenter,
|
||||||
) : Presenter<PreferencesRootState> {
|
) : Presenter<PreferencesRootState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -88,6 +90,8 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
mutableStateOf(null)
|
mutableStateOf(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val directLogoutState = directLogoutPresenter.present()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
initAccountManagementUrl(accountManagementUrl, devicesManagementUrl)
|
initAccountManagementUrl(accountManagementUrl, devicesManagementUrl)
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +109,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
showDeveloperSettings = showDeveloperSettings,
|
showDeveloperSettings = showDeveloperSettings,
|
||||||
showNotificationSettings = showNotificationSettings.value,
|
showNotificationSettings = showNotificationSettings.value,
|
||||||
showLockScreenSettings = showLockScreenSettings.value,
|
showLockScreenSettings = showLockScreenSettings.value,
|
||||||
|
directLogoutState = directLogoutState,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.root
|
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.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
|
|
@ -31,5 +32,6 @@ data class PreferencesRootState(
|
||||||
val showDeveloperSettings: Boolean,
|
val showDeveloperSettings: Boolean,
|
||||||
val showLockScreenSettings: Boolean,
|
val showLockScreenSettings: Boolean,
|
||||||
val showNotificationSettings: Boolean,
|
val showNotificationSettings: Boolean,
|
||||||
|
val directLogoutState: DirectLogoutState,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.root
|
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.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
|
@ -32,4 +34,12 @@ fun aPreferencesRootState() = PreferencesRootState(
|
||||||
showNotificationSettings = true,
|
showNotificationSettings = true,
|
||||||
showLockScreenSettings = true,
|
showLockScreenSettings = true,
|
||||||
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
||||||
|
directLogoutState = aDirectLogoutState(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aDirectLogoutState() = DirectLogoutState(
|
||||||
|
canDoDirectSignOut = true,
|
||||||
|
showConfirmationDialog = false,
|
||||||
|
logoutAction = Async.Uninitialized,
|
||||||
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
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.R
|
||||||
import io.element.android.features.preferences.impl.user.UserPreferences
|
import io.element.android.features.preferences.impl.user.UserPreferences
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
|
|
@ -57,7 +58,7 @@ fun PreferencesRootView(
|
||||||
onManageAccountClicked: (url: String) -> Unit,
|
onManageAccountClicked: (url: String) -> Unit,
|
||||||
onOpenAnalytics: () -> Unit,
|
onOpenAnalytics: () -> Unit,
|
||||||
onOpenRageShake: () -> Unit,
|
onOpenRageShake: () -> Unit,
|
||||||
onOpenLockScreenSettings: ()->Unit,
|
onOpenLockScreenSettings: () -> Unit,
|
||||||
onOpenAbout: () -> Unit,
|
onOpenAbout: () -> Unit,
|
||||||
onOpenDeveloperSettings: () -> Unit,
|
onOpenDeveloperSettings: () -> Unit,
|
||||||
onOpenAdvancedSettings: () -> Unit,
|
onOpenAdvancedSettings: () -> Unit,
|
||||||
|
|
@ -91,7 +92,7 @@ fun PreferencesRootView(
|
||||||
if (state.showSecureBackup) {
|
if (state.showSecureBackup) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.common_chat_backup)) },
|
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 },
|
trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge },
|
||||||
onClick = onSecureBackupClicked,
|
onClick = onSecureBackupClicked,
|
||||||
)
|
)
|
||||||
|
|
@ -162,7 +163,13 @@ fun PreferencesRootView(
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) },
|
headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_sign_out)),
|
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_sign_out)),
|
||||||
style = ListItemStyle.Destructive,
|
style = ListItemStyle.Destructive,
|
||||||
onClick = onSignOutClicked,
|
onClick = {
|
||||||
|
if (state.directLogoutState.canDoDirectSignOut) {
|
||||||
|
state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||||
|
} else {
|
||||||
|
onSignOutClicked()
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,14 @@
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.root
|
package io.element.android.features.preferences.impl.root
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import app.cash.molecule.RecompositionMode
|
import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.core.meta.BuildType
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
|
@ -41,23 +45,34 @@ class PreferencesRootPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
private val aDirectLogoutState = DirectLogoutState(
|
||||||
|
canDoDirectSignOut = true,
|
||||||
|
showConfirmationDialog = false,
|
||||||
|
logoutAction = Async.Uninitialized,
|
||||||
|
eventSink = {},
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient()
|
val matrixClient = FakeMatrixClient()
|
||||||
val sessionVerificationService = FakeSessionVerificationService()
|
val sessionVerificationService = FakeSessionVerificationService()
|
||||||
val presenter = PreferencesRootPresenter(
|
val presenter = PreferencesRootPresenter(
|
||||||
matrixClient,
|
matrixClient = matrixClient,
|
||||||
sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
FakeAnalyticsService(),
|
analyticsService = FakeAnalyticsService(),
|
||||||
BuildType.DEBUG,
|
buildType = BuildType.DEBUG,
|
||||||
FakeVersionFormatter(),
|
versionFormatter = FakeVersionFormatter(),
|
||||||
SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
DefaultIndicatorService(
|
indicatorService = DefaultIndicatorService(
|
||||||
sessionVerificationService = sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
encryptionService = FakeEncryptionService(),
|
encryptionService = FakeEncryptionService(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
),
|
),
|
||||||
|
directLogoutPresenter = object : DirectLogoutPresenter {
|
||||||
|
@Composable
|
||||||
|
override fun present() = aDirectLogoutState
|
||||||
|
}
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -77,6 +92,7 @@ class PreferencesRootPresenterTest {
|
||||||
assertThat(loadedState.showAnalyticsSettings).isFalse()
|
assertThat(loadedState.showAnalyticsSettings).isFalse()
|
||||||
assertThat(loadedState.accountManagementUrl).isNull()
|
assertThat(loadedState.accountManagementUrl).isNull()
|
||||||
assertThat(loadedState.devicesManagementUrl).isNull()
|
assertThat(loadedState.devicesManagementUrl).isNull()
|
||||||
|
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue