Skip to content

Commit 044737a

Browse files
authored
Merge pull request #6719 from element-hq/feature/bma/qrCodeLoginIteration
QR code login iteration
2 parents 7d5877c + 668243f commit 044737a

11 files changed

Lines changed: 156 additions & 17 deletions

File tree

features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import kotlinx.coroutines.CoroutineScope
5656
import kotlinx.coroutines.Job
5757
import kotlinx.coroutines.flow.launchIn
5858
import kotlinx.coroutines.flow.onEach
59+
import kotlinx.coroutines.launch
5960
import kotlinx.parcelize.Parcelize
6061
import timber.log.Timber
6162

@@ -102,6 +103,7 @@ class LinkNewDeviceFlowNode(
102103
@Suppress("AssignedValueIsNeverRead")
103104
linkDesktopHandlerJob = observeLinkNewDesktopHandler()
104105
},
106+
onResume = ::onResume,
105107
onDestroy = {
106108
linkMobileHandlerJob?.cancel()
107109
linkDesktopHandlerJob?.cancel()
@@ -209,6 +211,18 @@ class LinkNewDeviceFlowNode(
209211
.launchIn(sessionCoroutineScope)
210212
}
211213

214+
private fun onResume() = sessionCoroutineScope.launch {
215+
// Application is resumed, if the step is waiting for auth, send the confirmation
216+
(linkNewMobileHandler.stepFlow.value as? LinkMobileStep.WaitingForAuth)?.let {
217+
Timber.tag(tag.value).d("Resuming while waiting for auth on mobile, sending confirmation")
218+
it.continuationMessageSender.confirm()
219+
}
220+
(linkNewDesktopHandler.stepFlow.value as? LinkDesktopStep.WaitingForAuth)?.let {
221+
Timber.tag(tag.value).d("Resuming while waiting for auth on desktop, sending confirmation")
222+
it.continuationMessageSender.confirm()
223+
}
224+
}
225+
212226
private fun navigateToError(errorType: ErrorType) {
213227
// Map the error to an error screen
214228
val error = when (errorType) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright (c) 2026 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.matrix.api.linknewdevice
9+
10+
interface ContinuationMessageSender {
11+
suspend fun cancel(): Result<Unit>
12+
suspend fun confirm(): Result<Unit>
13+
}

libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkDesktopHandler.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ interface LinkDesktopHandler {
1818
sealed interface LinkDesktopStep {
1919
data object Uninitialized : LinkDesktopStep
2020
data object Starting : LinkDesktopStep
21+
2122
data class WaitingForAuth(
2223
val verificationUri: String,
24+
val continuationMessageSender: ContinuationMessageSender,
2325
) : LinkDesktopStep
2426

2527
data class EstablishingSecureChannel(

libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkMobileHandler.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ sealed interface LinkMobileStep {
2222
data object Starting : LinkMobileStep
2323
data class QrReady(val data: String) : LinkMobileStep
2424
data object QrRotating : LinkMobileStep
25-
data class WaitingForAuth(val verificationUri: String) : LinkMobileStep
25+
26+
data class WaitingForAuth(
27+
val verificationUri: String,
28+
val continuationMessageSender: ContinuationMessageSender,
29+
) : LinkMobileStep
30+
2631
data class QrScanned(val checkCodeSender: CheckCodeSender) : LinkMobileStep
2732
data class Error(val errorType: ErrorType) : LinkMobileStep
2833
data object SyncingSecrets : LinkMobileStep
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (c) 2026 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.matrix.impl.linknewdevice
9+
10+
import io.element.android.libraries.core.extensions.runCatchingExceptions
11+
import io.element.android.libraries.matrix.api.linknewdevice.ContinuationMessageSender
12+
import kotlinx.coroutines.CoroutineDispatcher
13+
import kotlinx.coroutines.withContext
14+
import timber.log.Timber
15+
import org.matrix.rustcomponents.sdk.ContinuationMessageSender as FfiContinuationMessageSender
16+
17+
class RustContinuationMessageSender(
18+
private val inner: FfiContinuationMessageSender,
19+
private val sessionDispatcher: CoroutineDispatcher,
20+
) : ContinuationMessageSender {
21+
override suspend fun cancel(): Result<Unit> = withContext(sessionDispatcher) {
22+
runCatchingExceptions {
23+
inner.cancel()
24+
}.onFailure {
25+
Timber.e(it, "Error cancelling continuation message")
26+
}
27+
}
28+
29+
override suspend fun confirm(): Result<Unit> = withContext(sessionDispatcher) {
30+
runCatchingExceptions {
31+
inner.confirm()
32+
}.onFailure {
33+
Timber.e(it, "Error confirming continuation message")
34+
}
35+
}
36+
}

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ class RustLinkDesktopHandler(
7575
GrantQrLoginProgress.SyncingSecrets -> LinkDesktopStep.SyncingSecrets
7676
is GrantQrLoginProgress.WaitingForAuth -> LinkDesktopStep.WaitingForAuth(
7777
verificationUri = verificationUri,
78+
continuationMessageSender = RustContinuationMessageSender(
79+
inner = continuationSender,
80+
sessionDispatcher = sessionDispatcher,
81+
)
7882
)
7983
is GrantQrLoginProgress.EstablishingSecureChannel -> LinkDesktopStep.EstablishingSecureChannel(
8084
checkCode = checkCode,

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,13 @@ class RustLinkMobileHandler(
8484
)
8585
GrantGeneratedQrLoginProgress.Starting -> LinkMobileStep.Starting
8686
GrantGeneratedQrLoginProgress.SyncingSecrets -> LinkMobileStep.SyncingSecrets
87-
is GrantGeneratedQrLoginProgress.WaitingForAuth -> LinkMobileStep.WaitingForAuth(verificationUri)
87+
is GrantGeneratedQrLoginProgress.WaitingForAuth -> LinkMobileStep.WaitingForAuth(
88+
verificationUri = verificationUri,
89+
continuationMessageSender = RustContinuationMessageSender(
90+
inner = continuationSender,
91+
sessionDispatcher = sessionDispatcher,
92+
)
93+
)
8894
}
8995
}
9096
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2026 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.matrix.impl.linknewdevice
9+
10+
import io.element.android.libraries.matrix.api.linknewdevice.ContinuationMessageSender
11+
import io.element.android.tests.testutils.lambda.lambdaError
12+
import io.element.android.tests.testutils.simulateLongTask
13+
14+
class FakeContinuationMessageSender(
15+
private val cancelResult: () -> Result<Unit> = { lambdaError() },
16+
private val confirmResult: () -> Result<Unit> = { lambdaError() },
17+
) : ContinuationMessageSender {
18+
override suspend fun cancel(): Result<Unit> = simulateLongTask {
19+
cancelResult()
20+
}
21+
22+
override suspend fun confirm(): Result<Unit> = simulateLongTask {
23+
confirmResult()
24+
}
25+
}

libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep
1616
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiContinuationMessageSender
1717
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
1818
import io.element.android.libraries.matrix.test.QR_CODE_DATA
19+
import io.element.android.tests.testutils.ExpectedResult
20+
import io.element.android.tests.testutils.match
1921
import kotlinx.coroutines.CompletableDeferred
2022
import kotlinx.coroutines.ExperimentalCoroutinesApi
2123
import kotlinx.coroutines.launch
@@ -50,16 +52,18 @@ class RustLinkDesktopHandlerTest {
5052
runCurrent()
5153
// progress from the handler is mapped and emitted
5254
listOf(
53-
GrantQrLoginProgress.Starting to LinkDesktopStep.Starting,
54-
GrantQrLoginProgress.SyncingSecrets to LinkDesktopStep.SyncingSecrets,
55+
GrantQrLoginProgress.Starting to ExpectedResult(LinkDesktopStep.Starting),
56+
GrantQrLoginProgress.SyncingSecrets to ExpectedResult(LinkDesktopStep.SyncingSecrets),
5557
GrantQrLoginProgress.WaitingForAuth("aVerificationUri", FakeFfiContinuationMessageSender())
56-
to LinkDesktopStep.WaitingForAuth("aVerificationUri"),
58+
to ExpectedResult(
59+
resultClass = LinkDesktopStep.WaitingForAuth::class.java,
60+
),
5761
GrantQrLoginProgress.EstablishingSecureChannel(1.toUByte(), "1")
58-
to LinkDesktopStep.EstablishingSecureChannel(1.toUByte(), "1"),
59-
GrantQrLoginProgress.Done to LinkDesktopStep.Done,
60-
).forEach { (progress, expectedStep) ->
62+
to ExpectedResult(LinkDesktopStep.EstablishingSecureChannel(1.toUByte(), "1")),
63+
GrantQrLoginProgress.Done to ExpectedResult(LinkDesktopStep.Done),
64+
).forEach { (progress, expectedResult) ->
6165
handler.emitScanProgress(progress)
62-
assertThat(awaitItem()).isEqualTo(expectedStep)
66+
assertThat(awaitItem()).match(expectedResult)
6367
}
6468
// scan returns, no new event is emitted
6569
completable.complete(Unit)

libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiContinuati
1818
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
1919
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData
2020
import io.element.android.libraries.matrix.test.QR_CODE_DATA_RECIPROCATE
21+
import io.element.android.tests.testutils.ExpectedResult
22+
import io.element.android.tests.testutils.match
2123
import kotlinx.coroutines.CompletableDeferred
2224
import kotlinx.coroutines.ExperimentalCoroutinesApi
2325
import kotlinx.coroutines.launch
@@ -51,18 +53,18 @@ class RustLinkMobileHandlerTest {
5153
runCurrent()
5254
// progress from the handler is mapped and emitted
5355
listOf(
54-
GrantGeneratedQrLoginProgress.Starting to LinkMobileStep.Starting::class.java,
55-
GrantGeneratedQrLoginProgress.SyncingSecrets to LinkMobileStep.SyncingSecrets::class.java,
56+
GrantGeneratedQrLoginProgress.Starting to ExpectedResult(LinkMobileStep.Starting),
57+
GrantGeneratedQrLoginProgress.SyncingSecrets to ExpectedResult(LinkMobileStep.SyncingSecrets),
5658
GrantGeneratedQrLoginProgress.WaitingForAuth("aVerificationUri", FakeFfiContinuationMessageSender())
57-
to LinkMobileStep.WaitingForAuth::class.java,
59+
to ExpectedResult(LinkMobileStep.WaitingForAuth::class.java),
5860
GrantGeneratedQrLoginProgress.QrScanned(FakeFfiCheckCodeSender())
59-
to LinkMobileStep.QrScanned::class.java,
61+
to ExpectedResult(LinkMobileStep.QrScanned::class.java),
6062
GrantGeneratedQrLoginProgress.QrReady(FakeFfiQrCodeData(toBytesResult = { QR_CODE_DATA_RECIPROCATE }))
61-
to LinkMobileStep.QrReady::class.java,
62-
GrantGeneratedQrLoginProgress.Done to LinkMobileStep.Done::class.java,
63-
).forEach { (progress, expectedStepClass) ->
63+
to ExpectedResult(LinkMobileStep.QrReady::class.java),
64+
GrantGeneratedQrLoginProgress.Done to ExpectedResult(LinkMobileStep.Done),
65+
).forEach { (progress, expectedResult) ->
6466
handler.emitGenerateProgress(progress)
65-
assertThat(awaitItem()).isInstanceOf(expectedStepClass)
67+
assertThat(awaitItem()).match(expectedResult)
6668
}
6769
// generate returns, no new event is emitted
6870
completable.complete(Unit)

0 commit comments

Comments
 (0)