Improve APIs, add tests

This commit is contained in:
Jorge Martín 2024-08-12 12:22:36 +02:00
parent 175bada0df
commit 7fd0ad09dc
22 changed files with 562 additions and 76 deletions

View file

@ -19,7 +19,6 @@ package io.element.android.features.securebackup.impl.reset
import io.element.android.libraries.architecture.AsyncData
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
@ -46,10 +45,6 @@ class ResetIdentityFlowManager @Inject constructor(
}
}
fun currentSessionId(): SessionId {
return matrixClient.sessionId
}
fun getResetHandle(): StateFlow<AsyncData<IdentityResetHandle>> {
return if (resetHandleFlow.value.isLoading() || resetHandleFlow.value.isSuccess()) {
resetHandleFlow

View file

@ -34,8 +34,8 @@ 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.features.securebackup.impl.reset.password.ResetIdentityPasswordNode
import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.BackstackView
@ -108,18 +108,18 @@ class ResetIdentityFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.Root -> {
val callback = object : ResetKeyRootNode.Callback {
val callback = object : ResetIdentityRootNode.Callback {
override fun onContinue() {
coroutineScope.startReset()
}
}
createNode<ResetKeyRootNode>(buildContext, listOf(callback))
createNode<ResetIdentityRootNode>(buildContext, listOf(callback))
}
is NavTarget.ResetPassword -> {
val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found")
createNode<ResetKeyPasswordNode>(
createNode<ResetIdentityPasswordNode>(
buildContext,
listOf(ResetKeyPasswordNode.Inputs(resetIdentityFlowManager.currentSessionId(), handle))
listOf(ResetIdentityPasswordNode.Inputs(handle))
)
}
is NavTarget.ResetOidc -> {

View file

@ -16,7 +16,7 @@
package io.element.android.features.securebackup.impl.reset.password
sealed interface ResetKeyPasswordEvent {
data class Reset(val password: String) : ResetKeyPasswordEvent
data object DismissError : ResetKeyPasswordEvent
sealed interface ResetIdentityPasswordEvent {
data class Reset(val password: String) : ResetIdentityPasswordEvent
data object DismissError : ResetIdentityPasswordEvent
}

View file

@ -21,33 +21,33 @@ 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.core.coroutine.CoroutineDispatchers
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(
class ResetIdentityPasswordNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val coroutineDispatchers: CoroutineDispatchers,
) : Node(buildContext, plugins = plugins) {
data class Inputs(val userId: UserId, val handle: IdentityPasswordResetHandle) : NodeInputs
data class Inputs(val handle: IdentityPasswordResetHandle) : NodeInputs
private val presenter by lazy {
val inputs = inputs<Inputs>()
ResetKeyPasswordPresenter(inputs.userId, inputs.handle)
ResetIdentityPasswordPresenter(inputs.handle, dispatchers = coroutineDispatchers)
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
ResetKeyPasswordView(
ResetIdentityPasswordView(
state = state,
onBack = ::navigateUp
)

View file

@ -24,37 +24,37 @@ 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.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class ResetKeyPasswordPresenter(
private val userId: UserId,
class ResetIdentityPasswordPresenter(
private val identityPasswordResetHandle: IdentityPasswordResetHandle,
) : Presenter<ResetKeyPasswordState> {
private val dispatchers: CoroutineDispatchers,
) : Presenter<ResetIdentityPasswordState> {
@Composable
override fun present(): ResetKeyPasswordState {
override fun present(): ResetIdentityPasswordState {
val coroutineScope = rememberCoroutineScope()
val resetAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
fun handleEvent(event: ResetKeyPasswordEvent) {
fun handleEvent(event: ResetIdentityPasswordEvent) {
when (event) {
is ResetKeyPasswordEvent.Reset -> coroutineScope.reset(userId, event.password, resetAction)
ResetKeyPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized
is ResetIdentityPasswordEvent.Reset -> coroutineScope.reset(event.password, resetAction)
ResetIdentityPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized
}
}
return ResetKeyPasswordState(
return ResetIdentityPasswordState(
resetAction = resetAction.value,
eventSink = ::handleEvent
)
}
private fun CoroutineScope.reset(userId: UserId, password: String, action: MutableState<AsyncAction<Unit>>) = launch {
private fun CoroutineScope.reset(password: String, action: MutableState<AsyncAction<Unit>>) = launch(dispatchers.io) {
suspend {
identityPasswordResetHandle.resetPassword(userId, password).getOrThrow()
identityPasswordResetHandle.resetPassword(password).getOrThrow()
}.runCatchingUpdatingState(action)
}
}

View file

@ -18,7 +18,7 @@ package io.element.android.features.securebackup.impl.reset.password
import io.element.android.libraries.architecture.AsyncAction
data class ResetKeyPasswordState(
data class ResetIdentityPasswordState(
val resetAction: AsyncAction<Unit>,
val eventSink: (ResetKeyPasswordEvent) -> Unit,
val eventSink: (ResetIdentityPasswordEvent) -> Unit,
)

View file

@ -46,8 +46,8 @@ import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKe
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun ResetKeyPasswordView(
state: ResetKeyPasswordState,
fun ResetIdentityPasswordView(
state: ResetIdentityPasswordState,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -63,7 +63,7 @@ fun ResetKeyPasswordView(
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_reset_identity),
onClick = { state.eventSink(ResetKeyPasswordEvent.Reset(passwordState.value)) },
onClick = { state.eventSink(ResetIdentityPasswordEvent.Reset(passwordState.value)) },
destructive = true,
)
}
@ -74,7 +74,7 @@ fun ResetKeyPasswordView(
} else if (state.resetAction.isFailure()) {
ErrorDialog(
content = stringResource(CommonStrings.error_unknown),
onDismiss = { state.eventSink(ResetKeyPasswordEvent.DismissError) }
onDismiss = { state.eventSink(ResetIdentityPasswordEvent.DismissError) }
)
}
}
@ -107,10 +107,10 @@ private fun Content(textFieldState: MutableState<String>) {
@PreviewsDayNight
@Composable
internal fun ResetKeyPasswordViewPreview() {
internal fun ResetIdentityPasswordViewPreview() {
ElementPreview {
ResetKeyPasswordView(
state = ResetKeyPasswordState(
ResetIdentityPasswordView(
state = ResetIdentityPasswordState(
resetAction = AsyncAction.Uninitialized,
eventSink = {}
),

View file

@ -16,7 +16,7 @@
package io.element.android.features.securebackup.impl.reset.root
sealed interface ResetKeyRootEvent {
data object Continue : ResetKeyRootEvent
data object DismissDialog : ResetKeyRootEvent
sealed interface ResetIdentityRootEvent {
data object Continue : ResetIdentityRootEvent
data object DismissDialog : ResetIdentityRootEvent
}

View file

@ -27,7 +27,7 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class ResetKeyRootNode @AssistedInject constructor(
class ResetIdentityRootNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : Node(buildContext, plugins = plugins) {
@ -35,13 +35,13 @@ class ResetKeyRootNode @AssistedInject constructor(
fun onContinue()
}
private val presenter = ResetKeyRootPresenter()
private val presenter = ResetIdentityRootPresenter()
private val callback: Callback = plugins.filterIsInstance<Callback>().first()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
ResetKeyRootView(
ResetIdentityRootView(
state = state,
onContinue = callback::onContinue,
onBack = ::navigateUp,

View file

@ -23,19 +23,19 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Presenter
class ResetKeyRootPresenter : Presenter<ResetKeyRootState> {
class ResetIdentityRootPresenter : Presenter<ResetIdentityRootState> {
@Composable
override fun present(): ResetKeyRootState {
override fun present(): ResetIdentityRootState {
var displayConfirmDialog by remember { mutableStateOf(false) }
fun handleEvent(event: ResetKeyRootEvent) {
fun handleEvent(event: ResetIdentityRootEvent) {
displayConfirmDialog = when (event) {
ResetKeyRootEvent.Continue -> true
ResetKeyRootEvent.DismissDialog -> false
ResetIdentityRootEvent.Continue -> true
ResetIdentityRootEvent.DismissDialog -> false
}
}
return ResetKeyRootState(
return ResetIdentityRootState(
displayConfirmationDialog = displayConfirmDialog,
eventSink = ::handleEvent
)

View file

@ -16,7 +16,7 @@
package io.element.android.features.securebackup.impl.reset.root
data class ResetKeyRootState(
data class ResetIdentityRootState(
val displayConfirmationDialog: Boolean,
val eventSink: (ResetKeyRootEvent) -> Unit,
val eventSink: (ResetIdentityRootEvent) -> Unit,
)

View file

@ -18,14 +18,14 @@ 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>
class ResetIdentityRootStateProvider : PreviewParameterProvider<ResetIdentityRootState> {
override val values: Sequence<ResetIdentityRootState>
get() = sequenceOf(
ResetKeyRootState(
ResetIdentityRootState(
displayConfirmationDialog = false,
eventSink = {}
),
ResetKeyRootState(
ResetIdentityRootState(
displayConfirmationDialog = true,
eventSink = {}
)

View file

@ -43,8 +43,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf
@Composable
fun ResetKeyRootView(
state: ResetKeyRootState,
fun ResetIdentityRootView(
state: ResetIdentityRootState,
onContinue: () -> Unit,
onBack: () -> Unit,
) {
@ -58,7 +58,7 @@ fun ResetKeyRootView(
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = CommonStrings.action_continue),
onClick = { state.eventSink(ResetKeyRootEvent.Continue) },
onClick = { state.eventSink(ResetIdentityRootEvent.Continue) },
destructive = true,
)
},
@ -71,11 +71,11 @@ fun ResetKeyRootView(
content = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_subtitle),
submitText = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_action),
onSubmitClick = {
state.eventSink(ResetKeyRootEvent.DismissDialog)
state.eventSink(ResetIdentityRootEvent.DismissDialog)
onContinue()
},
destructiveSubmit = true,
onDismiss = { state.eventSink(ResetKeyRootEvent.DismissDialog) }
onDismiss = { state.eventSink(ResetIdentityRootEvent.DismissDialog) }
)
}
}
@ -138,9 +138,9 @@ private fun Content() {
@PreviewsDayNight
@Composable
internal fun ResetKeyRootViewPreview(@PreviewParameter(ResetKeyRootStateProvider::class) state: ResetKeyRootState) {
internal fun ResetIdentityRootViewPreview(@PreviewParameter(ResetIdentityRootStateProvider::class) state: ResetIdentityRootState) {
ElementPreview {
ResetKeyRootView(
ResetIdentityRootView(
state = state,
onContinue = {},
onBack = {},