Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
import io.element.android.libraries.matrix.api.logs.LoggerTags
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import timber.log.Timber

Expand Down Expand Up @@ -91,6 +93,7 @@ class LinkNewDeviceFlowNode(
@Suppress("AssignedValueIsNeverRead")
linkDesktopHandlerJob = observeLinkNewDesktopHandler()
},
onResume = ::onResume,
onDestroy = {
linkMobileHandlerJob?.cancel()
linkDesktopHandlerJob?.cancel()
Expand Down Expand Up @@ -152,9 +155,14 @@ class LinkNewDeviceFlowNode(
// This step is not received at the moment, so do nothing
}
LinkMobileStep.SyncingSecrets -> Unit
is LinkMobileStep.WaitingForAuth -> {
is LinkMobileStep.OpeningVerificationUri -> {
// Confirm right now, we do not have a confirmation dialog on Android
linkMobileStep.continuationMessageSender.confirm()
navigateToBrowser(linkMobileStep.verificationUri)
}
is LinkMobileStep.WaitingForAuth -> {
// Just wait for the application to be resumed
}
}
}
.launchIn(sessionCoroutineScope)
Expand All @@ -178,14 +186,31 @@ class LinkNewDeviceFlowNode(
LinkDesktopStep.Starting -> Unit
LinkDesktopStep.SyncingSecrets -> Unit
LinkDesktopStep.Uninitialized -> Unit
is LinkDesktopStep.WaitingForAuth -> {
is LinkDesktopStep.OpeningVerificationUri -> {
// Confirm right now, we do not have a confirmation dialog on Android
linkDesktopStep.continuationMessageSender.confirm()
navigateToBrowser(linkDesktopStep.verificationUri)
}
is LinkDesktopStep.WaitingForAuth -> {
// Just wait for the application to be resumed
}
}
}
.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) {
// Map the error to an error screen
val error = when (errorType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ interface LinkDesktopHandler {
sealed interface LinkDesktopStep {
data object Uninitialized : LinkDesktopStep
data object Starting : LinkDesktopStep
data class WaitingForAuth(
data class OpeningVerificationUri(
val verificationUri: String,
val continuationMessageSender: ContinuationMessageSender,
) : LinkDesktopStep

data class WaitingForAuth(
val continuationMessageSender: ContinuationMessageSender,
) : LinkDesktopStep

data class EstablishingSecureChannel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ sealed interface LinkMobileStep {
data object Uninitialized : LinkMobileStep
data object Starting : LinkMobileStep
data class QrReady(val data: String) : LinkMobileStep
data class WaitingForAuth(val verificationUri: String) : LinkMobileStep
data class OpeningVerificationUri(
val verificationUri: String,
val continuationMessageSender: ContinuationMessageSender,
) : LinkMobileStep

data class WaitingForAuth(
val continuationMessageSender: ContinuationMessageSender,
) : LinkMobileStep

data class QrScanned(val checkCodeSender: CheckCodeSender) : LinkMobileStep
data class Error(val errorType: ErrorType) : LinkMobileStep
data object SyncingSecrets : LinkMobileStep
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,18 @@ class RustLinkDesktopHandler(
GrantQrLoginProgress.Done -> LinkDesktopStep.Done
GrantQrLoginProgress.Starting -> LinkDesktopStep.Starting
GrantQrLoginProgress.SyncingSecrets -> LinkDesktopStep.SyncingSecrets
is GrantQrLoginProgress.WaitingForAuth -> LinkDesktopStep.WaitingForAuth(
is GrantQrLoginProgress.OpeningVerificationUri -> LinkDesktopStep.OpeningVerificationUri(
verificationUri = verificationUri,
continuationMessageSender = RustContinuationMessageSender(
inner = continuationSender,
sessionDispatcher = sessionDispatcher,
)
)
is GrantQrLoginProgress.WaitingForAuth -> LinkDesktopStep.WaitingForAuth(
continuationMessageSender = RustContinuationMessageSender(
inner = continuationSender,
sessionDispatcher = sessionDispatcher,
)
)
is GrantQrLoginProgress.EstablishingSecureChannel -> LinkDesktopStep.EstablishingSecureChannel(
checkCode = checkCode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,21 @@ class RustLinkMobileHandler(
)
GrantGeneratedQrLoginProgress.Starting -> LinkMobileStep.Starting
GrantGeneratedQrLoginProgress.SyncingSecrets -> LinkMobileStep.SyncingSecrets
is GrantGeneratedQrLoginProgress.WaitingForAuth -> LinkMobileStep.WaitingForAuth(verificationUri)
is GrantGeneratedQrLoginProgress.OpeningVerificationUri -> {
LinkMobileStep.OpeningVerificationUri(
verificationUri = verificationUri,
continuationMessageSender = RustContinuationMessageSender(
inner = continuationSender,
sessionDispatcher = sessionDispatcher,
),
)
}
is GrantGeneratedQrLoginProgress.WaitingForAuth -> LinkMobileStep.WaitingForAuth(
continuationMessageSender = RustContinuationMessageSender(
inner = continuationSender,
sessionDispatcher = sessionDispatcher,
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ internal fun aRustRoomInfo(
isLowPriority: Boolean = false,
activeRoomCallConsensusIntent: RtcCallIntentConsensus = RtcCallIntentConsensus.None,
activeServiceMembersCount: Int = 0,
isDm: Boolean = false,
) = RoomInfo(
id = id,
displayName = displayName,
Expand Down Expand Up @@ -104,5 +103,4 @@ internal fun aRustRoomInfo(
isLowPriority = isLowPriority,
activeRoomCallConsensusIntent = activeRoomCallConsensusIntent,
activeServiceMembersCount = activeServiceMembersCount.toULong(),
isDm = isDm,
)
Original file line number Diff line number Diff line change
@@ -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.impl.fixtures.fakes

import org.matrix.rustcomponents.sdk.ContinuationMessageSender
import org.matrix.rustcomponents.sdk.NoHandle

class FakeFfiContinuationMessageSender : ContinuationMessageSender(NoHandle)
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.linknewdevice.ErrorType
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.FakeFfiGrantLoginWithQrCodeHandler
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.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -49,16 +52,24 @@ class RustLinkDesktopHandlerTest {
runCurrent()
// progress from the handler is mapped and emitted
listOf(
GrantQrLoginProgress.Starting to LinkDesktopStep.Starting,
GrantQrLoginProgress.SyncingSecrets to LinkDesktopStep.SyncingSecrets,
GrantQrLoginProgress.WaitingForAuth("aVerificationUri")
to LinkDesktopStep.WaitingForAuth("aVerificationUri"),
GrantQrLoginProgress.Starting to ExpectedResult(LinkDesktopStep.Starting),
GrantQrLoginProgress.SyncingSecrets to ExpectedResult(LinkDesktopStep.SyncingSecrets),
GrantQrLoginProgress.OpeningVerificationUri(
verificationUri = "aVerificationUri",
continuationSender = FakeFfiContinuationMessageSender(),
) to ExpectedResult(
resultClass = LinkDesktopStep.OpeningVerificationUri::class.java,
),
GrantQrLoginProgress.WaitingForAuth(FakeFfiContinuationMessageSender())
to ExpectedResult(
resultClass = LinkDesktopStep.WaitingForAuth::class.java,
),
GrantQrLoginProgress.EstablishingSecureChannel(1.toUByte(), "1")
to LinkDesktopStep.EstablishingSecureChannel(1.toUByte(), "1"),
GrantQrLoginProgress.Done to LinkDesktopStep.Done,
).forEach { (progress, expectedStep) ->
to ExpectedResult(LinkDesktopStep.EstablishingSecureChannel(1.toUByte(), "1")),
GrantQrLoginProgress.Done to ExpectedResult(LinkDesktopStep.Done),
).forEach { (progress, expectedResult) ->
handler.emitScanProgress(progress)
assertThat(awaitItem()).isEqualTo(expectedStep)
assertThat(awaitItem()).match(expectedResult)
}
// scan returns, no new event is emitted
completable.complete(Unit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.linknewdevice.ErrorType
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiCheckCodeSender
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.FakeFfiQrCodeData
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.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -50,18 +53,22 @@ class RustLinkMobileHandlerTest {
runCurrent()
// progress from the handler is mapped and emitted
listOf(
GrantGeneratedQrLoginProgress.Starting to LinkMobileStep.Starting::class.java,
GrantGeneratedQrLoginProgress.SyncingSecrets to LinkMobileStep.SyncingSecrets::class.java,
GrantGeneratedQrLoginProgress.WaitingForAuth("aVerificationUri")
to LinkMobileStep.WaitingForAuth::class.java,
GrantGeneratedQrLoginProgress.Starting to ExpectedResult(LinkMobileStep.Starting),
GrantGeneratedQrLoginProgress.SyncingSecrets to ExpectedResult(LinkMobileStep.SyncingSecrets),
GrantGeneratedQrLoginProgress.OpeningVerificationUri(
verificationUri = "aVerificationUri",
continuationSender = FakeFfiContinuationMessageSender(),
) to ExpectedResult(LinkMobileStep.OpeningVerificationUri::class.java),
GrantGeneratedQrLoginProgress.WaitingForAuth(FakeFfiContinuationMessageSender())
to ExpectedResult(LinkMobileStep.WaitingForAuth::class.java),
GrantGeneratedQrLoginProgress.QrScanned(FakeFfiCheckCodeSender())
to LinkMobileStep.QrScanned::class.java,
to ExpectedResult(LinkMobileStep.QrScanned::class.java),
GrantGeneratedQrLoginProgress.QrReady(FakeFfiQrCodeData(toBytesResult = { QR_CODE_DATA_RECIPROCATE }))
to LinkMobileStep.QrReady::class.java,
GrantGeneratedQrLoginProgress.Done to LinkMobileStep.Done::class.java,
).forEach { (progress, expectedStepClass) ->
to ExpectedResult(LinkMobileStep.QrReady::class.java),
GrantGeneratedQrLoginProgress.Done to ExpectedResult(LinkMobileStep.Done),
).forEach { (progress, expectedResult) ->
handler.emitGenerateProgress(progress)
assertThat(awaitItem()).isInstanceOf(expectedStepClass)
assertThat(awaitItem()).match(expectedResult)
}
// generate returns, no new event is emitted
completable.complete(Unit)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading