Benoit Marty
9 months ago
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 |
||||||
|
) |
||||||
|
} |
@ -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, |
||||||
|
) |
||||||
|
} |
Loading…
Reference in new issue