Skip to content

Commit 3ea10c2

Browse files
authored
Merge pull request #5909 from element-hq/feature/bma/qrCodeLogin
Link new device using QrCode - First version
2 parents 8948c2e + 798132f commit 3ea10c2

File tree

180 files changed

+5052
-144
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

180 files changed

+5052
-144
lines changed

appnav/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ dependencies {
4848

4949
implementation(projects.features.announcement.api)
5050
implementation(projects.features.ftue.api)
51+
implementation(projects.features.linknewdevice.api)
5152
implementation(projects.features.share.api)
5253

5354
implementation(projects.services.apperror.impl)

appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import io.element.android.features.ftue.api.FtueEntryPoint
5353
import io.element.android.features.ftue.api.state.FtueService
5454
import io.element.android.features.ftue.api.state.FtueState
5555
import io.element.android.features.home.api.HomeEntryPoint
56+
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
5657
import io.element.android.features.networkmonitor.api.NetworkMonitor
5758
import io.element.android.features.networkmonitor.api.NetworkStatus
5859
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
@@ -123,6 +124,7 @@ class LoggedInFlowNode(
123124
private val secureBackupEntryPoint: SecureBackupEntryPoint,
124125
private val userProfileEntryPoint: UserProfileEntryPoint,
125126
private val ftueEntryPoint: FtueEntryPoint,
127+
private val linkNewDeviceEntryPoint: LinkNewDeviceEntryPoint,
126128
@SessionCoroutineScope
127129
private val sessionCoroutineScope: CoroutineScope,
128130
private val ftueService: FtueService,
@@ -293,6 +295,9 @@ class LoggedInFlowNode(
293295
@Parcelize
294296
data object Ftue : NavTarget
295297

298+
@Parcelize
299+
data object LinkNewDevice : NavTarget
300+
296301
@Parcelize
297302
data object RoomDirectory : NavTarget
298303

@@ -419,6 +424,10 @@ class LoggedInFlowNode(
419424
callback.navigateToAddAccount()
420425
}
421426

427+
override fun navigateToLinkNewDevice() {
428+
backstack.push(NavTarget.LinkNewDevice)
429+
}
430+
422431
override fun navigateToBugReport() {
423432
callback.navigateToBugReport()
424433
}
@@ -475,6 +484,14 @@ class LoggedInFlowNode(
475484
NavTarget.Ftue -> {
476485
ftueEntryPoint.createNode(this, buildContext)
477486
}
487+
NavTarget.LinkNewDevice -> {
488+
val callback = object : LinkNewDeviceEntryPoint.Callback {
489+
override fun onDone() {
490+
backstack.pop()
491+
}
492+
}
493+
linkNewDeviceEntryPoint.createNode(this, buildContext, callback)
494+
}
478495
NavTarget.RoomDirectory -> {
479496
roomDirectoryEntryPoint.createNode(
480497
parentNode = this,

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import io.element.android.compound.theme.ElementTheme
2525
import io.element.android.compound.tokens.generated.CompoundIcons
2626
import io.element.android.features.ftue.impl.R
2727
import io.element.android.libraries.architecture.AsyncData
28+
import io.element.android.libraries.designsystem.atomic.atoms.LoadingButtonAtom
2829
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
2930
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
3031
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@@ -111,13 +112,7 @@ private fun ChooseSelfVerificationModeButtons(
111112
AsyncData.Uninitialized,
112113
is AsyncData.Failure,
113114
is AsyncData.Loading -> {
114-
Button(
115-
modifier = Modifier.fillMaxWidth(),
116-
enabled = false,
117-
showProgress = true,
118-
text = stringResource(CommonStrings.common_loading),
119-
onClick = {},
120-
)
115+
LoadingButtonAtom()
121116
}
122117
is AsyncData.Success -> {
123118
if (state.buttonsState.data.canUseAnotherDevice) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright (c) 2025 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+
plugins {
8+
id("io.element.android-library")
9+
}
10+
11+
android {
12+
namespace = "io.element.android.features.linknewdevice.api"
13+
}
14+
15+
dependencies {
16+
implementation(projects.libraries.architecture)
17+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2025 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.features.linknewdevice.api
9+
10+
import com.bumble.appyx.core.modality.BuildContext
11+
import com.bumble.appyx.core.node.Node
12+
import com.bumble.appyx.core.plugin.Plugin
13+
import io.element.android.libraries.architecture.FeatureEntryPoint
14+
15+
interface LinkNewDeviceEntryPoint : FeatureEntryPoint {
16+
interface Callback : Plugin {
17+
fun onDone()
18+
}
19+
20+
fun createNode(
21+
parentNode: Node,
22+
buildContext: BuildContext,
23+
callback: Callback,
24+
): Node
25+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import extension.setupDependencyInjection
2+
import extension.testCommonDependencies
3+
4+
/*
5+
* Copyright (c) 2025 Element Creations Ltd.
6+
*
7+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
8+
* Please see LICENSE files in the repository root for full details.
9+
*/
10+
11+
plugins {
12+
id("io.element.android-compose-library")
13+
id("kotlin-parcelize")
14+
alias(libs.plugins.kotlin.serialization)
15+
}
16+
17+
android {
18+
namespace = "io.element.android.features.linknewdevice.impl"
19+
20+
testOptions {
21+
unitTests {
22+
isIncludeAndroidResources = true
23+
}
24+
}
25+
}
26+
27+
setupDependencyInjection()
28+
29+
dependencies {
30+
// TODO Cleanup
31+
implementation(projects.appconfig)
32+
implementation(projects.features.enterprise.api)
33+
implementation(projects.features.rageshake.api)
34+
implementation(projects.libraries.core)
35+
implementation(projects.libraries.androidutils)
36+
implementation(projects.libraries.architecture)
37+
implementation(projects.libraries.featureflag.api)
38+
implementation(projects.libraries.matrix.api)
39+
implementation(projects.libraries.matrix.api)
40+
implementation(projects.libraries.designsystem)
41+
implementation(projects.libraries.testtags)
42+
implementation(projects.libraries.uiStrings)
43+
implementation(projects.libraries.permissions.api)
44+
implementation(projects.libraries.sessionStorage.api)
45+
implementation(projects.libraries.qrcode)
46+
implementation(projects.libraries.oidc.api)
47+
implementation(projects.libraries.uiUtils)
48+
implementation(projects.libraries.wellknown.api)
49+
implementation(libs.androidx.browser)
50+
implementation(libs.androidx.webkit)
51+
implementation(libs.serialization.json)
52+
api(projects.features.linknewdevice.api)
53+
54+
testCommonDependencies(libs, true)
55+
testImplementation(projects.features.linknewdevice.test)
56+
testImplementation(projects.features.enterprise.test)
57+
testImplementation(projects.libraries.featureflag.test)
58+
testImplementation(projects.libraries.matrix.test)
59+
testImplementation(projects.libraries.oidc.test)
60+
testImplementation(projects.libraries.permissions.test)
61+
testImplementation(projects.libraries.sessionStorage.test)
62+
testImplementation(projects.libraries.wellknown.test)
63+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ Copyright (c) 2025 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+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
8+
9+
<queries>
10+
<!-- To open URL in CustomTab (prefetch, etc.). It makes CustomTabsClient.getPackageName() work
11+
see https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs -->
12+
<intent>
13+
<action android:name="android.support.customtabs.action.CustomTabsService" />
14+
</intent>
15+
</queries>
16+
17+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2025 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.features.linknewdevice.impl
9+
10+
import com.bumble.appyx.core.modality.BuildContext
11+
import com.bumble.appyx.core.node.Node
12+
import dev.zacsweers.metro.ContributesBinding
13+
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
14+
import io.element.android.libraries.architecture.createNode
15+
import io.element.android.libraries.di.SessionScope
16+
17+
@ContributesBinding(SessionScope::class)
18+
class DefaultLinkNewDeviceEntryPoint : LinkNewDeviceEntryPoint {
19+
override fun createNode(
20+
parentNode: Node,
21+
buildContext: BuildContext,
22+
callback: LinkNewDeviceEntryPoint.Callback,
23+
): Node {
24+
return parentNode.createNode<LinkNewDeviceFlowNode>(
25+
buildContext = buildContext,
26+
plugins = listOf(
27+
callback,
28+
)
29+
)
30+
}
31+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2025 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.features.linknewdevice.impl
9+
10+
import dev.zacsweers.metro.Inject
11+
import dev.zacsweers.metro.SingleIn
12+
import io.element.android.libraries.core.log.logger.LoggerTag
13+
import io.element.android.libraries.di.SessionScope
14+
import io.element.android.libraries.matrix.api.MatrixClient
15+
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
16+
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep
17+
import io.element.android.libraries.matrix.api.logs.LoggerTags
18+
import kotlinx.coroutines.Job
19+
import kotlinx.coroutines.flow.MutableStateFlow
20+
import kotlinx.coroutines.flow.StateFlow
21+
import kotlinx.coroutines.flow.asStateFlow
22+
import kotlinx.coroutines.flow.launchIn
23+
import kotlinx.coroutines.flow.onEach
24+
import kotlinx.coroutines.launch
25+
import timber.log.Timber
26+
27+
private val loggerTag = LoggerTag("LinkNewDesktopHandler", LoggerTags.linkNewDevice)
28+
29+
@Inject
30+
@SingleIn(SessionScope::class)
31+
class LinkNewDesktopHandler(
32+
private val matrixClient: MatrixClient,
33+
) {
34+
private val sessionScope = matrixClient.sessionCoroutineScope
35+
private val linkDesktopStepFlow = MutableStateFlow<LinkDesktopStep>(
36+
LinkDesktopStep.Uninitialized
37+
)
38+
39+
val stepFlow: StateFlow<LinkDesktopStep>
40+
get() = linkDesktopStepFlow.asStateFlow()
41+
42+
private var currentJob: Job? = null
43+
private var handler: LinkDesktopHandler? = null
44+
45+
fun createNewHandler() {
46+
currentJob?.cancel()
47+
currentJob = null
48+
handler = matrixClient.createLinkDesktopHandler().getOrNull()
49+
}
50+
51+
fun reset() {
52+
currentJob?.cancel()
53+
currentJob = null
54+
sessionScope.launch {
55+
linkDesktopStepFlow.emit(LinkDesktopStep.Uninitialized)
56+
}
57+
}
58+
59+
fun onScannedCode(data: ByteArray) {
60+
currentJob?.cancel()
61+
currentJob = null
62+
val currentHandler = handler
63+
if (currentHandler == null) {
64+
Timber.tag(loggerTag.value).e("onScannedCode: Handler is not initialized. Call createNewHandler() first.")
65+
} else {
66+
currentJob = matrixClient.sessionCoroutineScope.launch {
67+
currentHandler.linkDesktopStep.onEach {
68+
linkDesktopStepFlow.emit(it)
69+
}.launchIn(this)
70+
currentHandler.handleScannedQrCode(data)
71+
}
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)