Merge pull request #6680 from element-hq/feature/bma/qrCodeFix
[Link new device] Add missing screen to render digits that the user has to type on the other device
This commit is contained in:
commit
97e700eb18
7 changed files with 214 additions and 2 deletions
|
|
@ -27,6 +27,7 @@ import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
|
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
|
||||||
|
import io.element.android.features.linknewdevice.impl.screens.confirmation.CodeConfirmationNode
|
||||||
import io.element.android.features.linknewdevice.impl.screens.desktop.DesktopNoticeNode
|
import io.element.android.features.linknewdevice.impl.screens.desktop.DesktopNoticeNode
|
||||||
import io.element.android.features.linknewdevice.impl.screens.error.ErrorNode
|
import io.element.android.features.linknewdevice.impl.screens.error.ErrorNode
|
||||||
import io.element.android.features.linknewdevice.impl.screens.error.ErrorScreenType
|
import io.element.android.features.linknewdevice.impl.screens.error.ErrorScreenType
|
||||||
|
|
@ -107,6 +108,11 @@ class LinkNewDeviceFlowNode(
|
||||||
val data: String,
|
val data: String,
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class CodeConfirmation(
|
||||||
|
val code: String,
|
||||||
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object MobileEnterNumber : NavTarget
|
data object MobileEnterNumber : NavTarget
|
||||||
|
|
||||||
|
|
@ -166,7 +172,9 @@ class LinkNewDeviceFlowNode(
|
||||||
is LinkDesktopStep.Error -> {
|
is LinkDesktopStep.Error -> {
|
||||||
navigateToError(linkDesktopStep.errorType)
|
navigateToError(linkDesktopStep.errorType)
|
||||||
}
|
}
|
||||||
is LinkDesktopStep.EstablishingSecureChannel -> Unit
|
is LinkDesktopStep.EstablishingSecureChannel -> {
|
||||||
|
backstack.push(NavTarget.CodeConfirmation(linkDesktopStep.checkCodeString))
|
||||||
|
}
|
||||||
is LinkDesktopStep.InvalidQrCode -> {
|
is LinkDesktopStep.InvalidQrCode -> {
|
||||||
// This error will be handled by the ScanQrCodeNode
|
// This error will be handled by the ScanQrCodeNode
|
||||||
}
|
}
|
||||||
|
|
@ -250,6 +258,18 @@ class LinkNewDeviceFlowNode(
|
||||||
}
|
}
|
||||||
createNode<EnterNumberNode>(buildContext, listOf(callback))
|
createNode<EnterNumberNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
|
is NavTarget.CodeConfirmation -> {
|
||||||
|
val callback = object : CodeConfirmationNode.Callback {
|
||||||
|
override fun onCancel() {
|
||||||
|
// Push error
|
||||||
|
backstack.push(NavTarget.Error(ErrorScreenType.Cancelled))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val inputs = CodeConfirmationNode.Inputs(
|
||||||
|
code = navTarget.code,
|
||||||
|
)
|
||||||
|
createNode<CodeConfirmationNode>(buildContext, listOf(inputs, callback))
|
||||||
|
}
|
||||||
is NavTarget.MobileShowQrCode -> {
|
is NavTarget.MobileShowQrCode -> {
|
||||||
val callback = object : ShowQrCodeNode.Callback {
|
val callback = object : ShowQrCodeNode.Callback {
|
||||||
override fun navigateBack() {
|
override fun navigateBack() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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.features.linknewdevice.impl.screens.confirmation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import dev.zacsweers.metro.Assisted
|
||||||
|
import dev.zacsweers.metro.AssistedInject
|
||||||
|
import io.element.android.annotations.ContributesNode
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.architecture.callback
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
@AssistedInject
|
||||||
|
class CodeConfirmationNode(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
) : Node(buildContext = buildContext, plugins = plugins) {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Inputs(
|
||||||
|
val code: String,
|
||||||
|
) : NodeInputs
|
||||||
|
|
||||||
|
private val callback: Callback = callback()
|
||||||
|
private val input = inputs<Inputs>()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
CodeConfirmationView(
|
||||||
|
code = input.code,
|
||||||
|
onCancel = callback::onCancel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* 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.features.linknewdevice.impl.screens.confirmation
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.linknewdevice.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||||
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CodeConfirmationView(
|
||||||
|
code: String,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
BackHandler(onBack = onCancel)
|
||||||
|
FlowStepPage(
|
||||||
|
modifier = modifier,
|
||||||
|
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
|
||||||
|
title = stringResource(R.string.screen_qr_code_login_device_code_title),
|
||||||
|
subTitle = stringResource(R.string.screen_qr_code_login_device_code_subtitle),
|
||||||
|
content = { Content(code = code) },
|
||||||
|
buttons = { Buttons(onCancel = onCancel) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Content(code: String) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Digits(code = code)
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
WaitingForOtherDevice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun Digits(code: String) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
code.forEach {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 6.dp, vertical = 4.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(ElementTheme.colors.bgActionSecondaryPressed)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 17.dp),
|
||||||
|
text = it.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WaitingForOtherDevice() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.padding(2.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_qr_code_login_verify_code_loading),
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Buttons(
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = stringResource(CommonStrings.action_cancel),
|
||||||
|
onClick = onCancel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun CodeConfirmationViewPreview() {
|
||||||
|
ElementPreview {
|
||||||
|
CodeConfirmationView(
|
||||||
|
code = "67",
|
||||||
|
onCancel = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,8 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesn’t work, sign in manually"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesn’t work, sign in manually"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"You’ll be asked to enter the two digits shown on this device."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string>
|
||||||
<string name="screen_qr_code_login_error_cancelled_subtitle">"The sign in was cancelled on the other device."</string>
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"The sign in was cancelled on the other device."</string>
|
||||||
<string name="screen_qr_code_login_error_cancelled_title">"Sign in request cancelled"</string>
|
<string name="screen_qr_code_login_error_cancelled_title">"Sign in request cancelled"</string>
|
||||||
<string name="screen_qr_code_login_error_declined_subtitle">"The sign in was declined on the other device."</string>
|
<string name="screen_qr_code_login_error_declined_subtitle">"The sign in was declined on the other device."</string>
|
||||||
|
|
@ -54,4 +56,5 @@ Try signing in manually, or scan the QR code with another device."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for %1$s to use your device’s camera in order to continue."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for %1$s to use your device’s camera in order to continue."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string>
|
||||||
<string name="screen_qr_code_login_unknown_error_description">"An unexpected error occurred. Please try again."</string>
|
<string name="screen_qr_code_login_unknown_error_description">"An unexpected error occurred. Please try again."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Waiting for your other device"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c8dab1dc964cea9a76dc7130d8ac2bfe5d3c866fd6d8d969101eb50e828775d3
|
||||||
|
size 31915
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ec67e3fef25c57331cb2425f8fc46a733e16a98c9945d0a4e5b950843c42fa34
|
||||||
|
size 31087
|
||||||
|
|
@ -163,7 +163,9 @@
|
||||||
"screen_qr_code_login_connection_note_secure_state.*",
|
"screen_qr_code_login_connection_note_secure_state.*",
|
||||||
"screen_qr_code_login_unknown_error_description",
|
"screen_qr_code_login_unknown_error_description",
|
||||||
"screen_qr_code_login_invalid_scan_state_.*",
|
"screen_qr_code_login_invalid_scan_state_.*",
|
||||||
"screen_qr_code_login_no_camera_permission_state_.*"
|
"screen_qr_code_login_no_camera_permission_state_.*",
|
||||||
|
"screen_qr_code_login_device_code_.*",
|
||||||
|
"screen_qr_code_login_verify_code_loading"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue