Merge pull request #6817 from element-hq/feature/bma/automaticRetry
[Link new device] Rotate QrCode instead of showing an error
This commit is contained in:
commit
535af93d85
15 changed files with 346 additions and 13 deletions
|
|
@ -144,8 +144,14 @@ class LinkNewDeviceFlowNode(
|
||||||
navigateToError(linkMobileStep.errorType)
|
navigateToError(linkMobileStep.errorType)
|
||||||
}
|
}
|
||||||
is LinkMobileStep.QrReady -> {
|
is LinkMobileStep.QrReady -> {
|
||||||
// The QrCode is ready, navigate to its display
|
// The QrCode is ready, navigate to its display, if not already there
|
||||||
backstack.push(NavTarget.MobileShowQrCode(linkMobileStep.data))
|
val navTarget = backstack.elements.value.last().key.navTarget
|
||||||
|
if (navTarget !is NavTarget.MobileShowQrCode) {
|
||||||
|
backstack.push(NavTarget.MobileShowQrCode(linkMobileStep.data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinkMobileStep.QrRotating -> {
|
||||||
|
// This step is handled in ShowQrCodePresenter
|
||||||
}
|
}
|
||||||
is LinkMobileStep.QrScanned -> {
|
is LinkMobileStep.QrScanned -> {
|
||||||
backstack.replace(NavTarget.MobileEnterNumber)
|
backstack.replace(NavTarget.MobileEnterNumber)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.linknewdevice.ErrorType
|
||||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
|
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
|
||||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
|
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
|
||||||
import io.element.android.libraries.matrix.api.logs.LoggerTags
|
import io.element.android.libraries.matrix.api.logs.LoggerTags
|
||||||
|
|
@ -65,4 +66,15 @@ class LinkNewMobileHandler(
|
||||||
linkMobileStepFlow.emit(LinkMobileStep.Uninitialized)
|
linkMobileStepFlow.emit(LinkMobileStep.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun rotateQrCode() {
|
||||||
|
createAndStartNewHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTooManyRotation() {
|
||||||
|
reset()
|
||||||
|
sessionScope.launch {
|
||||||
|
linkMobileStepFlow.emit(LinkMobileStep.Error(ErrorType.Expired("Too many QR code rotations")))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import io.element.android.libraries.di.SessionScope
|
||||||
class ShowQrCodeNode(
|
class ShowQrCodeNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
showQrCodePresenterFactory: ShowQrCodePresenter.Factory,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
class Inputs(
|
class Inputs(
|
||||||
val data: String,
|
val data: String,
|
||||||
|
|
@ -36,11 +37,15 @@ class ShowQrCodeNode(
|
||||||
|
|
||||||
private val inputs: Inputs = inputs<Inputs>()
|
private val inputs: Inputs = inputs<Inputs>()
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
|
private val showQrCodePresenter: ShowQrCodePresenter = showQrCodePresenterFactory.create(
|
||||||
|
initialData = inputs.data,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = showQrCodePresenter.present()
|
||||||
ShowQrCodeView(
|
ShowQrCodeView(
|
||||||
data = inputs.data,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackClick = callback::navigateBack,
|
onBackClick = callback::navigateBack,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* 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.qrcode
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import dev.zacsweers.metro.Assisted
|
||||||
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
|
import dev.zacsweers.metro.AssistedInject
|
||||||
|
import io.element.android.features.linknewdevice.impl.LinkNewMobileHandler
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
|
||||||
|
import io.element.android.libraries.matrix.api.logs.LoggerTags
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private val tag = LoggerTag("ShowQrCodePresenter", LoggerTags.linkNewDevice)
|
||||||
|
|
||||||
|
@AssistedInject
|
||||||
|
class ShowQrCodePresenter(
|
||||||
|
@Assisted private val initialData: String,
|
||||||
|
private val linkNewMobileHandler: LinkNewMobileHandler,
|
||||||
|
) : Presenter<ShowQrCodeState> {
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialData: String): ShowQrCodePresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadingJob: Job? = null
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): ShowQrCodeState {
|
||||||
|
var qrCodeRotationCounter by remember { mutableIntStateOf(MAX_QR_CODE_ROTATION) }
|
||||||
|
val state by produceState(
|
||||||
|
initialValue = ShowQrCodeState(
|
||||||
|
data = AsyncData.Success(initialData),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
linkNewMobileHandler.stepFlow.collect { step ->
|
||||||
|
when (step) {
|
||||||
|
is LinkMobileStep.QrReady -> {
|
||||||
|
loadingJob?.cancel()
|
||||||
|
value = ShowQrCodeState(
|
||||||
|
data = AsyncData.Success(step.data),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is LinkMobileStep.QrRotating -> {
|
||||||
|
if (qrCodeRotationCounter-- > 0) {
|
||||||
|
Timber.tag(tag.value).d("Rotating QrCode")
|
||||||
|
linkNewMobileHandler.rotateQrCode()
|
||||||
|
// Ensure that outdated data is not rendered too long while rotating QR code
|
||||||
|
loadingJob = launch {
|
||||||
|
delay(1000)
|
||||||
|
value = ShowQrCodeState(
|
||||||
|
data = AsyncData.Loading(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.tag(tag.value).w("Max QR code rotation reached, not rotating anymore")
|
||||||
|
linkNewMobileHandler.onTooManyRotation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_QR_CODE_ROTATION = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* 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.qrcode
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
|
data class ShowQrCodeState(
|
||||||
|
val data: AsyncData<String>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.qrcode
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
|
class ShowQrCodeStateProvider : PreviewParameterProvider<ShowQrCodeState> {
|
||||||
|
override val values: Sequence<ShowQrCodeState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aShowQrCodeState(),
|
||||||
|
aShowQrCodeState(
|
||||||
|
data = AsyncData.Loading(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun aShowQrCodeState(
|
||||||
|
data: AsyncData<String> = AsyncData.Success("DATA"),
|
||||||
|
) = ShowQrCodeState(
|
||||||
|
data = data,
|
||||||
|
)
|
||||||
|
|
@ -9,6 +9,12 @@
|
||||||
|
|
||||||
package io.element.android.features.linknewdevice.impl.screens.qrcode
|
package io.element.android.features.linknewdevice.impl.screens.qrcode
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
|
@ -21,6 +27,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.linknewdevice.impl.R
|
import io.element.android.features.linknewdevice.impl.R
|
||||||
|
|
@ -30,6 +37,7 @@ import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||||
import io.element.android.libraries.qrcode.QrCodeImage
|
import io.element.android.libraries.qrcode.QrCodeImage
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -38,9 +46,10 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
* QrCode display screen:
|
* QrCode display screen:
|
||||||
* https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=2027-23617
|
* https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=2027-23617
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ShowQrCodeView(
|
fun ShowQrCodeView(
|
||||||
data: String,
|
state: ShowQrCodeState,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -55,11 +64,17 @@ fun ShowQrCodeView(
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
QrCodeImage(
|
AnimatedContent(
|
||||||
data = data,
|
modifier = Modifier.size(220.dp),
|
||||||
modifier = Modifier
|
targetState = state.data.dataOrNull(),
|
||||||
.size(220.dp)
|
transitionSpec = {
|
||||||
)
|
fadeIn().togetherWith(fadeOut())
|
||||||
|
}
|
||||||
|
) { data ->
|
||||||
|
QrCodeOrLoading(
|
||||||
|
data = data,
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
NumberedListOrganism(
|
NumberedListOrganism(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
@ -79,11 +94,33 @@ fun ShowQrCodeView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun QrCodeOrLoading(
|
||||||
|
data: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (data == null) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier,
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QrCodeImage(
|
||||||
|
modifier = modifier,
|
||||||
|
data = data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ShowQrCodeViewPreview() = ElementPreview {
|
internal fun ShowQrCodeViewPreview(
|
||||||
|
@PreviewParameter(ShowQrCodeStateProvider::class) state: ShowQrCodeState,
|
||||||
|
) = ElementPreview {
|
||||||
ShowQrCodeView(
|
ShowQrCodeView(
|
||||||
data = "DATA",
|
state = state,
|
||||||
onBackClick = { },
|
onBackClick = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package io.element.android.features.linknewdevice.impl.screens.qrcode
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.linknewdevice.impl.LinkNewMobileHandler
|
||||||
|
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
|
||||||
|
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.linknewdevice.FakeLinkMobileHandler
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ShowQrCodePresenterTest {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
createPresenter().test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.data.dataOrNull()).isEqualTo("DATA")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when handler emits QrRotating, the presenter requests to rotate the QrCode`() = runTest {
|
||||||
|
val linkMobileHandler = FakeLinkMobileHandler(
|
||||||
|
startResult = {},
|
||||||
|
)
|
||||||
|
val createLinkMobileHandlerResult = lambdaRecorder<Result<LinkMobileHandler>> {
|
||||||
|
Result.success(linkMobileHandler)
|
||||||
|
}
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
|
createLinkMobileHandlerResult = createLinkMobileHandlerResult,
|
||||||
|
)
|
||||||
|
val linkNewMobileHandler = LinkNewMobileHandler(matrixClient)
|
||||||
|
linkNewMobileHandler.createAndStartNewHandler()
|
||||||
|
createPresenter(
|
||||||
|
linkNewMobileHandler = linkNewMobileHandler,
|
||||||
|
).test {
|
||||||
|
awaitItem()
|
||||||
|
linkMobileHandler.emitStep(
|
||||||
|
LinkMobileStep.QrRotating
|
||||||
|
)
|
||||||
|
runCurrent()
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.data.isLoading()).isTrue()
|
||||||
|
createLinkMobileHandlerResult.assertions().isCalledExactly(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when handler emits QrRotating, the presenter requests to rotate the QrCode and the code is rotated`() = runTest {
|
||||||
|
val linkMobileHandler = FakeLinkMobileHandler(
|
||||||
|
startResult = {},
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
|
createLinkMobileHandlerResult = { Result.success(linkMobileHandler) },
|
||||||
|
)
|
||||||
|
val linkNewMobileHandler = LinkNewMobileHandler(matrixClient)
|
||||||
|
linkNewMobileHandler.createAndStartNewHandler()
|
||||||
|
createPresenter(
|
||||||
|
linkNewMobileHandler = linkNewMobileHandler,
|
||||||
|
).test {
|
||||||
|
awaitItem()
|
||||||
|
linkMobileHandler.emitStep(
|
||||||
|
LinkMobileStep.QrRotating
|
||||||
|
)
|
||||||
|
runCurrent()
|
||||||
|
linkMobileHandler.emitStep(
|
||||||
|
LinkMobileStep.QrReady("DATA2")
|
||||||
|
)
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.data.dataOrNull()).isEqualTo("DATA2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPresenter(
|
||||||
|
linkNewMobileHandler: LinkNewMobileHandler = LinkNewMobileHandler(FakeMatrixClient()),
|
||||||
|
) = ShowQrCodePresenter(
|
||||||
|
initialData = "DATA",
|
||||||
|
linkNewMobileHandler = linkNewMobileHandler,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,7 @@ class ShowQrCodeViewTest {
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
ShowQrCodeView(
|
ShowQrCodeView(
|
||||||
data = "DATA",
|
state = aShowQrCodeState(),
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ sealed interface LinkMobileStep {
|
||||||
data object Uninitialized : LinkMobileStep
|
data object Uninitialized : LinkMobileStep
|
||||||
data object Starting : LinkMobileStep
|
data object Starting : LinkMobileStep
|
||||||
data class QrReady(val data: String) : LinkMobileStep
|
data class QrReady(val data: String) : LinkMobileStep
|
||||||
|
data object QrRotating : LinkMobileStep
|
||||||
data class WaitingForAuth(val verificationUri: String) : LinkMobileStep
|
data class WaitingForAuth(val verificationUri: String) : LinkMobileStep
|
||||||
data class QrScanned(val checkCodeSender: CheckCodeSender) : LinkMobileStep
|
data class QrScanned(val checkCodeSender: CheckCodeSender) : LinkMobileStep
|
||||||
data class Error(val errorType: ErrorType) : LinkMobileStep
|
data class Error(val errorType: ErrorType) : LinkMobileStep
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,15 @@ class RustLinkMobileHandler(
|
||||||
)
|
)
|
||||||
// We emit Done in case the progress listener was deallocated before generate() sent the Done
|
// We emit Done in case the progress listener was deallocated before generate() sent the Done
|
||||||
_linkMobileStep.emit(LinkMobileStep.Done)
|
_linkMobileStep.emit(LinkMobileStep.Done)
|
||||||
|
} catch (e: HumanQrGrantLoginException.NotFound) {
|
||||||
|
Timber.tag(tag.value).w(e, "Error during QR login grant")
|
||||||
|
// Catch timeout here?
|
||||||
|
if (_linkMobileStep.value is LinkMobileStep.QrReady) {
|
||||||
|
Timber.tag(tag.value).d("Emit QrRotating due to HumanQrGrantLoginException.NotFound")
|
||||||
|
_linkMobileStep.emit(LinkMobileStep.QrRotating)
|
||||||
|
} else {
|
||||||
|
_linkMobileStep.emit(LinkMobileStep.Error(e.map()))
|
||||||
|
}
|
||||||
} catch (e: HumanQrGrantLoginException) {
|
} catch (e: HumanQrGrantLoginException) {
|
||||||
Timber.tag(tag.value).w(e, "Error during QR login grant")
|
Timber.tag(tag.value).w(e, "Error during QR login grant")
|
||||||
_linkMobileStep.emit(LinkMobileStep.Error(e.map()))
|
_linkMobileStep.emit(LinkMobileStep.Error(e.map()))
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,35 @@ class RustLinkMobileHandlerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when start throws HumanQrGrantLoginException_NotFound when in state QrReady, the handler emits QrRotating step`() = runTest {
|
||||||
|
val completable = CompletableDeferred<Unit>()
|
||||||
|
val handler = FakeFfiGrantLoginWithQrCodeHandler(
|
||||||
|
generateResult = {
|
||||||
|
completable.await()
|
||||||
|
throw HumanQrGrantLoginException.NotFound("Timeout")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val sut = createRustLinkMobileHandler(
|
||||||
|
handler,
|
||||||
|
)
|
||||||
|
sut.linkMobileStep.test {
|
||||||
|
val initialItem = awaitItem()
|
||||||
|
assertThat(initialItem).isEqualTo(LinkMobileStep.Uninitialized)
|
||||||
|
backgroundScope.launch {
|
||||||
|
sut.start()
|
||||||
|
}
|
||||||
|
runCurrent()
|
||||||
|
handler.emitGenerateProgress(GrantGeneratedQrLoginProgress.QrReady(FakeFfiQrCodeData(toBytesResult = { QR_CODE_DATA_RECIPROCATE })))
|
||||||
|
val readyState = awaitItem()
|
||||||
|
assertThat(readyState).isInstanceOf(LinkMobileStep.QrReady::class.java)
|
||||||
|
// generate returns, error is emitted
|
||||||
|
completable.complete(Unit)
|
||||||
|
val qrRotatingState = awaitItem()
|
||||||
|
assertThat(qrRotatingState).isEqualTo(LinkMobileStep.QrRotating)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun TestScope.createRustLinkMobileHandler(
|
private fun TestScope.createRustLinkMobileHandler(
|
||||||
handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(),
|
handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(),
|
||||||
) = RustLinkMobileHandler(
|
) = RustLinkMobileHandler(
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,8 @@ private fun BitMatrix.toBitmap(
|
||||||
@Composable
|
@Composable
|
||||||
fun QrCodeImage(
|
fun QrCodeImage(
|
||||||
data: String,
|
data: String,
|
||||||
forceMaxBrightness: Boolean = true,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
forceMaxBrightness: Boolean = true,
|
||||||
) {
|
) {
|
||||||
if (forceMaxBrightness) {
|
if (forceMaxBrightness) {
|
||||||
ForceMaxBrightness()
|
ForceMaxBrightness()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:48cafd6b98791b64e4cc6a16c602f17e3a886659698c3c93bc4acaf875337e0f
|
||||||
|
size 31102
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3f6f8d1282ae47a5240aec2b2c63a137b8e3e25ec8b746cf2d82d0fa7e0c0a34
|
||||||
|
size 30287
|
||||||
Loading…
Add table
Add a link
Reference in a new issue