Skip to content

Commit 4ab0b10

Browse files
committed
Initial implementation of the reset identity feature
1 parent 45775d7 commit 4ab0b10

File tree

23 files changed

+1003
-68
lines changed

23 files changed

+1003
-68
lines changed

features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
5858

5959
@Parcelize
6060
data object EnterRecoveryKey : NavTarget
61+
62+
@Parcelize
63+
data object ResetIdentity : NavTarget
6164
}
6265

6366
interface Callback : Plugin {
@@ -85,6 +88,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
8588
override fun onDone() {
8689
plugins<Callback>().forEach { it.onDone() }
8790
}
91+
92+
override fun onResetKey() {
93+
backstack.push(NavTarget.ResetIdentity)
94+
}
8895
})
8996
.build()
9097
}
@@ -94,6 +101,16 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
94101
.callback(secureBackupEntryPointCallback)
95102
.build()
96103
}
104+
is NavTarget.ResetIdentity -> {
105+
secureBackupEntryPoint.nodeBuilder(this, buildContext)
106+
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.ResetIdentity))
107+
.callback(object : SecureBackupEntryPoint.Callback {
108+
override fun onDone() {
109+
plugins<Callback>().forEach { it.onDone() }
110+
}
111+
})
112+
.build()
113+
}
97114
}
98115
}
99116

features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ interface SecureBackupEntryPoint : FeatureEntryPoint {
3434

3535
@Parcelize
3636
data object CreateNewRecoveryKey : InitialTarget
37+
38+
@Parcelize
39+
data object ResetIdentity : InitialTarget
3740
}
3841

3942
data class Params(val initialElement: InitialTarget) : NodeInputs

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import io.element.android.features.securebackup.impl.createkey.CreateNewRecovery
3434
import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode
3535
import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode
3636
import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode
37+
import io.element.android.features.securebackup.impl.reset.ResetIdentityFlowNode
3738
import io.element.android.features.securebackup.impl.root.SecureBackupRootNode
3839
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode
3940
import io.element.android.libraries.architecture.BackstackView
@@ -48,10 +49,11 @@ class SecureBackupFlowNode @AssistedInject constructor(
4849
@Assisted plugins: List<Plugin>,
4950
) : BaseFlowNode<SecureBackupFlowNode.NavTarget>(
5051
backstack = BackStack(
51-
initialElement = when (plugins.filterIsInstance(SecureBackupEntryPoint.Params::class.java).first().initialElement) {
52+
initialElement = when (plugins.filterIsInstance<SecureBackupEntryPoint.Params>().first().initialElement) {
5253
SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root
5354
SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey
5455
SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey -> NavTarget.CreateNewRecoveryKey
56+
is SecureBackupEntryPoint.InitialTarget.ResetIdentity -> NavTarget.ResetIdentity
5557
},
5658
savedStateMap = buildContext.savedStateMap,
5759
),
@@ -79,6 +81,9 @@ class SecureBackupFlowNode @AssistedInject constructor(
7981

8082
@Parcelize
8183
data object CreateNewRecoveryKey : NavTarget
84+
85+
@Parcelize
86+
data object ResetIdentity : NavTarget
8287
}
8388

8489
private val callbacks = plugins<SecureBackupEntryPoint.Callback>()
@@ -146,6 +151,14 @@ class SecureBackupFlowNode @AssistedInject constructor(
146151
NavTarget.CreateNewRecoveryKey -> {
147152
createNode<CreateNewRecoveryKeyNode>(buildContext)
148153
}
154+
is NavTarget.ResetIdentity -> {
155+
val callback = object : ResetIdentityFlowNode.Callback {
156+
override fun onDone() {
157+
callbacks.forEach { it.onDone() }
158+
}
159+
}
160+
createNode<ResetIdentityFlowNode>(buildContext, listOf(callback))
161+
}
149162
}
150163
}
151164

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.securebackup.impl.reset
18+
19+
import io.element.android.libraries.architecture.AsyncData
20+
import io.element.android.libraries.di.SessionScope
21+
import io.element.android.libraries.di.SingleIn
22+
import io.element.android.libraries.di.annotations.SessionCoroutineScope
23+
import io.element.android.libraries.matrix.api.MatrixClient
24+
import io.element.android.libraries.matrix.api.core.SessionId
25+
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
26+
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
27+
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
28+
import kotlinx.coroutines.CoroutineScope
29+
import kotlinx.coroutines.flow.MutableStateFlow
30+
import kotlinx.coroutines.flow.StateFlow
31+
import kotlinx.coroutines.flow.distinctUntilChanged
32+
import kotlinx.coroutines.flow.filter
33+
import kotlinx.coroutines.flow.filterIsInstance
34+
import kotlinx.coroutines.flow.first
35+
import kotlinx.coroutines.flow.map
36+
import kotlinx.coroutines.launch
37+
import javax.inject.Inject
38+
39+
class ResetIdentityFlowManager @Inject constructor(
40+
private val matrixClient: MatrixClient,
41+
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
42+
private val sessionVerificationService: SessionVerificationService,
43+
) {
44+
private val resetHandleFlow: MutableStateFlow<AsyncData<IdentityResetHandle>> = MutableStateFlow(AsyncData.Uninitialized)
45+
val currentHandleFlow: StateFlow<AsyncData<IdentityResetHandle>> = resetHandleFlow
46+
47+
fun whenResetIsDone(block: () -> Unit) {
48+
sessionCoroutineScope.launch {
49+
sessionVerificationService.sessionVerifiedStatus.filterIsInstance<SessionVerifiedStatus.Verified>().first()
50+
block()
51+
}
52+
}
53+
54+
fun currentSessionId(): SessionId {
55+
return matrixClient.sessionId
56+
}
57+
58+
fun getResetHandle(): StateFlow<AsyncData<IdentityResetHandle>> {
59+
return if (resetHandleFlow.value.isLoading() || resetHandleFlow.value.isSuccess()) {
60+
resetHandleFlow
61+
} else {
62+
resetHandleFlow.value = AsyncData.Loading()
63+
64+
sessionCoroutineScope.launch {
65+
matrixClient.encryptionService().startIdentityReset()
66+
.onSuccess { handle ->
67+
resetHandleFlow.value = if (handle != null) {
68+
AsyncData.Success(handle)
69+
} else {
70+
AsyncData.Failure(IllegalStateException("Could not get a reset identity handle"))
71+
}
72+
}
73+
.onFailure { resetHandleFlow.value = AsyncData.Failure(it) }
74+
}
75+
76+
resetHandleFlow
77+
}
78+
}
79+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.securebackup.impl.reset
18+
19+
import android.app.Activity
20+
import android.os.Parcelable
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.platform.LocalContext
24+
import com.bumble.appyx.core.modality.BuildContext
25+
import com.bumble.appyx.core.node.Node
26+
import com.bumble.appyx.core.plugin.Plugin
27+
import com.bumble.appyx.core.plugin.plugins
28+
import com.bumble.appyx.navmodel.backstack.BackStack
29+
import com.bumble.appyx.navmodel.backstack.operation.push
30+
import dagger.assisted.Assisted
31+
import dagger.assisted.AssistedInject
32+
import io.element.android.anvilannotations.ContributesNode
33+
import io.element.android.features.securebackup.impl.reset.password.ResetKeyPasswordNode
34+
import io.element.android.features.securebackup.impl.reset.root.ResetKeyRootNode
35+
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
36+
import io.element.android.libraries.architecture.AsyncData
37+
import io.element.android.libraries.architecture.BackstackView
38+
import io.element.android.libraries.architecture.BaseFlowNode
39+
import io.element.android.libraries.architecture.createNode
40+
import io.element.android.libraries.di.SessionScope
41+
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle
42+
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
43+
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
44+
import kotlinx.coroutines.CoroutineScope
45+
import kotlinx.coroutines.flow.filterIsInstance
46+
import kotlinx.coroutines.flow.first
47+
import kotlinx.coroutines.launch
48+
import kotlinx.parcelize.Parcelize
49+
50+
@ContributesNode(SessionScope::class)
51+
class ResetIdentityFlowNode @AssistedInject constructor(
52+
@Assisted buildContext: BuildContext,
53+
@Assisted plugins: List<Plugin>,
54+
private val resetIdentityFlowManager: ResetIdentityFlowManager,
55+
private val coroutineScope: CoroutineScope,
56+
) : BaseFlowNode<ResetIdentityFlowNode.NavTarget>(
57+
backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap),
58+
buildContext = buildContext,
59+
plugins = plugins,
60+
) {
61+
interface Callback: Plugin {
62+
fun onDone()
63+
}
64+
65+
sealed interface NavTarget : Parcelable {
66+
@Parcelize
67+
data object Root : NavTarget
68+
69+
@Parcelize
70+
data object ResetPassword : NavTarget
71+
72+
// @Parcelize
73+
// data class ResetOidc(val url: String) : NavTarget
74+
}
75+
76+
private lateinit var activity: Activity
77+
78+
override fun onBuilt() {
79+
super.onBuilt()
80+
81+
resetIdentityFlowManager.whenResetIsDone {
82+
plugins<Callback>().forEach { it.onDone() }
83+
}
84+
}
85+
86+
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
87+
return when (navTarget) {
88+
is NavTarget.Root -> {
89+
val callback = object : ResetKeyRootNode.Callback {
90+
override fun onContinue() {
91+
coroutineScope.startReset()
92+
}
93+
}
94+
createNode<ResetKeyRootNode>(buildContext, listOf(callback))
95+
}
96+
is NavTarget.ResetPassword -> {
97+
val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found")
98+
createNode<ResetKeyPasswordNode>(
99+
buildContext,
100+
listOf(ResetKeyPasswordNode.Inputs(resetIdentityFlowManager.currentSessionId(), handle))
101+
)
102+
}
103+
}
104+
}
105+
106+
private fun CoroutineScope.startReset() = launch {
107+
val handle = resetIdentityFlowManager.getResetHandle()
108+
.filterIsInstance<AsyncData.Success<IdentityResetHandle>>()
109+
.first()
110+
.data
111+
112+
when (handle) {
113+
is IdentityOidcResetHandle -> {
114+
activity.openUrlInChromeCustomTab(null, false, handle.url)
115+
handle.resetOidc()
116+
}
117+
is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword)
118+
}
119+
}
120+
121+
@Composable
122+
override fun View(modifier: Modifier) {
123+
(LocalContext.current as? Activity)?.let { activity = it }
124+
125+
BackstackView(modifier)
126+
}
127+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.securebackup.impl.reset.password
18+
19+
sealed interface ResetKeyPasswordEvent {
20+
data class Reset(val password: String) : ResetKeyPasswordEvent
21+
data object DismissError : ResetKeyPasswordEvent
22+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.securebackup.impl.reset.password
18+
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.Modifier
21+
import com.bumble.appyx.core.modality.BuildContext
22+
import com.bumble.appyx.core.node.Node
23+
import com.bumble.appyx.core.plugin.Plugin
24+
import com.bumble.appyx.core.plugin.plugins
25+
import dagger.assisted.Assisted
26+
import dagger.assisted.AssistedInject
27+
import io.element.android.anvilannotations.ContributesNode
28+
import io.element.android.libraries.architecture.NodeInputs
29+
import io.element.android.libraries.architecture.inputs
30+
import io.element.android.libraries.di.SessionScope
31+
import io.element.android.libraries.matrix.api.core.UserId
32+
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
33+
34+
@ContributesNode(SessionScope::class)
35+
class ResetKeyPasswordNode @AssistedInject constructor(
36+
@Assisted buildContext: BuildContext,
37+
@Assisted plugins: List<Plugin>,
38+
) : Node(buildContext, plugins = plugins) {
39+
40+
data class Inputs(val userId: UserId, val handle: IdentityPasswordResetHandle) : NodeInputs
41+
42+
private val presenter by lazy {
43+
val inputs = inputs<Inputs>()
44+
ResetKeyPasswordPresenter(inputs.userId, inputs.handle)
45+
}
46+
47+
@Composable
48+
override fun View(modifier: Modifier) {
49+
val state = presenter.present()
50+
ResetKeyPasswordView(
51+
state = state,
52+
onBack = ::navigateUp
53+
)
54+
}
55+
}

0 commit comments

Comments
 (0)