mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
[Link new device] Handle split steps in the SDK
This commit is contained in:
+15
@@ -54,8 +54,10 @@ import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
|
|||||||
import io.element.android.libraries.matrix.api.logs.LoggerTags
|
import io.element.android.libraries.matrix.api.logs.LoggerTags
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -102,6 +104,7 @@ class LinkNewDeviceFlowNode(
|
|||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
linkDesktopHandlerJob = observeLinkNewDesktopHandler()
|
linkDesktopHandlerJob = observeLinkNewDesktopHandler()
|
||||||
},
|
},
|
||||||
|
onResume = ::onResume,
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
linkMobileHandlerJob?.cancel()
|
linkMobileHandlerJob?.cancel()
|
||||||
linkDesktopHandlerJob?.cancel()
|
linkDesktopHandlerJob?.cancel()
|
||||||
@@ -209,6 +212,18 @@ class LinkNewDeviceFlowNode(
|
|||||||
.launchIn(sessionCoroutineScope)
|
.launchIn(sessionCoroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onResume() = sessionCoroutineScope.launch {
|
||||||
|
// Application is resumed, if the step is waiting for auth, send the confirmation
|
||||||
|
(linkNewMobileHandler.stepFlow.first() as? LinkMobileStep.WaitingForAuth)?.let {
|
||||||
|
Timber.tag(tag.value).d("Resuming while waiting for auth on mobile, sending confirmation")
|
||||||
|
it.continuationMessageSender.confirm()
|
||||||
|
}
|
||||||
|
(linkNewDesktopHandler.stepFlow.value as? LinkDesktopStep.WaitingForAuth)?.let {
|
||||||
|
Timber.tag(tag.value).d("Resuming while waiting for auth on desktop, sending confirmation")
|
||||||
|
it.continuationMessageSender.confirm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun navigateToError(errorType: ErrorType) {
|
private fun navigateToError(errorType: ErrorType) {
|
||||||
// Map the error to an error screen
|
// Map the error to an error screen
|
||||||
val error = when (errorType) {
|
val error = when (errorType) {
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.api.linknewdevice
|
||||||
|
|
||||||
|
interface ContinuationMessageSender {
|
||||||
|
suspend fun cancel(): Result<Unit>
|
||||||
|
suspend fun confirm(): Result<Unit>
|
||||||
|
}
|
||||||
+2
@@ -18,8 +18,10 @@ interface LinkDesktopHandler {
|
|||||||
sealed interface LinkDesktopStep {
|
sealed interface LinkDesktopStep {
|
||||||
data object Uninitialized : LinkDesktopStep
|
data object Uninitialized : LinkDesktopStep
|
||||||
data object Starting : LinkDesktopStep
|
data object Starting : LinkDesktopStep
|
||||||
|
|
||||||
data class WaitingForAuth(
|
data class WaitingForAuth(
|
||||||
val verificationUri: String,
|
val verificationUri: String,
|
||||||
|
val continuationMessageSender: ContinuationMessageSender,
|
||||||
) : LinkDesktopStep
|
) : LinkDesktopStep
|
||||||
|
|
||||||
data class EstablishingSecureChannel(
|
data class EstablishingSecureChannel(
|
||||||
|
|||||||
+6
-1
@@ -22,7 +22,12 @@ sealed interface LinkMobileStep {
|
|||||||
data object Starting : LinkMobileStep
|
data object Starting : LinkMobileStep
|
||||||
data class QrReady(val data: String) : LinkMobileStep
|
data class QrReady(val data: String) : LinkMobileStep
|
||||||
data object QrRotating : LinkMobileStep
|
data object QrRotating : LinkMobileStep
|
||||||
data class WaitingForAuth(val verificationUri: String) : LinkMobileStep
|
|
||||||
|
data class WaitingForAuth(
|
||||||
|
val verificationUri: String,
|
||||||
|
val continuationMessageSender: ContinuationMessageSender,
|
||||||
|
) : LinkMobileStep
|
||||||
|
|
||||||
data class QrScanned(val checkCodeSender: CheckCodeSender) : LinkMobileStep
|
data class QrScanned(val checkCodeSender: CheckCodeSender) : LinkMobileStep
|
||||||
data class Error(val errorType: ErrorType) : LinkMobileStep
|
data class Error(val errorType: ErrorType) : LinkMobileStep
|
||||||
data object SyncingSecrets : LinkMobileStep
|
data object SyncingSecrets : LinkMobileStep
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.linknewdevice
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
|
import io.element.android.libraries.matrix.api.linknewdevice.ContinuationMessageSender
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.rustcomponents.sdk.ContinuationMessageSender as FfiContinuationMessageSender
|
||||||
|
|
||||||
|
class RustContinuationMessageSender(
|
||||||
|
private val inner: FfiContinuationMessageSender,
|
||||||
|
private val sessionDispatcher: CoroutineDispatcher,
|
||||||
|
) : ContinuationMessageSender {
|
||||||
|
override suspend fun cancel(): Result<Unit> = withContext(sessionDispatcher) {
|
||||||
|
runCatchingExceptions {
|
||||||
|
inner.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun confirm(): Result<Unit> = withContext(sessionDispatcher) {
|
||||||
|
runCatchingExceptions {
|
||||||
|
inner.confirm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -75,6 +75,10 @@ class RustLinkDesktopHandler(
|
|||||||
GrantQrLoginProgress.SyncingSecrets -> LinkDesktopStep.SyncingSecrets
|
GrantQrLoginProgress.SyncingSecrets -> LinkDesktopStep.SyncingSecrets
|
||||||
is GrantQrLoginProgress.WaitingForAuth -> LinkDesktopStep.WaitingForAuth(
|
is GrantQrLoginProgress.WaitingForAuth -> LinkDesktopStep.WaitingForAuth(
|
||||||
verificationUri = verificationUri,
|
verificationUri = verificationUri,
|
||||||
|
continuationMessageSender = RustContinuationMessageSender(
|
||||||
|
inner = continuationSender,
|
||||||
|
sessionDispatcher = sessionDispatcher,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
is GrantQrLoginProgress.EstablishingSecureChannel -> LinkDesktopStep.EstablishingSecureChannel(
|
is GrantQrLoginProgress.EstablishingSecureChannel -> LinkDesktopStep.EstablishingSecureChannel(
|
||||||
checkCode = checkCode,
|
checkCode = checkCode,
|
||||||
|
|||||||
+7
-1
@@ -84,7 +84,13 @@ class RustLinkMobileHandler(
|
|||||||
)
|
)
|
||||||
GrantGeneratedQrLoginProgress.Starting -> LinkMobileStep.Starting
|
GrantGeneratedQrLoginProgress.Starting -> LinkMobileStep.Starting
|
||||||
GrantGeneratedQrLoginProgress.SyncingSecrets -> LinkMobileStep.SyncingSecrets
|
GrantGeneratedQrLoginProgress.SyncingSecrets -> LinkMobileStep.SyncingSecrets
|
||||||
is GrantGeneratedQrLoginProgress.WaitingForAuth -> LinkMobileStep.WaitingForAuth(verificationUri)
|
is GrantGeneratedQrLoginProgress.WaitingForAuth -> LinkMobileStep.WaitingForAuth(
|
||||||
|
verificationUri = verificationUri,
|
||||||
|
continuationMessageSender = RustContinuationMessageSender(
|
||||||
|
inner = continuationSender,
|
||||||
|
sessionDispatcher = sessionDispatcher,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.linknewdevice
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.linknewdevice.ContinuationMessageSender
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
|
class FakeContinuationMessageSender(
|
||||||
|
private val cancelResult: () -> Result<Unit> = { lambdaError() },
|
||||||
|
private val confirmResult: () -> Result<Unit> = { lambdaError() },
|
||||||
|
) : ContinuationMessageSender {
|
||||||
|
override suspend fun cancel(): Result<Unit> = simulateLongTask {
|
||||||
|
cancelResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun confirm(): Result<Unit> = simulateLongTask {
|
||||||
|
confirmResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-7
@@ -16,6 +16,8 @@ import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep
|
|||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiContinuationMessageSender
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiContinuationMessageSender
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
|
||||||
import io.element.android.libraries.matrix.test.QR_CODE_DATA
|
import io.element.android.libraries.matrix.test.QR_CODE_DATA
|
||||||
|
import io.element.android.tests.testutils.ExpectedResult
|
||||||
|
import io.element.android.tests.testutils.match
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -50,16 +52,18 @@ class RustLinkDesktopHandlerTest {
|
|||||||
runCurrent()
|
runCurrent()
|
||||||
// progress from the handler is mapped and emitted
|
// progress from the handler is mapped and emitted
|
||||||
listOf(
|
listOf(
|
||||||
GrantQrLoginProgress.Starting to LinkDesktopStep.Starting,
|
GrantQrLoginProgress.Starting to ExpectedResult(LinkDesktopStep.Starting),
|
||||||
GrantQrLoginProgress.SyncingSecrets to LinkDesktopStep.SyncingSecrets,
|
GrantQrLoginProgress.SyncingSecrets to ExpectedResult(LinkDesktopStep.SyncingSecrets),
|
||||||
GrantQrLoginProgress.WaitingForAuth("aVerificationUri", FakeFfiContinuationMessageSender())
|
GrantQrLoginProgress.WaitingForAuth("aVerificationUri", FakeFfiContinuationMessageSender())
|
||||||
to LinkDesktopStep.WaitingForAuth("aVerificationUri"),
|
to ExpectedResult(
|
||||||
|
resultClass = LinkDesktopStep.WaitingForAuth::class.java,
|
||||||
|
),
|
||||||
GrantQrLoginProgress.EstablishingSecureChannel(1.toUByte(), "1")
|
GrantQrLoginProgress.EstablishingSecureChannel(1.toUByte(), "1")
|
||||||
to LinkDesktopStep.EstablishingSecureChannel(1.toUByte(), "1"),
|
to ExpectedResult(LinkDesktopStep.EstablishingSecureChannel(1.toUByte(), "1")),
|
||||||
GrantQrLoginProgress.Done to LinkDesktopStep.Done,
|
GrantQrLoginProgress.Done to ExpectedResult(LinkDesktopStep.Done),
|
||||||
).forEach { (progress, expectedStep) ->
|
).forEach { (progress, expectedResult) ->
|
||||||
handler.emitScanProgress(progress)
|
handler.emitScanProgress(progress)
|
||||||
assertThat(awaitItem()).isEqualTo(expectedStep)
|
assertThat(awaitItem()).match(expectedResult)
|
||||||
}
|
}
|
||||||
// scan returns, no new event is emitted
|
// scan returns, no new event is emitted
|
||||||
completable.complete(Unit)
|
completable.complete(Unit)
|
||||||
|
|||||||
+10
-8
@@ -18,6 +18,8 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiContinuati
|
|||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData
|
||||||
import io.element.android.libraries.matrix.test.QR_CODE_DATA_RECIPROCATE
|
import io.element.android.libraries.matrix.test.QR_CODE_DATA_RECIPROCATE
|
||||||
|
import io.element.android.tests.testutils.ExpectedResult
|
||||||
|
import io.element.android.tests.testutils.match
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -51,18 +53,18 @@ class RustLinkMobileHandlerTest {
|
|||||||
runCurrent()
|
runCurrent()
|
||||||
// progress from the handler is mapped and emitted
|
// progress from the handler is mapped and emitted
|
||||||
listOf(
|
listOf(
|
||||||
GrantGeneratedQrLoginProgress.Starting to LinkMobileStep.Starting::class.java,
|
GrantGeneratedQrLoginProgress.Starting to ExpectedResult(LinkMobileStep.Starting),
|
||||||
GrantGeneratedQrLoginProgress.SyncingSecrets to LinkMobileStep.SyncingSecrets::class.java,
|
GrantGeneratedQrLoginProgress.SyncingSecrets to ExpectedResult(LinkMobileStep.SyncingSecrets),
|
||||||
GrantGeneratedQrLoginProgress.WaitingForAuth("aVerificationUri", FakeFfiContinuationMessageSender())
|
GrantGeneratedQrLoginProgress.WaitingForAuth("aVerificationUri", FakeFfiContinuationMessageSender())
|
||||||
to LinkMobileStep.WaitingForAuth::class.java,
|
to ExpectedResult(LinkMobileStep.WaitingForAuth::class.java),
|
||||||
GrantGeneratedQrLoginProgress.QrScanned(FakeFfiCheckCodeSender())
|
GrantGeneratedQrLoginProgress.QrScanned(FakeFfiCheckCodeSender())
|
||||||
to LinkMobileStep.QrScanned::class.java,
|
to ExpectedResult(LinkMobileStep.QrScanned::class.java),
|
||||||
GrantGeneratedQrLoginProgress.QrReady(FakeFfiQrCodeData(toBytesResult = { QR_CODE_DATA_RECIPROCATE }))
|
GrantGeneratedQrLoginProgress.QrReady(FakeFfiQrCodeData(toBytesResult = { QR_CODE_DATA_RECIPROCATE }))
|
||||||
to LinkMobileStep.QrReady::class.java,
|
to ExpectedResult(LinkMobileStep.QrReady::class.java),
|
||||||
GrantGeneratedQrLoginProgress.Done to LinkMobileStep.Done::class.java,
|
GrantGeneratedQrLoginProgress.Done to ExpectedResult(LinkMobileStep.Done),
|
||||||
).forEach { (progress, expectedStepClass) ->
|
).forEach { (progress, expectedResult) ->
|
||||||
handler.emitGenerateProgress(progress)
|
handler.emitGenerateProgress(progress)
|
||||||
assertThat(awaitItem()).isInstanceOf(expectedStepClass)
|
assertThat(awaitItem()).match(expectedResult)
|
||||||
}
|
}
|
||||||
// generate returns, no new event is emitted
|
// generate returns, no new event is emitted
|
||||||
completable.complete(Unit)
|
completable.complete(Unit)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.tests.testutils
|
||||||
|
|
||||||
|
import com.google.common.truth.Subject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class to assert expected results in tests. It can be used to assert either the type of result or its value.
|
||||||
|
*/
|
||||||
|
data class ExpectedResult<T : Any>(
|
||||||
|
val resultClass: Class<out T>,
|
||||||
|
val result: T? = null,
|
||||||
|
) {
|
||||||
|
constructor(result: T) : this(result::class.java, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Any> Subject.match(expectedResult: ExpectedResult<T>) {
|
||||||
|
if (expectedResult.result != null) {
|
||||||
|
isEqualTo(expectedResult.result)
|
||||||
|
} else {
|
||||||
|
isInstanceOf(expectedResult.resultClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user