Jorge Martín
1 month ago
23 changed files with 1003 additions and 68 deletions
@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset |
||||
|
||||
import io.element.android.libraries.architecture.AsyncData |
||||
import io.element.android.libraries.di.SessionScope |
||||
import io.element.android.libraries.di.SingleIn |
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope |
||||
import io.element.android.libraries.matrix.api.MatrixClient |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle |
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService |
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.flow.MutableStateFlow |
||||
import kotlinx.coroutines.flow.StateFlow |
||||
import kotlinx.coroutines.flow.distinctUntilChanged |
||||
import kotlinx.coroutines.flow.filter |
||||
import kotlinx.coroutines.flow.filterIsInstance |
||||
import kotlinx.coroutines.flow.first |
||||
import kotlinx.coroutines.flow.map |
||||
import kotlinx.coroutines.launch |
||||
import javax.inject.Inject |
||||
|
||||
class ResetIdentityFlowManager @Inject constructor( |
||||
private val matrixClient: MatrixClient, |
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, |
||||
private val sessionVerificationService: SessionVerificationService, |
||||
) { |
||||
private val resetHandleFlow: MutableStateFlow<AsyncData<IdentityResetHandle>> = MutableStateFlow(AsyncData.Uninitialized) |
||||
val currentHandleFlow: StateFlow<AsyncData<IdentityResetHandle>> = resetHandleFlow |
||||
|
||||
fun whenResetIsDone(block: () -> Unit) { |
||||
sessionCoroutineScope.launch { |
||||
sessionVerificationService.sessionVerifiedStatus.filterIsInstance<SessionVerifiedStatus.Verified>().first() |
||||
block() |
||||
} |
||||
} |
||||
|
||||
fun currentSessionId(): SessionId { |
||||
return matrixClient.sessionId |
||||
} |
||||
|
||||
fun getResetHandle(): StateFlow<AsyncData<IdentityResetHandle>> { |
||||
return if (resetHandleFlow.value.isLoading() || resetHandleFlow.value.isSuccess()) { |
||||
resetHandleFlow |
||||
} else { |
||||
resetHandleFlow.value = AsyncData.Loading() |
||||
|
||||
sessionCoroutineScope.launch { |
||||
matrixClient.encryptionService().startIdentityReset() |
||||
.onSuccess { handle -> |
||||
resetHandleFlow.value = if (handle != null) { |
||||
AsyncData.Success(handle) |
||||
} else { |
||||
AsyncData.Failure(IllegalStateException("Could not get a reset identity handle")) |
||||
} |
||||
} |
||||
.onFailure { resetHandleFlow.value = AsyncData.Failure(it) } |
||||
} |
||||
|
||||
resetHandleFlow |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,127 @@
@@ -0,0 +1,127 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset |
||||
|
||||
import android.app.Activity |
||||
import android.os.Parcelable |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.platform.LocalContext |
||||
import com.bumble.appyx.core.modality.BuildContext |
||||
import com.bumble.appyx.core.node.Node |
||||
import com.bumble.appyx.core.plugin.Plugin |
||||
import com.bumble.appyx.core.plugin.plugins |
||||
import com.bumble.appyx.navmodel.backstack.BackStack |
||||
import com.bumble.appyx.navmodel.backstack.operation.push |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.anvilannotations.ContributesNode |
||||
import io.element.android.features.securebackup.impl.reset.password.ResetKeyPasswordNode |
||||
import io.element.android.features.securebackup.impl.reset.root.ResetKeyRootNode |
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab |
||||
import io.element.android.libraries.architecture.AsyncData |
||||
import io.element.android.libraries.architecture.BackstackView |
||||
import io.element.android.libraries.architecture.BaseFlowNode |
||||
import io.element.android.libraries.architecture.createNode |
||||
import io.element.android.libraries.di.SessionScope |
||||
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle |
||||
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle |
||||
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.flow.filterIsInstance |
||||
import kotlinx.coroutines.flow.first |
||||
import kotlinx.coroutines.launch |
||||
import kotlinx.parcelize.Parcelize |
||||
|
||||
@ContributesNode(SessionScope::class) |
||||
class ResetIdentityFlowNode @AssistedInject constructor( |
||||
@Assisted buildContext: BuildContext, |
||||
@Assisted plugins: List<Plugin>, |
||||
private val resetIdentityFlowManager: ResetIdentityFlowManager, |
||||
private val coroutineScope: CoroutineScope, |
||||
) : BaseFlowNode<ResetIdentityFlowNode.NavTarget>( |
||||
backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap), |
||||
buildContext = buildContext, |
||||
plugins = plugins, |
||||
) { |
||||
interface Callback: Plugin { |
||||
fun onDone() |
||||
} |
||||
|
||||
sealed interface NavTarget : Parcelable { |
||||
@Parcelize |
||||
data object Root : NavTarget |
||||
|
||||
@Parcelize |
||||
data object ResetPassword : NavTarget |
||||
|
||||
// @Parcelize |
||||
// data class ResetOidc(val url: String) : NavTarget |
||||
} |
||||
|
||||
private lateinit var activity: Activity |
||||
|
||||
override fun onBuilt() { |
||||
super.onBuilt() |
||||
|
||||
resetIdentityFlowManager.whenResetIsDone { |
||||
plugins<Callback>().forEach { it.onDone() } |
||||
} |
||||
} |
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { |
||||
return when (navTarget) { |
||||
is NavTarget.Root -> { |
||||
val callback = object : ResetKeyRootNode.Callback { |
||||
override fun onContinue() { |
||||
coroutineScope.startReset() |
||||
} |
||||
} |
||||
createNode<ResetKeyRootNode>(buildContext, listOf(callback)) |
||||
} |
||||
is NavTarget.ResetPassword -> { |
||||
val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found") |
||||
createNode<ResetKeyPasswordNode>( |
||||
buildContext, |
||||
listOf(ResetKeyPasswordNode.Inputs(resetIdentityFlowManager.currentSessionId(), handle)) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun CoroutineScope.startReset() = launch { |
||||
val handle = resetIdentityFlowManager.getResetHandle() |
||||
.filterIsInstance<AsyncData.Success<IdentityResetHandle>>() |
||||
.first() |
||||
.data |
||||
|
||||
when (handle) { |
||||
is IdentityOidcResetHandle -> { |
||||
activity.openUrlInChromeCustomTab(null, false, handle.url) |
||||
handle.resetOidc() |
||||
} |
||||
is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
override fun View(modifier: Modifier) { |
||||
(LocalContext.current as? Activity)?.let { activity = it } |
||||
|
||||
BackstackView(modifier) |
||||
} |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.password |
||||
|
||||
sealed interface ResetKeyPasswordEvent { |
||||
data class Reset(val password: String) : ResetKeyPasswordEvent |
||||
data object DismissError : ResetKeyPasswordEvent |
||||
} |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.password |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import com.bumble.appyx.core.modality.BuildContext |
||||
import com.bumble.appyx.core.node.Node |
||||
import com.bumble.appyx.core.plugin.Plugin |
||||
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.libraries.architecture.NodeInputs |
||||
import io.element.android.libraries.architecture.inputs |
||||
import io.element.android.libraries.di.SessionScope |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle |
||||
|
||||
@ContributesNode(SessionScope::class) |
||||
class ResetKeyPasswordNode @AssistedInject constructor( |
||||
@Assisted buildContext: BuildContext, |
||||
@Assisted plugins: List<Plugin>, |
||||
) : Node(buildContext, plugins = plugins) { |
||||
|
||||
data class Inputs(val userId: UserId, val handle: IdentityPasswordResetHandle) : NodeInputs |
||||
|
||||
private val presenter by lazy { |
||||
val inputs = inputs<Inputs>() |
||||
ResetKeyPasswordPresenter(inputs.userId, inputs.handle) |
||||
} |
||||
|
||||
@Composable |
||||
override fun View(modifier: Modifier) { |
||||
val state = presenter.present() |
||||
ResetKeyPasswordView( |
||||
state = state, |
||||
onBack = ::navigateUp |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.password |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.MutableState |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.rememberCoroutineScope |
||||
import io.element.android.libraries.architecture.AsyncAction |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.launch |
||||
|
||||
class ResetKeyPasswordPresenter( |
||||
private val userId: UserId, |
||||
private val identityPasswordResetHandle: IdentityPasswordResetHandle, |
||||
) : Presenter<ResetKeyPasswordState> { |
||||
@Composable |
||||
override fun present(): ResetKeyPasswordState { |
||||
val coroutineScope = rememberCoroutineScope() |
||||
|
||||
val resetAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } |
||||
|
||||
fun handleEvent(event: ResetKeyPasswordEvent) { |
||||
when (event) { |
||||
is ResetKeyPasswordEvent.Reset -> coroutineScope.reset(userId, event.password, resetAction) |
||||
ResetKeyPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized |
||||
} |
||||
} |
||||
|
||||
return ResetKeyPasswordState( |
||||
resetAction = resetAction.value, |
||||
eventSink = ::handleEvent |
||||
) |
||||
} |
||||
|
||||
private fun CoroutineScope.reset(userId: UserId, password: String, action: MutableState<AsyncAction<Unit>>) = launch { |
||||
suspend { |
||||
identityPasswordResetHandle.resetPassword(userId, password).getOrThrow() |
||||
}.runCatchingUpdatingState(action) |
||||
} |
||||
} |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.password |
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction |
||||
|
||||
data class ResetKeyPasswordState( |
||||
val resetAction: AsyncAction<Unit>, |
||||
val eventSink: (ResetKeyPasswordEvent) -> Unit, |
||||
) |
@ -0,0 +1,120 @@
@@ -0,0 +1,120 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.password |
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.MutableState |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.setValue |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.platform.LocalFocusManager |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation |
||||
import androidx.compose.ui.text.input.VisualTransformation |
||||
import io.element.android.compound.tokens.generated.CompoundIcons |
||||
import io.element.android.libraries.architecture.AsyncAction |
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage |
||||
import io.element.android.libraries.designsystem.components.BigIcon |
||||
import io.element.android.libraries.designsystem.components.ProgressDialog |
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog |
||||
import io.element.android.libraries.designsystem.components.form.textFieldState |
||||
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 |
||||
import io.element.android.libraries.designsystem.theme.components.Icon |
||||
import io.element.android.libraries.designsystem.theme.components.IconButton |
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField |
||||
import io.element.android.libraries.designsystem.theme.components.Text |
||||
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
|
||||
@Composable |
||||
fun ResetKeyPasswordView( |
||||
state: ResetKeyPasswordState, |
||||
onBack: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
val passwordState = textFieldState(stateValue = "") |
||||
FlowStepPage( |
||||
modifier = modifier, |
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()), |
||||
title = stringResource(CommonStrings.screen_reset_encryption_password_title), |
||||
subTitle = stringResource(CommonStrings.screen_reset_encryption_password_subtitle), |
||||
onBackClick = onBack, |
||||
content = { Content(textFieldState = passwordState) }, |
||||
buttons = { |
||||
Button( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
text = stringResource(CommonStrings.action_reset_identity), |
||||
onClick = { state.eventSink(ResetKeyPasswordEvent.Reset(passwordState.value)) }, |
||||
destructive = true, |
||||
) |
||||
} |
||||
) |
||||
|
||||
if (state.resetAction.isLoading() || state.resetAction.isSuccess()) { |
||||
ProgressDialog() |
||||
} else if (state.resetAction.isFailure()) { |
||||
ErrorDialog( |
||||
content = stringResource(CommonStrings.error_unknown), |
||||
onDismiss = { state.eventSink(ResetKeyPasswordEvent.DismissError) } |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun Content(textFieldState: MutableState<String>) { |
||||
var showPassword by remember { mutableStateOf(false) } |
||||
OutlinedTextField( |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.onTabOrEnterKeyFocusNext(LocalFocusManager.current), |
||||
value = textFieldState.value, |
||||
onValueChange = { text -> textFieldState.value = text }, |
||||
label = { Text(stringResource(CommonStrings.common_password)) }, |
||||
placeholder = { Text(stringResource(CommonStrings.screen_reset_encryption_password_placeholder)) }, |
||||
singleLine = true, |
||||
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), |
||||
trailingIcon = { |
||||
val image = |
||||
if (showPassword) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff() |
||||
val description = |
||||
if (showPassword) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password) |
||||
|
||||
IconButton(onClick = { showPassword = !showPassword }) { |
||||
Icon(imageVector = image, description) |
||||
} |
||||
} |
||||
) |
||||
} |
||||
|
||||
@PreviewsDayNight |
||||
@Composable |
||||
internal fun ResetKeyPasswordViewPreview() { |
||||
ElementPreview { |
||||
ResetKeyPasswordView( |
||||
state = ResetKeyPasswordState( |
||||
resetAction = AsyncAction.Uninitialized, |
||||
eventSink = {} |
||||
), |
||||
onBack = {} |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.root |
||||
|
||||
sealed interface ResetKeyRootEvent { |
||||
data object Continue : ResetKeyRootEvent |
||||
data object DismissDialog : ResetKeyRootEvent |
||||
} |
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.root |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import com.bumble.appyx.core.modality.BuildContext |
||||
import com.bumble.appyx.core.node.Node |
||||
import com.bumble.appyx.core.plugin.Plugin |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.anvilannotations.ContributesNode |
||||
import io.element.android.libraries.di.SessionScope |
||||
|
||||
@ContributesNode(SessionScope::class) |
||||
class ResetKeyRootNode @AssistedInject constructor( |
||||
@Assisted buildContext: BuildContext, |
||||
@Assisted plugins: List<Plugin>, |
||||
) : Node(buildContext, plugins = plugins) { |
||||
interface Callback : Plugin { |
||||
fun onContinue() |
||||
} |
||||
|
||||
private val presenter = ResetKeyRootPresenter() |
||||
private val callback: Callback = plugins.filterIsInstance<Callback>().first() |
||||
|
||||
@Composable |
||||
override fun View(modifier: Modifier) { |
||||
val state = presenter.present() |
||||
ResetKeyRootView( |
||||
state = state, |
||||
onContinue = callback::onContinue, |
||||
onBack = ::navigateUp, |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.root |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.setValue |
||||
import io.element.android.libraries.architecture.Presenter |
||||
|
||||
class ResetKeyRootPresenter : Presenter<ResetKeyRootState> { |
||||
@Composable |
||||
override fun present(): ResetKeyRootState { |
||||
var displayConfirmDialog by remember { mutableStateOf(false) } |
||||
|
||||
fun handleEvent(event: ResetKeyRootEvent) { |
||||
displayConfirmDialog = when (event) { |
||||
ResetKeyRootEvent.Continue -> true |
||||
ResetKeyRootEvent.DismissDialog -> false |
||||
} |
||||
} |
||||
|
||||
return ResetKeyRootState( |
||||
displayConfirmationDialog = displayConfirmDialog, |
||||
eventSink = ::handleEvent |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.root |
||||
|
||||
data class ResetKeyRootState( |
||||
val displayConfirmationDialog: Boolean, |
||||
val eventSink: (ResetKeyRootEvent) -> Unit, |
||||
) |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.root |
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
|
||||
class ResetKeyRootStateProvider : PreviewParameterProvider<ResetKeyRootState> { |
||||
override val values: Sequence<ResetKeyRootState> |
||||
get() = sequenceOf( |
||||
ResetKeyRootState( |
||||
displayConfirmationDialog = false, |
||||
eventSink = {} |
||||
), |
||||
ResetKeyRootState( |
||||
displayConfirmationDialog = true, |
||||
eventSink = {} |
||||
) |
||||
) |
||||
} |
@ -0,0 +1,149 @@
@@ -0,0 +1,149 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.securebackup.impl.reset.root |
||||
|
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
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.libraries.designsystem.atomic.organisms.InfoListItem |
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism |
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage |
||||
import io.element.android.libraries.designsystem.components.BigIcon |
||||
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 |
||||
import io.element.android.libraries.designsystem.theme.components.Icon |
||||
import io.element.android.libraries.designsystem.theme.components.Text |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
import kotlinx.collections.immutable.persistentListOf |
||||
|
||||
@Composable |
||||
fun ResetKeyRootView( |
||||
state: ResetKeyRootState, |
||||
onContinue: () -> Unit, |
||||
onBack: () -> Unit, |
||||
) { |
||||
FlowStepPage( |
||||
iconStyle = BigIcon.Style.AlertSolid, |
||||
title = stringResource(io.element.android.libraries.ui.strings.R.string.screen_encryption_reset_title), |
||||
subTitle = stringResource(io.element.android.libraries.ui.strings.R.string.screen_encryption_reset_subtitle), |
||||
isScrollable = true, |
||||
content = { Content() }, |
||||
buttons = { |
||||
Button( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
text = stringResource(id = CommonStrings.action_continue), |
||||
onClick = { state.eventSink(ResetKeyRootEvent.Continue) }, |
||||
destructive = true, |
||||
) |
||||
}, |
||||
onBackClick = onBack, |
||||
) |
||||
|
||||
if (state.displayConfirmationDialog) { |
||||
ConfirmationDialog( |
||||
title = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_title), |
||||
content = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_subtitle), |
||||
submitText = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_action), |
||||
onSubmitClick = { |
||||
state.eventSink(ResetKeyRootEvent.DismissDialog) |
||||
onContinue() |
||||
}, |
||||
destructiveSubmit = true, |
||||
onDismiss = { state.eventSink(ResetKeyRootEvent.DismissDialog) } |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun Content() { |
||||
Column( |
||||
modifier = Modifier.padding(top = 8.dp, bottom = 40.dp), |
||||
verticalArrangement = Arrangement.spacedBy(24.dp), |
||||
) { |
||||
InfoListOrganism( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
items = persistentListOf( |
||||
InfoListItem( |
||||
message = stringResource(CommonStrings.screen_encryption_reset_bullet_1), |
||||
iconComposable = { |
||||
Icon( |
||||
modifier = Modifier.size(20.dp), |
||||
imageVector = CompoundIcons.Check(), |
||||
contentDescription = null, |
||||
tint = ElementTheme.colors.iconSuccessPrimary, |
||||
) |
||||
}, |
||||
), |
||||
InfoListItem( |
||||
message = stringResource(CommonStrings.screen_encryption_reset_bullet_2), |
||||
iconComposable = { |
||||
Icon( |
||||
modifier = Modifier.size(20.dp), |
||||
imageVector = CompoundIcons.Close(), |
||||
contentDescription = null, |
||||
tint = ElementTheme.colors.iconCriticalPrimary, |
||||
) |
||||
}, |
||||
), |
||||
InfoListItem( |
||||
message = stringResource(CommonStrings.screen_encryption_reset_bullet_3), |
||||
iconComposable = { |
||||
Icon( |
||||
modifier = Modifier.size(20.dp), |
||||
imageVector = CompoundIcons.Close(), |
||||
contentDescription = null, |
||||
tint = ElementTheme.colors.iconCriticalPrimary, |
||||
) |
||||
}, |
||||
), |
||||
), |
||||
backgroundColor = ElementTheme.colors.bgActionSecondaryHovered, |
||||
) |
||||
|
||||
Text( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
text = stringResource(CommonStrings.screen_encryption_reset_footer), |
||||
style = ElementTheme.typography.fontBodyMdMedium, |
||||
color = ElementTheme.colors.textActionPrimary, |
||||
textAlign = TextAlign.Center, |
||||
) |
||||
} |
||||
} |
||||
|
||||
@PreviewsDayNight |
||||
@Composable |
||||
internal fun ResetKeyRootViewPreview(@PreviewParameter(ResetKeyRootStateProvider::class) state: ResetKeyRootState) { |
||||
ElementPreview { |
||||
ResetKeyRootView( |
||||
state = state, |
||||
onContinue = {}, |
||||
onBack = {}, |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.libraries.matrix.impl.encryption |
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle |
||||
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle |
||||
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle |
||||
import org.matrix.rustcomponents.sdk.AuthData |
||||
import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails |
||||
import org.matrix.rustcomponents.sdk.CrossSigningResetAuthType |
||||
|
||||
object RustIdentityResetHandleFactory { |
||||
fun create(identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle): Result<IdentityResetHandle> { |
||||
return runCatching { |
||||
when (val authType = identityResetHandle.authType()) { |
||||
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl) |
||||
// User interactive authentication (user + password) |
||||
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(identityResetHandle) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
class RustPasswordIdentityResetHandle( |
||||
private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle, |
||||
) : IdentityPasswordResetHandle { |
||||
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> { |
||||
return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) } |
||||
} |
||||
} |
||||
|
||||
class RustOidcIdentityResetHandle( |
||||
private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle, |
||||
override val url: String, |
||||
) : IdentityOidcResetHandle { |
||||
override suspend fun resetOidc(): Result<Unit> { |
||||
return runCatching { identityResetHandle.reset(null) } |
||||
} |
||||
} |
Loading…
Reference in new issue