Skip to content

Commit d56b278

Browse files
committed
[Link new device] Automatically rotate the QR Code
1 parent 1111315 commit d56b278

9 files changed

Lines changed: 152 additions & 12 deletions

File tree

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,14 @@ class LinkNewDeviceFlowNode(
144144
navigateToError(linkMobileStep.errorType)
145145
}
146146
is LinkMobileStep.QrReady -> {
147-
// The QrCode is ready, navigate to its display
148-
backstack.push(NavTarget.MobileShowQrCode(linkMobileStep.data))
147+
// The QrCode is ready, navigate to its display, if not already there
148+
val navTarget = backstack.elements.value.last().key.navTarget
149+
if (navTarget !is NavTarget.MobileShowQrCode) {
150+
backstack.push(NavTarget.MobileShowQrCode(linkMobileStep.data))
151+
}
152+
}
153+
LinkMobileStep.QrRotating -> {
154+
// This step is handled in ShowQrCodePresenter
149155
}
150156
is LinkMobileStep.QrScanned -> {
151157
backstack.replace(NavTarget.MobileEnterNumber)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,8 @@ class LinkNewMobileHandler(
6565
linkMobileStepFlow.emit(LinkMobileStep.Uninitialized)
6666
}
6767
}
68+
69+
fun rotateQrCode() {
70+
createAndStartNewHandler()
71+
}
6872
}

features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeNode.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import io.element.android.libraries.di.SessionScope
2525
class ShowQrCodeNode(
2626
@Assisted buildContext: BuildContext,
2727
@Assisted plugins: List<Plugin>,
28+
showQrCodePresenterFactory: ShowQrCodePresenter.Factory,
2829
) : Node(buildContext, plugins = plugins) {
2930
class Inputs(
3031
val data: String,
@@ -36,11 +37,15 @@ class ShowQrCodeNode(
3637

3738
private val inputs: Inputs = inputs<Inputs>()
3839
private val callback: Callback = callback()
40+
private val showQrCodePresenter: ShowQrCodePresenter = showQrCodePresenterFactory.create(
41+
initialData = inputs.data,
42+
)
3943

4044
@Composable
4145
override fun View(modifier: Modifier) {
46+
val state = showQrCodePresenter.present()
4247
ShowQrCodeView(
43-
data = inputs.data,
48+
state = state,
4449
modifier = modifier,
4550
onBackClick = callback::navigateBack,
4651
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.features.linknewdevice.impl.screens.qrcode
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.runtime.produceState
13+
import dev.zacsweers.metro.Assisted
14+
import dev.zacsweers.metro.AssistedFactory
15+
import dev.zacsweers.metro.AssistedInject
16+
import io.element.android.features.linknewdevice.impl.LinkNewMobileHandler
17+
import io.element.android.libraries.architecture.AsyncData
18+
import io.element.android.libraries.architecture.Presenter
19+
import io.element.android.libraries.core.log.logger.LoggerTag
20+
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
21+
import io.element.android.libraries.matrix.api.logs.LoggerTags
22+
import timber.log.Timber
23+
24+
private val tag = LoggerTag("ShowQrCodePresenter", LoggerTags.linkNewDevice)
25+
26+
@AssistedInject
27+
class ShowQrCodePresenter(
28+
@Assisted private val initialData: String,
29+
private val linkNewMobileHandler: LinkNewMobileHandler,
30+
) : Presenter<ShowQrCodeState> {
31+
@AssistedFactory
32+
interface Factory {
33+
fun create(initialData: String): ShowQrCodePresenter
34+
}
35+
36+
@Composable
37+
override fun present(): ShowQrCodeState {
38+
val data by produceState<AsyncData<String>>(AsyncData.Success(initialData)) {
39+
linkNewMobileHandler.stepFlow.collect { step ->
40+
when (step) {
41+
is LinkMobileStep.QrReady -> {
42+
value = AsyncData.Success(step.data)
43+
}
44+
is LinkMobileStep.QrRotating -> {
45+
Timber.tag(tag.value).d("Rotating QrCode")
46+
linkNewMobileHandler.rotateQrCode()
47+
value = AsyncData.Loading()
48+
}
49+
else -> Unit
50+
}
51+
}
52+
}
53+
54+
return ShowQrCodeState(
55+
data = data,
56+
)
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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.features.linknewdevice.impl.screens.qrcode
9+
10+
import io.element.android.libraries.architecture.AsyncData
11+
12+
data class ShowQrCodeState(
13+
val data: AsyncData<String>,
14+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.features.linknewdevice.impl.screens.qrcode
9+
10+
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
11+
import io.element.android.libraries.architecture.AsyncData
12+
13+
class ShowQrCodeStateProvider : PreviewParameterProvider<ShowQrCodeState> {
14+
override val values: Sequence<ShowQrCodeState>
15+
get() = sequenceOf(
16+
aShowQrCodeState(),
17+
ShowQrCodeState(
18+
data = AsyncData.Loading(),
19+
),
20+
)
21+
}
22+
23+
private fun aShowQrCodeState(
24+
data: AsyncData.Success<String> = AsyncData.Success("DATA"),
25+
) = ShowQrCodeState(
26+
data = data,
27+
)

features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeView.kt

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package io.element.android.features.linknewdevice.impl.screens.qrcode
1111

12+
import androidx.compose.foundation.layout.Box
1213
import androidx.compose.foundation.layout.Column
1314
import androidx.compose.foundation.layout.Spacer
1415
import androidx.compose.foundation.layout.fillMaxSize
@@ -21,6 +22,7 @@ import androidx.compose.ui.Alignment
2122
import androidx.compose.ui.Modifier
2223
import androidx.compose.ui.res.stringResource
2324
import androidx.compose.ui.text.AnnotatedString
25+
import androidx.compose.ui.tooling.preview.PreviewParameter
2426
import androidx.compose.ui.unit.dp
2527
import io.element.android.compound.tokens.generated.CompoundIcons
2628
import io.element.android.features.linknewdevice.impl.R
@@ -30,6 +32,7 @@ import io.element.android.libraries.designsystem.components.BigIcon
3032
import io.element.android.libraries.designsystem.preview.ElementPreview
3133
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
3234
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
35+
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
3336
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
3437
import io.element.android.libraries.qrcode.QrCodeImage
3538
import kotlinx.collections.immutable.persistentListOf
@@ -40,7 +43,7 @@ import kotlinx.collections.immutable.persistentListOf
4043
*/
4144
@Composable
4245
fun ShowQrCodeView(
43-
data: String,
46+
state: ShowQrCodeState,
4447
onBackClick: () -> Unit,
4548
modifier: Modifier = Modifier,
4649
) {
@@ -55,11 +58,24 @@ fun ShowQrCodeView(
5558
Modifier.fillMaxWidth(),
5659
horizontalAlignment = Alignment.CenterHorizontally,
5760
) {
58-
QrCodeImage(
59-
data = data,
60-
modifier = Modifier
61-
.size(220.dp)
62-
)
61+
when (val str = state.data.dataOrNull()) {
62+
null -> {
63+
Box(
64+
modifier = Modifier
65+
.size(220.dp),
66+
contentAlignment = Alignment.Center,
67+
) {
68+
CircularProgressIndicator()
69+
}
70+
}
71+
else -> {
72+
QrCodeImage(
73+
data = str,
74+
modifier = Modifier
75+
.size(220.dp)
76+
)
77+
}
78+
}
6379
Spacer(modifier = Modifier.height(32.dp))
6480
NumberedListOrganism(
6581
modifier = Modifier.fillMaxSize(),
@@ -81,9 +97,11 @@ fun ShowQrCodeView(
8197

8298
@PreviewsDayNight
8399
@Composable
84-
internal fun ShowQrCodeViewPreview() = ElementPreview {
100+
internal fun ShowQrCodeViewPreview(
101+
@PreviewParameter(ShowQrCodeStateProvider::class) state: ShowQrCodeState,
102+
) = ElementPreview {
85103
ShowQrCodeView(
86-
data = "DATA",
104+
state = state,
87105
onBackClick = { },
88106
)
89107
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ sealed interface LinkMobileStep {
1818
data object Uninitialized : LinkMobileStep
1919
data object Starting : LinkMobileStep
2020
data class QrReady(val data: String) : LinkMobileStep
21+
data object QrRotating : LinkMobileStep
2122
data class WaitingForAuth(val verificationUri: String) : LinkMobileStep
2223
data class QrScanned(val checkCodeSender: CheckCodeSender) : LinkMobileStep
2324
data class Error(val errorType: ErrorType) : LinkMobileStep

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,14 @@ class RustLinkMobileHandler(
5353
_linkMobileStep.emit(LinkMobileStep.Done)
5454
} catch (e: HumanQrGrantLoginException) {
5555
Timber.tag(tag.value).w(e, "Error during QR login grant")
56-
_linkMobileStep.emit(LinkMobileStep.Error(e.map()))
56+
// Catch timeout here?
57+
if (_linkMobileStep.value is LinkMobileStep.QrReady
58+
&& e is HumanQrGrantLoginException.NotFound) {
59+
Timber.tag(tag.value).d("Emit QrRotating due to HumanQrGrantLoginException.NotFound")
60+
_linkMobileStep.emit(LinkMobileStep.QrRotating)
61+
} else {
62+
_linkMobileStep.emit(LinkMobileStep.Error(e.map()))
63+
}
5764
}
5865
}
5966

0 commit comments

Comments
 (0)