Sign in with QR code (#2793)
* Add QR code login. * Add FF to disable it in release mode. * Force portrait orientation on the login flow. * Create `NumberedList` UI components. * Improve camera permission dialog. * Make nodes in qrcode feature use `QrCodeLoginScope` instead of `AppScope` * Bump SDK version. * Fix maestro tests --------- Co-authored-by: Benoit Marty <benoit@matrix.org> Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
e0c55ff4c8
commit
35702c04e9
253 changed files with 4421 additions and 326 deletions
|
|
@ -32,7 +32,7 @@ class DefaultLoginEntryPoint @Inject constructor() : LoginEntryPoint {
|
|||
|
||||
return object : LoginEntryPoint.NodeBuilder {
|
||||
override fun params(params: LoginEntryPoint.Params): LoginEntryPoint.NodeBuilder {
|
||||
plugins += LoginFlowNode.Inputs(isAccountCreation = params.isAccountCreation)
|
||||
plugins += LoginFlowNode.Inputs(flowType = params.flowType)
|
||||
return this
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,12 +35,14 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.login.api.LoginFlowType
|
||||
import io.element.android.features.login.api.oidc.OidcAction
|
||||
import io.element.android.features.login.api.oidc.OidcActionFlow
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker
|
||||
import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler
|
||||
import io.element.android.features.login.impl.oidc.webview.OidcNode
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
|
||||
import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
|
||||
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
|
||||
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
|
||||
|
|
@ -69,7 +71,7 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
private val oidcActionFlow: OidcActionFlow,
|
||||
) : BaseFlowNode<LoginFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.ConfirmAccountProvider,
|
||||
initialElement = NavTarget.Root,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
|
|
@ -79,7 +81,7 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
private var darkTheme: Boolean = false
|
||||
|
||||
data class Inputs(
|
||||
val isAccountCreation: Boolean,
|
||||
val flowType: LoginFlowType,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
|
@ -107,6 +109,9 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Root : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ConfirmAccountProvider : NavTarget
|
||||
|
||||
|
|
@ -128,9 +133,16 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Root -> {
|
||||
if (inputs.flowType == LoginFlowType.SIGN_IN_QR_CODE) {
|
||||
createNode<QrCodeLoginFlowNode>(buildContext)
|
||||
} else {
|
||||
resolve(NavTarget.ConfirmAccountProvider, buildContext)
|
||||
}
|
||||
}
|
||||
NavTarget.ConfirmAccountProvider -> {
|
||||
val inputs = ConfirmAccountProviderNode.Inputs(
|
||||
isAccountCreation = inputs.isAccountCreation
|
||||
isAccountCreation = inputs.flowType == LoginFlowType.SIGN_UP,
|
||||
)
|
||||
val callback = object : ConfirmAccountProviderNode.Callback {
|
||||
override fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
|
||||
|
||||
@ContributesTo(QrCodeLoginScope::class)
|
||||
interface QrCodeLoginBindings {
|
||||
fun qrCodeLoginManager(): QrCodeLoginManager
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import com.squareup.anvil.annotations.MergeSubcomponent
|
||||
import dagger.Subcomponent
|
||||
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
|
||||
@SingleIn(QrCodeLoginScope::class)
|
||||
@MergeSubcomponent(QrCodeLoginScope::class)
|
||||
interface QrCodeLoginComponent : NodeFactoriesBindings {
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
fun build(): QrCodeLoginComponent
|
||||
}
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface ParentBindings {
|
||||
fun qrCodeLoginComponentBuilder(): Builder
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.di
|
||||
|
||||
abstract class QrCodeLoginScope private constructor()
|
||||
|
|
@ -89,6 +89,6 @@ fun OidcView(
|
|||
internal fun OidcViewPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) = ElementPreview {
|
||||
OidcView(
|
||||
state = state,
|
||||
onNavigateBack = { },
|
||||
onNavigateBack = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.qrcode
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@SingleIn(QrCodeLoginScope::class)
|
||||
@ContributesBinding(QrCodeLoginScope::class)
|
||||
class DefaultQrCodeLoginManager @Inject constructor(
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
) : QrCodeLoginManager {
|
||||
private val _currentLoginStep = MutableStateFlow<QrCodeLoginStep>(QrCodeLoginStep.Uninitialized)
|
||||
override val currentLoginStep: StateFlow<QrCodeLoginStep> = _currentLoginStep
|
||||
|
||||
override suspend fun authenticate(qrCodeLoginData: MatrixQrCodeLoginData): Result<SessionId> {
|
||||
reset()
|
||||
|
||||
return authenticationService.loginWithQrCode(qrCodeLoginData) { step ->
|
||||
_currentLoginStep.value = step
|
||||
}.onFailure { throwable ->
|
||||
if (throwable is QrLoginException) {
|
||||
_currentLoginStep.value = QrCodeLoginStep.Failed(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
_currentLoginStep.value = QrCodeLoginStep.Uninitialized
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.qrcode
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.newRoot
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginBindings
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginComponent
|
||||
import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationNode
|
||||
import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationStep
|
||||
import io.element.android.features.login.impl.screens.qrcode.error.QrCodeErrorNode
|
||||
import io.element.android.features.login.impl.screens.qrcode.intro.QrCodeIntroNode
|
||||
import io.element.android.features.login.impl.screens.qrcode.scan.QrCodeScanNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class QrCodeLoginFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
qrCodeLoginComponentBuilder: QrCodeLoginComponent.Builder,
|
||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : BaseFlowNode<QrCodeLoginFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Initial,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
), DaggerComponentOwner {
|
||||
private var authenticationJob: Job? = null
|
||||
|
||||
override val daggerComponent = qrCodeLoginComponentBuilder.build()
|
||||
private val qrCodeLoginManager by lazy { bindings<QrCodeLoginBindings>().qrCodeLoginManager() }
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Initial : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object QrCodeScan : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class QrCodeConfirmation(val step: QrCodeConfirmationStep) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class Error(val errorType: QrCodeErrorScreenType) : NavTarget
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
|
||||
observeLoginStep()
|
||||
}
|
||||
|
||||
fun isLoginInProgress(): Boolean {
|
||||
return authenticationJob?.isActive == true
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun observeLoginStep() {
|
||||
lifecycleScope.launch {
|
||||
qrCodeLoginManager.currentLoginStep
|
||||
.collect { step ->
|
||||
when (step) {
|
||||
is QrCodeLoginStep.EstablishingSecureChannel -> {
|
||||
backstack.replace(NavTarget.QrCodeConfirmation(QrCodeConfirmationStep.DisplayCheckCode(step.checkCode)))
|
||||
}
|
||||
is QrCodeLoginStep.WaitingForToken -> {
|
||||
backstack.replace(NavTarget.QrCodeConfirmation(QrCodeConfirmationStep.DisplayVerificationCode(step.userCode)))
|
||||
}
|
||||
is QrCodeLoginStep.Failed -> {
|
||||
when (val error = step.error) {
|
||||
is QrLoginException.OtherDeviceNotSignedIn -> {
|
||||
// Do nothing here, it'll be handled in the scan QR screen
|
||||
}
|
||||
is QrLoginException.Cancelled -> {
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Cancelled))
|
||||
}
|
||||
is QrLoginException.Expired -> {
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Expired))
|
||||
}
|
||||
is QrLoginException.Declined -> {
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.Declined))
|
||||
}
|
||||
is QrLoginException.ConnectionInsecure -> {
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.InsecureChannelDetected))
|
||||
}
|
||||
is QrLoginException.LinkingNotSupported -> {
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.ProtocolNotSupported))
|
||||
}
|
||||
is QrLoginException.SlidingSyncNotAvailable -> {
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.SlidingSyncNotAvailable))
|
||||
}
|
||||
is QrLoginException.OidcMetadataInvalid -> {
|
||||
Timber.e(error, "OIDC metadata is invalid")
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
||||
}
|
||||
else -> {
|
||||
Timber.e(error, "Unknown error found")
|
||||
backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
is NavTarget.Initial -> {
|
||||
val callback = object : QrCodeIntroNode.Callback {
|
||||
override fun onCancelClicked() {
|
||||
navigateUp()
|
||||
}
|
||||
|
||||
override fun onContinue() {
|
||||
backstack.push(NavTarget.QrCodeScan)
|
||||
}
|
||||
}
|
||||
createNode<QrCodeIntroNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
is NavTarget.QrCodeScan -> {
|
||||
val callback = object : QrCodeScanNode.Callback {
|
||||
override fun onScannedCode(qrCodeLoginData: MatrixQrCodeLoginData) {
|
||||
lifecycleScope.startAuthentication(qrCodeLoginData)
|
||||
}
|
||||
|
||||
override fun onCancelClicked() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
createNode<QrCodeScanNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
is NavTarget.QrCodeConfirmation -> {
|
||||
val callback = object : QrCodeConfirmationNode.Callback {
|
||||
override fun onCancel() = reset()
|
||||
}
|
||||
createNode<QrCodeConfirmationNode>(buildContext, plugins = listOf(navTarget.step, callback))
|
||||
}
|
||||
is NavTarget.Error -> {
|
||||
val callback = object : QrCodeErrorNode.Callback {
|
||||
override fun onRetry() = reset()
|
||||
}
|
||||
createNode<QrCodeErrorNode>(buildContext, plugins = listOf(navTarget.errorType, callback))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun reset() {
|
||||
authenticationJob?.cancel()
|
||||
authenticationJob = null
|
||||
qrCodeLoginManager.reset()
|
||||
backstack.newRoot(NavTarget.Initial)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun CoroutineScope.startAuthentication(qrCodeLoginData: MatrixQrCodeLoginData) {
|
||||
authenticationJob = launch(coroutineDispatchers.main) {
|
||||
qrCodeLoginManager.authenticate(qrCodeLoginData)
|
||||
.onSuccess {
|
||||
defaultLoginUserStory.setLoginFlowIsDone(true)
|
||||
authenticationJob = null
|
||||
}
|
||||
.onFailure { throwable ->
|
||||
Timber.e(throwable, "QR code authentication failed")
|
||||
authenticationJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface QrCodeErrorScreenType : NodeInputs, Parcelable {
|
||||
@Parcelize
|
||||
data object Cancelled : QrCodeErrorScreenType
|
||||
|
||||
@Parcelize
|
||||
data object Expired : QrCodeErrorScreenType
|
||||
|
||||
@Parcelize
|
||||
data object InsecureChannelDetected : QrCodeErrorScreenType
|
||||
|
||||
@Parcelize
|
||||
data object Declined : QrCodeErrorScreenType
|
||||
|
||||
@Parcelize
|
||||
data object ProtocolNotSupported : QrCodeErrorScreenType
|
||||
|
||||
@Parcelize
|
||||
data object SlidingSyncNotAvailable : QrCodeErrorScreenType
|
||||
|
||||
@Parcelize
|
||||
data object UnknownError : QrCodeErrorScreenType
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.qrcode
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Helper to handle the QR code login flow after the QR code data has been provided.
|
||||
*/
|
||||
interface QrCodeLoginManager {
|
||||
/**
|
||||
* The current QR code login step.
|
||||
*/
|
||||
val currentLoginStep: StateFlow<QrCodeLoginStep>
|
||||
|
||||
/**
|
||||
* Authenticate using the provided [qrCodeLoginData].
|
||||
* @param qrCodeLoginData the QR code login data from the scanned QR code.
|
||||
* @return the logged in [SessionId] if the authentication was successful or a failure result.
|
||||
*/
|
||||
suspend fun authenticate(qrCodeLoginData: MatrixQrCodeLoginData): Result<SessionId>
|
||||
|
||||
fun reset()
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.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 com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginScope
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
|
||||
@ContributesNode(QrCodeLoginScope::class)
|
||||
class QrCodeConfirmationNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : Node(buildContext = buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onCancel()
|
||||
}
|
||||
|
||||
private val step = inputs<QrCodeConfirmationStep>()
|
||||
|
||||
private fun onCancel() {
|
||||
plugins<Callback>().forEach { it.onCancel() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
QrCodeConfirmationView(
|
||||
step = step,
|
||||
onCancel = ::onCancel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.confirmation
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Immutable
|
||||
sealed interface QrCodeConfirmationStep : NodeInputs, Parcelable {
|
||||
@Parcelize
|
||||
data class DisplayCheckCode(val code: String) : QrCodeConfirmationStep
|
||||
|
||||
@Parcelize
|
||||
data class DisplayVerificationCode(val code: String) : QrCodeConfirmationStep
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.confirmation
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
class QrCodeConfirmationStepPreviewProvider : PreviewParameterProvider<QrCodeConfirmationStep> {
|
||||
override val values: Sequence<QrCodeConfirmationStep>
|
||||
get() = sequenceOf(
|
||||
QrCodeConfirmationStep.DisplayCheckCode("12"),
|
||||
QrCodeConfirmationStep.DisplayVerificationCode("123456"),
|
||||
QrCodeConfirmationStep.DisplayVerificationCode("123456789"),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.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.tooling.preview.PreviewParameter
|
||||
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.login.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 QrCodeConfirmationView(
|
||||
step: QrCodeConfirmationStep,
|
||||
onCancel: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler {
|
||||
onCancel()
|
||||
}
|
||||
val icon = when (step) {
|
||||
is QrCodeConfirmationStep.DisplayCheckCode -> CompoundIcons.Computer()
|
||||
is QrCodeConfirmationStep.DisplayVerificationCode -> CompoundIcons.LockSolid()
|
||||
}
|
||||
val title = when (step) {
|
||||
is QrCodeConfirmationStep.DisplayCheckCode -> stringResource(R.string.screen_qr_code_login_device_code_title)
|
||||
is QrCodeConfirmationStep.DisplayVerificationCode -> stringResource(R.string.screen_qr_code_login_verify_code_title)
|
||||
}
|
||||
val subtitle = when (step) {
|
||||
is QrCodeConfirmationStep.DisplayCheckCode -> stringResource(R.string.screen_qr_code_login_device_code_subtitle)
|
||||
is QrCodeConfirmationStep.DisplayVerificationCode -> stringResource(R.string.screen_qr_code_login_verify_code_subtitle)
|
||||
}
|
||||
FlowStepPage(
|
||||
modifier = modifier,
|
||||
iconStyle = BigIcon.Style.Default(icon),
|
||||
title = title,
|
||||
subTitle = subtitle,
|
||||
content = { Content(step = step) },
|
||||
buttons = { Buttons(onCancel = onCancel) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(step: QrCodeConfirmationStep) {
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
when (step) {
|
||||
is QrCodeConfirmationStep.DisplayCheckCode -> {
|
||||
Digits(code = step.code)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
WaitingForOtherDevice()
|
||||
}
|
||||
is QrCodeConfirmationStep.DisplayVerificationCode -> {
|
||||
Digits(code = step.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 QrCodeConfirmationViewPreview(@PreviewParameter(QrCodeConfirmationStepPreviewProvider::class) step: QrCodeConfirmationStep) {
|
||||
ElementPreview {
|
||||
QrCodeConfirmationView(
|
||||
step = step,
|
||||
onCancel = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.error
|
||||
|
||||
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 com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginScope
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
|
||||
@ContributesNode(QrCodeLoginScope::class)
|
||||
class QrCodeErrorNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : Node(buildContext = buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onRetry()
|
||||
}
|
||||
|
||||
private fun onRetry() {
|
||||
plugins<Callback>().forEach { it.onRetry() }
|
||||
}
|
||||
|
||||
private val qrCodeErrorScreenType = inputs<QrCodeErrorScreenType>()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
QrCodeErrorView(
|
||||
modifier = modifier,
|
||||
errorScreenType = qrCodeErrorScreenType,
|
||||
appName = buildMeta.productionApplicationName,
|
||||
onRetry = ::onRetry,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.error
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism
|
||||
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.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun QrCodeErrorView(
|
||||
errorScreenType: QrCodeErrorScreenType,
|
||||
appName: String,
|
||||
onRetry: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler {
|
||||
onRetry()
|
||||
}
|
||||
FlowStepPage(
|
||||
modifier = modifier,
|
||||
iconStyle = BigIcon.Style.AlertSolid,
|
||||
title = titleText(errorScreenType, appName),
|
||||
subTitle = subtitleText(errorScreenType, appName),
|
||||
content = { Content(errorScreenType) },
|
||||
buttons = { Buttons(onRetry) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun titleText(errorScreenType: QrCodeErrorScreenType, appName: String) = when (errorScreenType) {
|
||||
QrCodeErrorScreenType.Cancelled -> stringResource(R.string.screen_qr_code_login_error_cancelled_title)
|
||||
QrCodeErrorScreenType.Declined -> stringResource(R.string.screen_qr_code_login_error_declined_title)
|
||||
QrCodeErrorScreenType.Expired -> stringResource(R.string.screen_qr_code_login_error_expired_title)
|
||||
QrCodeErrorScreenType.ProtocolNotSupported -> stringResource(R.string.screen_qr_code_login_error_linking_not_suported_title)
|
||||
QrCodeErrorScreenType.InsecureChannelDetected -> stringResource(id = R.string.screen_qr_code_login_connection_note_secure_state_title)
|
||||
QrCodeErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_title, appName)
|
||||
is QrCodeErrorScreenType.UnknownError -> stringResource(CommonStrings.common_something_went_wrong)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun subtitleText(errorScreenType: QrCodeErrorScreenType, appName: String) = when (errorScreenType) {
|
||||
QrCodeErrorScreenType.Cancelled -> stringResource(R.string.screen_qr_code_login_error_cancelled_subtitle)
|
||||
QrCodeErrorScreenType.Declined -> stringResource(R.string.screen_qr_code_login_error_declined_subtitle)
|
||||
QrCodeErrorScreenType.Expired -> stringResource(R.string.screen_qr_code_login_error_expired_subtitle)
|
||||
QrCodeErrorScreenType.ProtocolNotSupported -> stringResource(R.string.screen_qr_code_login_error_linking_not_suported_subtitle, appName)
|
||||
QrCodeErrorScreenType.InsecureChannelDetected -> stringResource(id = R.string.screen_qr_code_login_connection_note_secure_state_description)
|
||||
QrCodeErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_subtitle, appName)
|
||||
is QrCodeErrorScreenType.UnknownError -> stringResource(R.string.screen_qr_code_login_unknown_error_description)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.InsecureChannelDetectedError() {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
text = stringResource(R.string.screen_qr_code_login_connection_note_secure_state_list_header),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
NumberedListOrganism(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
items = persistentListOf(
|
||||
AnnotatedString(stringResource(R.string.screen_qr_code_login_connection_note_secure_state_list_item_1)),
|
||||
AnnotatedString(stringResource(R.string.screen_qr_code_login_connection_note_secure_state_list_item_2)),
|
||||
AnnotatedString(stringResource(R.string.screen_qr_code_login_connection_note_secure_state_list_item_3)),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(errorScreenType: QrCodeErrorScreenType) {
|
||||
when (errorScreenType) {
|
||||
QrCodeErrorScreenType.InsecureChannelDetected -> {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
InsecureChannelDetectedError()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Buttons(onRetry: () -> Unit) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.screen_qr_code_login_start_over_button),
|
||||
onClick = onRetry
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun QrCodeErrorViewPreview(@PreviewParameter(QrCodeErrorScreenTypeProvider::class) errorScreenType: QrCodeErrorScreenType) {
|
||||
ElementPreview {
|
||||
QrCodeErrorView(
|
||||
errorScreenType = errorScreenType,
|
||||
appName = "Element X",
|
||||
onRetry = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class QrCodeErrorScreenTypeProvider : PreviewParameterProvider<QrCodeErrorScreenType> {
|
||||
override val values: Sequence<QrCodeErrorScreenType> = sequenceOf(
|
||||
QrCodeErrorScreenType.Cancelled,
|
||||
QrCodeErrorScreenType.Declined,
|
||||
QrCodeErrorScreenType.Expired,
|
||||
QrCodeErrorScreenType.ProtocolNotSupported,
|
||||
QrCodeErrorScreenType.InsecureChannelDetected,
|
||||
QrCodeErrorScreenType.SlidingSyncNotAvailable,
|
||||
QrCodeErrorScreenType.UnknownError
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.intro
|
||||
|
||||
sealed interface QrCodeIntroEvents {
|
||||
data object Continue : QrCodeIntroEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.intro
|
||||
|
||||
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 com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginScope
|
||||
|
||||
@ContributesNode(QrCodeLoginScope::class)
|
||||
class QrCodeIntroNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: QrCodeIntroPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onCancelClicked()
|
||||
fun onContinue()
|
||||
}
|
||||
|
||||
private fun onCancelClicked() {
|
||||
plugins<Callback>().forEach { it.onCancelClicked() }
|
||||
}
|
||||
|
||||
private fun onContinue() {
|
||||
plugins<Callback>().forEach { it.onContinue() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
QrCodeIntroView(
|
||||
state = state,
|
||||
onBackClick = ::onCancelClicked,
|
||||
onContinue = ::onContinue,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.intro
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import javax.inject.Inject
|
||||
|
||||
class QrCodeIntroPresenter @Inject constructor(
|
||||
private val buildMeta: BuildMeta,
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
) : Presenter<QrCodeIntroState> {
|
||||
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
|
||||
private var pendingPermissionRequest by mutableStateOf(false)
|
||||
|
||||
@Composable
|
||||
override fun present(): QrCodeIntroState {
|
||||
val cameraPermissionState = cameraPermissionPresenter.present()
|
||||
var canContinue by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
||||
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
|
||||
pendingPermissionRequest = false
|
||||
canContinue = true
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: QrCodeIntroEvents) {
|
||||
when (event) {
|
||||
QrCodeIntroEvents.Continue -> if (cameraPermissionState.permissionGranted) {
|
||||
canContinue = true
|
||||
} else {
|
||||
pendingPermissionRequest = true
|
||||
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QrCodeIntroState(
|
||||
desktopAppName = buildMeta.desktopApplicationName,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
canContinue = canContinue,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.intro
|
||||
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
|
||||
data class QrCodeIntroState(
|
||||
val desktopAppName: String,
|
||||
val cameraPermissionState: PermissionsState,
|
||||
val canContinue: Boolean,
|
||||
val eventSink: (QrCodeIntroEvents) -> Unit
|
||||
)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.intro
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||
|
||||
open class QrCodeIntroStateProvider : PreviewParameterProvider<QrCodeIntroState> {
|
||||
override val values: Sequence<QrCodeIntroState>
|
||||
get() = sequenceOf(
|
||||
aQrCodeIntroState(),
|
||||
aQrCodeIntroState(cameraPermissionState = aPermissionsState(showDialog = true, permission = Manifest.permission.CAMERA)),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
||||
fun aQrCodeIntroState(
|
||||
desktopAppName: String = "Element",
|
||||
cameraPermissionState: PermissionsState = aPermissionsState(
|
||||
showDialog = false,
|
||||
permission = Manifest.permission.CAMERA,
|
||||
),
|
||||
canContinue: Boolean = false,
|
||||
eventSink: (QrCodeIntroEvents) -> Unit = {},
|
||||
) = QrCodeIntroState(
|
||||
desktopAppName = desktopAppName,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
canContinue = canContinue,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.intro
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism
|
||||
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.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||
import io.element.android.libraries.permissions.api.PermissionsView
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun QrCodeIntroView(
|
||||
state: QrCodeIntroState,
|
||||
onBackClick: () -> Unit,
|
||||
onContinue: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val latestOnContinue by rememberUpdatedState(onContinue)
|
||||
LaunchedEffect(state.canContinue) {
|
||||
if (state.canContinue) {
|
||||
latestOnContinue()
|
||||
}
|
||||
}
|
||||
FlowStepPage(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
|
||||
title = stringResource(id = R.string.screen_qr_code_login_initial_state_title, state.desktopAppName),
|
||||
content = { Content(state = state) },
|
||||
buttons = { Buttons(state = state) }
|
||||
)
|
||||
|
||||
PermissionsView(
|
||||
title = stringResource(R.string.screen_qr_code_login_no_camera_permission_state_title),
|
||||
content = stringResource(R.string.screen_qr_code_login_no_camera_permission_state_description),
|
||||
icon = { Icon(imageVector = CompoundIcons.TakePhotoSolid(), contentDescription = null) },
|
||||
state = state.cameraPermissionState,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: QrCodeIntroState) {
|
||||
NumberedListOrganism(
|
||||
modifier = Modifier.padding(top = 50.dp, start = 20.dp, end = 20.dp),
|
||||
items = persistentListOf(
|
||||
AnnotatedString(stringResource(R.string.screen_qr_code_login_initial_state_item_1, state.desktopAppName)),
|
||||
AnnotatedString(stringResource(R.string.screen_qr_code_login_initial_state_item_2)),
|
||||
annotatedTextWithBold(
|
||||
text = stringResource(
|
||||
id = R.string.screen_qr_code_login_initial_state_item_3,
|
||||
stringResource(R.string.screen_qr_code_login_initial_state_item_3_action),
|
||||
),
|
||||
boldText = stringResource(R.string.screen_qr_code_login_initial_state_item_3_action)
|
||||
),
|
||||
AnnotatedString(stringResource(R.string.screen_qr_code_login_initial_state_item_4)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.Buttons(
|
||||
state: QrCodeIntroState,
|
||||
) {
|
||||
Button(
|
||||
text = stringResource(id = CommonStrings.action_continue),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
state.eventSink.invoke(QrCodeIntroEvents.Continue)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun QrCodeIntroViewPreview(@PreviewParameter(QrCodeIntroStateProvider::class) state: QrCodeIntroState) = ElementPreview {
|
||||
QrCodeIntroView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onContinue = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||
|
||||
sealed interface QrCodeScanEvents {
|
||||
data class QrCodeScanned(val code: ByteArray) : QrCodeScanEvents
|
||||
data object TryAgain : QrCodeScanEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||
|
||||
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 com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.login.impl.di.QrCodeLoginScope
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
|
||||
@ContributesNode(QrCodeLoginScope::class)
|
||||
class QrCodeScanNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: QrCodeScanPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onScannedCode(qrCodeLoginData: MatrixQrCodeLoginData)
|
||||
fun onCancelClicked()
|
||||
}
|
||||
|
||||
private fun onQrCodeDataReady(qrCodeLoginData: MatrixQrCodeLoginData) {
|
||||
plugins<Callback>().forEach { it.onScannedCode(qrCodeLoginData) }
|
||||
}
|
||||
|
||||
private fun onCancelClicked() {
|
||||
plugins<Callback>().forEach { it.onCancelClicked() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
QrCodeScanView(
|
||||
state = state,
|
||||
onQrCodeDataReady = ::onQrCodeDataReady,
|
||||
onBackClick = ::onCancelClicked,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
class QrCodeScanPresenter @Inject constructor(
|
||||
private val qrCodeLoginDataFactory: MatrixQrCodeLoginDataFactory,
|
||||
private val qrCodeLoginManager: QrCodeLoginManager,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : Presenter<QrCodeScanState> {
|
||||
private var isScanning by mutableStateOf(true)
|
||||
|
||||
private val isProcessingCode = AtomicBoolean(false)
|
||||
|
||||
@Composable
|
||||
override fun present(): QrCodeScanState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val authenticationAction: MutableState<AsyncAction<MatrixQrCodeLoginData>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
|
||||
ObserveQRCodeLoginFailures {
|
||||
authenticationAction.value = AsyncAction.Failure(it)
|
||||
}
|
||||
|
||||
fun handleEvents(event: QrCodeScanEvents) {
|
||||
when (event) {
|
||||
QrCodeScanEvents.TryAgain -> {
|
||||
isScanning = true
|
||||
authenticationAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is QrCodeScanEvents.QrCodeScanned -> {
|
||||
isScanning = false
|
||||
coroutineScope.getQrCodeData(authenticationAction, event.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QrCodeScanState(
|
||||
isScanning = isScanning,
|
||||
authenticationAction = authenticationAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ObserveQRCodeLoginFailures(onQrCodeLoginError: (QrLoginException) -> Unit) {
|
||||
LaunchedEffect(onQrCodeLoginError) {
|
||||
qrCodeLoginManager.currentLoginStep
|
||||
.onEach { state ->
|
||||
if (state is QrCodeLoginStep.Failed) {
|
||||
onQrCodeLoginError(state.error)
|
||||
// The error was handled here, reset the login state
|
||||
qrCodeLoginManager.reset()
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.getQrCodeData(codeScannedAction: MutableState<AsyncAction<MatrixQrCodeLoginData>>, code: ByteArray) {
|
||||
if (codeScannedAction.value.isSuccess() || isProcessingCode.compareAndSet(true, true)) return
|
||||
|
||||
launch(coroutineDispatchers.computation) {
|
||||
suspend {
|
||||
qrCodeLoginDataFactory.parseQrCodeData(code).onFailure {
|
||||
Timber.e(it, "Error parsing QR code data")
|
||||
}.getOrThrow()
|
||||
}.runCatchingUpdatingState(codeScannedAction)
|
||||
}.invokeOnCompletion {
|
||||
isProcessingCode.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
|
||||
data class QrCodeScanState(
|
||||
val isScanning: Boolean,
|
||||
val authenticationAction: AsyncAction<MatrixQrCodeLoginData>,
|
||||
val eventSink: (QrCodeScanEvents) -> Unit
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||
|
||||
open class QrCodeScanStateProvider : PreviewParameterProvider<QrCodeScanState> {
|
||||
override val values: Sequence<QrCodeScanState>
|
||||
get() = sequenceOf(
|
||||
aQrCodeScanState(),
|
||||
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Loading),
|
||||
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(Exception("Error"))),
|
||||
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(QrLoginException.OtherDeviceNotSignedIn)),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
||||
fun aQrCodeScanState(
|
||||
isScanning: Boolean = true,
|
||||
authenticationAction: AsyncAction<MatrixQrCodeLoginData> = AsyncAction.Uninitialized,
|
||||
eventSink: (QrCodeScanEvents) -> Unit = {},
|
||||
) = QrCodeScanState(
|
||||
isScanning = isScanning,
|
||||
authenticationAction = authenticationAction,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.progressSemantics
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
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.login.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.modifiers.cornerBorder
|
||||
import io.element.android.libraries.designsystem.modifiers.squareSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||
import io.element.android.libraries.qrcode.QrCodeCameraView
|
||||
|
||||
@Composable
|
||||
fun QrCodeScanView(
|
||||
state: QrCodeScanState,
|
||||
onBackClick: () -> Unit,
|
||||
onQrCodeDataReady: (MatrixQrCodeLoginData) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val updatedOnQrCodeDataReady by rememberUpdatedState(onQrCodeDataReady)
|
||||
// QR code data parsed successfully, notify the parent node
|
||||
if (state.authenticationAction is AsyncAction.Success) {
|
||||
LaunchedEffect(state.authenticationAction, updatedOnQrCodeDataReady) {
|
||||
updatedOnQrCodeDataReady(state.authenticationAction.data)
|
||||
}
|
||||
}
|
||||
|
||||
FlowStepPage(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
|
||||
title = stringResource(R.string.screen_qr_code_login_scanning_state_title),
|
||||
content = { Content(state = state) },
|
||||
buttons = { Buttons(state = state) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
state: QrCodeScanState,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val modifier = if (constraints.maxWidth > constraints.maxHeight) {
|
||||
Modifier.fillMaxHeight()
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
}.then(
|
||||
Modifier
|
||||
.padding(start = 20.dp, end = 20.dp, top = 50.dp, bottom = 32.dp)
|
||||
.squareSize()
|
||||
.cornerBorder(
|
||||
strokeWidth = 4.dp,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
cornerSizeDp = 42.dp,
|
||||
)
|
||||
)
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
QrCodeCameraView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onScanQrCode = { state.eventSink.invoke(QrCodeScanEvents.QrCodeScanned(it)) },
|
||||
renderPreview = state.isScanning,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.Buttons(
|
||||
state: QrCodeScanState,
|
||||
) {
|
||||
Column(Modifier.heightIn(min = 130.dp)) {
|
||||
when (state.authenticationAction) {
|
||||
is AsyncAction.Failure -> {
|
||||
Button(
|
||||
text = stringResource(id = R.string.screen_qr_code_login_invalid_scan_state_retry_button),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
onClick = {
|
||||
state.eventSink.invoke(QrCodeScanEvents.TryAgain)
|
||||
}
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
val error = state.authenticationAction.error
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Error(),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = when (error) {
|
||||
is QrLoginException.OtherDeviceNotSignedIn -> {
|
||||
stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_subtitle)
|
||||
}
|
||||
else -> stringResource(R.string.screen_qr_code_login_invalid_scan_state_subtitle)
|
||||
},
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = when (error) {
|
||||
is QrLoginException.OtherDeviceNotSignedIn -> {
|
||||
stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_description)
|
||||
}
|
||||
else -> stringResource(R.string.screen_qr_code_login_invalid_scan_state_description)
|
||||
},
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
AsyncAction.Loading, is AsyncAction.Success -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.progressSemantics()
|
||||
.size(20.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.screen_qr_code_login_connecting_subtitle),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
AsyncAction.Uninitialized,
|
||||
AsyncAction.Confirming -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun QrCodeScanViewPreview(@PreviewParameter(QrCodeScanStateProvider::class) state: QrCodeScanState) = ElementPreview {
|
||||
QrCodeScanView(
|
||||
state = state,
|
||||
onQrCodeDataReady = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -30,6 +30,33 @@
|
|||
<string name="screen_login_subtitle">"Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі."</string>
|
||||
<string name="screen_login_title">"Сардэчна запрашаем!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Увайдзіце ў %1$s"</string>
|
||||
<string name="screen_qr_code_login_connecting_subtitle">"Ўсталяванне бяспечнага злучэння"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не атрымалася ўсталяваць бяспечнае злучэнне з новай прыладай. Існуючыя прылады па-ранейшаму ў бяспецы, і вам не трэба турбавацца пра іх."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Што зараз?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Паспрабуйце зноў увайсці ў сістэму з дапамогай QR-кода, калі гэта была сеткавая праблема"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Калі вы сутыкнуліся з той жа праблемай, паспрабуйце іншую сетку Wi-Fi або скарыстайцеся мабільнымі дадзенымі замест Wi-Fi."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Калі гэта не дапамагло, увайдзіце ўручную"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Злучэнне небяспечнае"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Вам будзе прапанавана ўвесці дзве лічбы, паказаныя на гэтай прыладзе."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Увядзіце наступны нумар на іншай прыладзе."</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце %1$s на настольнай прыладзе"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Выконвайце паказаныя інструкцыі"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Няправільны QR-код"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_button">"Перайсці ў налады камеры"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Каб працягнуць, вам неабходна дазволіць %1$s выкарыстоўваць камеру вашай прылады."</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Дазвольце доступ да камеры для сканавання QR-кода"</string>
|
||||
<string name="screen_qr_code_login_scanning_state_title">"Сканаваць QR-код"</string>
|
||||
<string name="screen_qr_code_login_start_over_button">"Пачаць спачатку"</string>
|
||||
<string name="screen_qr_code_login_unknown_error_description">"Адбылася нечаканая памылка. Калі ласка, паспрабуйце яшчэ раз."</string>
|
||||
<string name="screen_qr_code_login_verify_code_loading">"У чаканні іншай прылады"</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Ваш правайдэр уліковага запісу можа запытаць наступны код для праверкі ўваходу."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Ваш код спраўджання"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Змяніць правайдара ўліковага запісу"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Прыватны сервер для супрацоўнікаў Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі."</string>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<string name="screen_login_subtitle">"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."</string>
|
||||
<string name="screen_login_title">"Добре дошли отново!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Влизане в %1$s"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторен опит"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Промяна на доставчика на акаунт"</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."</string>
|
||||
<string name="screen_server_confirmation_message_register">"Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,33 @@
|
|||
<string name="screen_login_subtitle">"Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci."</string>
|
||||
<string name="screen_login_title">"Vítejte zpět!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Přihlaste se k %1$s"</string>
|
||||
<string name="screen_qr_code_login_connecting_subtitle">"Navazování zabezpečeného spojení"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zařízení se nepodařilo navázat bezpečné připojení. Vaše stávající zařízení jsou stále v bezpečí a nemusíte se o ně obávat."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Co teď?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Zkuste se znovu přihlásit pomocí QR kódu v případě, že se jednalo o problém se sítí"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Pokud narazíte na stejný problém, zkuste jinou síť wifi nebo použijte mobilní data místo wifi"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Pokud to nefunguje, přihlaste se ručně"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Připojení není zabezpečené"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Budete požádáni o zadání dvou níže uvedených číslic."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Zadejte níže uvedené číslo na svém dalším zařízení"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Otevřete %1$s na stolním počítači"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Postupujte podle uvedených pokynů"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Špatný QR kód"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_button">"Přejděte na nastavení fotoaparátu"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Abyste mohli pokračovat, musíte aplikaci %1$s udělit povolení k použití kamery vašeho zařízení."</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povolte přístup k fotoaparátu a naskenujte QR kód"</string>
|
||||
<string name="screen_qr_code_login_scanning_state_title">"Naskenujte QR kód"</string>
|
||||
<string name="screen_qr_code_login_start_over_button">"Začít znovu"</string>
|
||||
<string name="screen_qr_code_login_unknown_error_description">"Vyskytla se neočekávaná chyba. Prosím zkuste to znovu."</string>
|
||||
<string name="screen_qr_code_login_verify_code_loading">"Čekání na vaše další zařízení"</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Váš poskytovatel účtu může požádat o následující kód pro ověření přihlášení."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Váš ověřovací kód"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Změnit poskytovatele účtu"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Soukromý server pro zaměstnance Elementu."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,33 @@
|
|||
<string name="screen_login_subtitle">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string>
|
||||
<string name="screen_login_title">"Willkommen zurück!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Anmelden bei %1$s"</string>
|
||||
<string name="screen_qr_code_login_connecting_subtitle">"Sichere Verbindung aufbauen"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Es konnte keine sichere Verbindung zu dem neuen Gerät hergestellt werden."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Und jetzt?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Versuche, dich erneut mit einem QR-Code anzumelden, falls dies ein Netzwerkproblem war."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Wenn das Problem bestehen bleibt, versuche es mit einem anderen WLAN-Netzwerk oder verwende deine mobilen Daten statt WLAN."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Wenn das nicht funktioniert, melde dich manuell an"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Trage die unten angezeigte Zahl auf einem anderen Device ein"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Befolge die angezeigten Anweisungen"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Falscher QR-Code"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_button">"Gehe zu den Kameraeinstellungen"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Du musst %1$s die Erlaubnis erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren."</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes"</string>
|
||||
<string name="screen_qr_code_login_scanning_state_title">"QR-Code scannen"</string>
|
||||
<string name="screen_qr_code_login_start_over_button">"Neu beginnen"</string>
|
||||
<string name="screen_qr_code_login_unknown_error_description">"Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut."</string>
|
||||
<string name="screen_qr_code_login_verify_code_loading">"Warten auf dein anderes Gerät"</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Dein Account-Provider kann nach dem folgenden Code fragen, um die Anmeldung zu bestätigen."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Dein Verifizierungscode"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Kontoanbieter wechseln"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Ein privater Server für die Mitarbeiter von Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
<string name="screen_login_subtitle">"Matrix es una red abierta para una comunicación segura y descentralizada."</string>
|
||||
<string name="screen_login_title">"¡Hola de nuevo!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Iniciar sesión en %1$s"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Inténtalo de nuevo"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Cambiar el proveedor de la cuenta"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Un servidor privado para los empleados de Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix es una red abierta para una comunicación segura y descentralizada."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,33 @@
|
|||
<string name="screen_login_subtitle">"Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."</string>
|
||||
<string name="screen_login_title">"Content de vous revoir !"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Connectez-vous à %1$s"</string>
|
||||
<string name="screen_qr_code_login_connecting_subtitle">"Établissement d’une connexion sécurisée"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Aucune connexion sécurisée n’a pu être établie avec la nouvelle session. Vos sessions existantes sont toujours en sécurité et vous n’avez pas à vous en soucier."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Et maintenant ?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Essayez de vous connecter à nouveau à l’aide du code QR au cas où il s’agirait d’un problème réseau"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Si vous rencontrez le même problème, essayez un autre réseau wifi ou utilisez vos données mobiles au lieu du wifi"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Si cela ne fonctionne pas, connectez-vous manuellement"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"La connexion n’est pas sécurisée"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Il vous sera demandé de saisir les deux chiffres affichés sur cet appareil."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Saisissez le nombre ci-dessous sur votre autre appareil"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Ouvrez %1$s sur un ordinateur"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Suivez les instructions affichées"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur l’autre appareil."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"QR code erroné"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_button">"Accéder aux paramètres de l’appareil photo"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Vous devez autoriser %1$s à utiliser la camera de votre appareil pour continuer."</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Autoriser l’usage de la caméra pour scanner le code QR"</string>
|
||||
<string name="screen_qr_code_login_scanning_state_title">"Scannez le QR code"</string>
|
||||
<string name="screen_qr_code_login_start_over_button">"Recommencer"</string>
|
||||
<string name="screen_qr_code_login_unknown_error_description">"Une erreur inattendue s’est produite. Veuillez réessayer."</string>
|
||||
<string name="screen_qr_code_login_verify_code_loading">"En attente de votre autre session"</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Votre fournisseur de compte peut vous demander le code suivant pour vérifier la connexion."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Votre code de vérification"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Changer de fournisseur de compte"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Un serveur privé pour les employés d’Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,33 @@
|
|||
<string name="screen_login_subtitle">"A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz."</string>
|
||||
<string name="screen_login_title">"Örülünk, hogy visszatért!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Bejelentkezés ide: %1$s"</string>
|
||||
<string name="screen_qr_code_login_connecting_subtitle">"Biztonságos kapcsolat létesítése"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nem sikerült biztonságos kapcsolatot létesíteni az új eszközzel. A meglévő eszközei továbbra is biztonságban vannak, és nem kell aggódnia miattuk."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Most mi lesz?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Próbáljon meg újra bejelentkezni egy QR-kóddal, ha ez hálózati probléma volt."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Ha ugyanezzel a problémával találkozik, próbálkozzon másik Wi-Fi-hálózattal, vagy a Wi-Fi helyett használja a mobil-adatkapcsolatát"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Ha ez nem működik, jelentkezzen be kézileg"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet az eszközén."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Adja meg az alábbi számot a másik eszközén"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Kövesse a látható utasításokat"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Hibás QR-kód"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_button">"Ugrás a kamerabeállításokhoz"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"A folytatáshoz engedélyeznie kell, hogy az %1$s használhassa az eszköz kameráját."</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Engedélyezze a kamera elérését a QR-kód beolvasásához"</string>
|
||||
<string name="screen_qr_code_login_scanning_state_title">"Olvassa be a QR-kódot"</string>
|
||||
<string name="screen_qr_code_login_start_over_button">"Újrakezdés"</string>
|
||||
<string name="screen_qr_code_login_unknown_error_description">"Váratlan hiba történt. Próbálja meg újra."</string>
|
||||
<string name="screen_qr_code_login_verify_code_loading">"Várakozás a másik eszközre"</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"A fiókszolgáltatója kérheti a következő kódot a bejelentkezése ellenőrzéséhez."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Az Ön ellenőrzőkódja"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Fiókszolgáltató módosítása"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Egy privát kiszolgáló az Element alkalmazottai számára."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,32 @@
|
|||
<string name="screen_login_subtitle">"Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."</string>
|
||||
<string name="screen_login_title">"Selamat datang kembali!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Masuk ke %1$s"</string>
|
||||
<string name="screen_qr_code_login_connecting_subtitle">"Membuat koneksi"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Apa sekarang?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Coba masuk lagi dengan kode QR jika ini adalah masalah jaringan"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Jika Anda mengalami masalah yang sama, coba jaringan Wi-Fi yang berbeda atau gunakan data seluler Anda daripada Wi-Fi"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Jika tidak berhasil, masuk secara manual"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Koneksi tidak aman"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Anda akan diminta untuk memasukkan dua digit yang ditunjukkan di bawah ini."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Masukkan nomor di perangkat Anda"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Buka %1$s di perangkat desktop"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Tautkan perangkat baru”"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Buka %1$s di perangkat lain untuk mendapatkan kode QR"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Kode QR salah"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_button">"Pergi ke pengaturan kamera"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Anda perlu memberikan izin ke %1$s untuk menggunakan kamera perangkat Anda untuk melanjutkan."</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Izinkan akses kamera untuk memindai kode QR"</string>
|
||||
<string name="screen_qr_code_login_scanning_state_title">"Pindai kode QR"</string>
|
||||
<string name="screen_qr_code_login_start_over_button">"Mulai dari awal"</string>
|
||||
<string name="screen_qr_code_login_unknown_error_description">"Terjadi kesalahan tak terduga. Silakan coba lagi."</string>
|
||||
<string name="screen_qr_code_login_verify_code_loading">"Menunggu perangkat Anda yang lain"</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Penyedia akun Anda mungkin meminta kode berikut untuk memverifikasi proses masuk."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Kode verifikasi Anda"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Ubah penyedia akun"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Server pribadi untuk karyawan Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
<string name="screen_login_subtitle">"Matrix è una rete aperta per comunicazioni sicure e decentralizzate."</string>
|
||||
<string name="screen_login_title">"Bentornato!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Accedi a %1$s"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Riprova"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Cambia fornitore dell\'account"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Un server privato per i dipendenti di Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix è una rete aperta per comunicazioni sicure e decentralizzate."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
<string name="screen_login_subtitle">"Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată."</string>
|
||||
<string name="screen_login_title">"Bine ați revenit!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Conectați-vă la %1$s"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Încercați din nou"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Schimbați furnizorul contului"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Un server privat pentru angajații Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,32 @@
|
|||
<string name="screen_login_subtitle">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
|
||||
<string name="screen_login_title">"Рады видеть вас снова!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Войти в %1$s"</string>
|
||||
<string name="screen_qr_code_login_connecting_subtitle">"Установление соединения"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была сетевая проблема"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Вам будет предложено ввести две цифры, показанные ниже."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Введите номер на своем устройстве"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Неверный QR-код"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_button">"Перейдите в настройки камеры"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Чтобы продолжить, вам необходимо разрешить %1$s использовать камеру вашего устройства."</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Разрешите доступ к камере для сканирования QR-кода"</string>
|
||||
<string name="screen_qr_code_login_scanning_state_title">"Сканировать QR-код"</string>
|
||||
<string name="screen_qr_code_login_start_over_button">"Начать заново"</string>
|
||||
<string name="screen_qr_code_login_unknown_error_description">"Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз."</string>
|
||||
<string name="screen_qr_code_login_verify_code_loading">"В ожидании другого устройства"</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Поставщик учетной записи может запросить следующий код для подтверждения входа."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Ваш код подтверждения"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Сменить учетную запись"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Частный сервер для сотрудников Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,33 @@
|
|||
<string name="screen_login_subtitle">"Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."</string>
|
||||
<string name="screen_login_title">"Vitajte späť!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Prihlásiť sa do %1$s"</string>
|
||||
<string name="screen_qr_code_login_connecting_subtitle">"Nadväzovanie bezpečného spojenia"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"K novému zariadeniu sa nepodarilo vytvoriť bezpečné pripojenie. Vaše existujúce zariadenia sú stále v bezpečí a nemusíte sa o ne obávať."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Čo teraz?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Skúste sa znova prihlásiť pomocou QR kódu v prípade, že ide o problém so sieťou"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Ak narazíte na rovnaký problém, vyskúšajte inú sieť Wi-Fi alebo namiesto siete Wi-Fi použite mobilné dáta"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Ak to nefunguje, prihláste sa manuálne"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Pripojenie nie je bezpečené"</string>
|
||||
<string name="screen_qr_code_login_device_code_subtitle">"Budete požiadaní o zadanie dvoch číslic zobrazených na tomto zariadení."</string>
|
||||
<string name="screen_qr_code_login_device_code_title">"Zadajte nižšie uvedené číslo na vašom druhom zariadení"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Vyberte %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"„Prepojiť nové zariadenie“"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Postupujte podľa zobrazených pokynov"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Ak chcete získať QR kód, otvorte %1$s na inom zariadení"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Použite QR kód zobrazený na druhom zariadení."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Nesprávny QR kód"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_button">"Prejsť na nastavenia fotoaparátu"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Ak chcete pokračovať, musíte udeliť povolenie aplikácii %1$s používať fotoaparát vášho zariadenia."</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povoľte prístup k fotoaparátu na naskenovanie QR kódu"</string>
|
||||
<string name="screen_qr_code_login_scanning_state_title">"Naskenovať QR kód"</string>
|
||||
<string name="screen_qr_code_login_start_over_button">"Začať odznova"</string>
|
||||
<string name="screen_qr_code_login_unknown_error_description">"Vyskytla sa neočakávaná chyba. Prosím, skúste to znova."</string>
|
||||
<string name="screen_qr_code_login_verify_code_loading">"Čaká sa na vaše druhé zariadenie"</string>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Váš poskytovateľ účtu môže požiadať o nasledujúci kód na overenie prihlásenia."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Váš overovací kód"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Zmeniť poskytovateľa účtu"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Súkromný server pre zamestnancov spoločnosti Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
<string name="screen_login_subtitle">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>
|
||||
<string name="screen_login_title">"Välkommen tillbaka!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Logga in på %1$s"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Försök igen"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Byt kontoleverantör"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"En privat server för Element-anställda."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
<string name="screen_login_subtitle">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
|
||||
<string name="screen_login_title">"З поверненням!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Увійти в %1$s"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Спробуйте ще раз"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Змінити провайдера облікового запису"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Приватний сервер для співробітників Element."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."</string>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
<string name="screen_login_subtitle">"Matrix 是一個開放網路,為了安全且去中心化的通訊而生。"</string>
|
||||
<string name="screen_login_title">"歡迎回來!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"登入 %1$s"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"再試一次"</string>
|
||||
<string name="screen_server_confirmation_change_server">"更改帳號提供者"</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix 是一個開放網路,為了安全且去中心化的通訊而生。"</string>
|
||||
<string name="screen_server_confirmation_message_register">"您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"</string>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,48 @@
|
|||
<string name="screen_login_subtitle">"Matrix is an open network for secure, decentralised communication."</string>
|
||||
<string name="screen_login_title">"Welcome back!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Sign in to %1$s"</string>
|
||||
<string name="screen_qr_code_login_connecting_subtitle">"Establishing a secure connection"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing devices are still safe and you don\'t need to worry about them."</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"What now?"</string>
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Try signing in again with a QR code in case this was a network problem"</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_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_device_not_signed_in_scan_state_description">"Sign in to your other device and then try again, or use another device that’s already signed in."</string>
|
||||
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Other device not signed in"</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_declined_subtitle">"The sign in was declined on the other device."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Sign in declined"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Sign in expired. Please try again."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"The sign in was not completed in time"</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Your other device does not support signing in to %s with a QR code.
|
||||
|
||||
Try signing in manually, or scan the QR code with another device."</string>
|
||||
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR code not supported"</string>
|
||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Your account provider does not support %1$s."</string>
|
||||
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s not supported"</string>
|
||||
<string name="screen_qr_code_login_initial_state_button_title">"Ready to scan"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string>
|
||||
<string name="screen_qr_code_login_initial_state_item_4">"Scan the QR code with this device"</string>
|
||||
<string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>
|
||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Wrong QR code"</string>
|
||||
<string name="screen_qr_code_login_no_camera_permission_button">"Go to camera settings"</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_scanning_state_title">"Scan the QR code"</string>
|
||||
<string name="screen_qr_code_login_start_over_button">"Start over"</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>
|
||||
<string name="screen_qr_code_login_verify_code_subtitle">"Your account provider may ask for the following code to verify the sign in."</string>
|
||||
<string name="screen_qr_code_login_verify_code_title">"Your verification code"</string>
|
||||
<string name="screen_server_confirmation_change_server">"Change account provider"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"A private server for Element employees."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix is an open network for secure, decentralised communication."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.di
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.features.login.impl.qrcode.FakeQrCodeLoginManager
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
|
||||
import io.element.android.libraries.architecture.AssistedNodeFactory
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
||||
internal class FakeQrCodeLoginComponent(private val qrCodeLoginManager: QrCodeLoginManager) :
|
||||
QrCodeLoginComponent {
|
||||
// Ignore this error, it does override a method once code generation is done
|
||||
override fun qrCodeLoginManager(): QrCodeLoginManager = qrCodeLoginManager
|
||||
|
||||
class Builder(private val qrCodeLoginManager: QrCodeLoginManager = FakeQrCodeLoginManager()) :
|
||||
QrCodeLoginComponent.Builder {
|
||||
override fun build(): QrCodeLoginComponent {
|
||||
return FakeQrCodeLoginComponent(qrCodeLoginManager)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFactories(): Map<Class<out Node>, AssistedNodeFactory<*>> {
|
||||
return mapOf(
|
||||
QrCodeLoginFlowNode::class.java to object : AssistedNodeFactory<QrCodeLoginFlowNode> {
|
||||
override fun create(buildContext: BuildContext, plugins: List<Plugin>): QrCodeLoginFlowNode {
|
||||
return createNode<QrCodeLoginFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.qrcode
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultQrCodeLoginManagerTest {
|
||||
@Test
|
||||
fun `authenticate - returns success if the login succeeded`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
loginWithQrCodeResult = { _, _ -> Result.success(A_SESSION_ID) }
|
||||
)
|
||||
val manager = DefaultQrCodeLoginManager(authenticationService)
|
||||
val result = manager.authenticate(FakeMatrixQrCodeLoginData())
|
||||
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
assertThat(result.getOrNull()).isEqualTo(A_SESSION_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticate - returns failure if the login failed`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
loginWithQrCodeResult = { _, _ -> Result.failure(IllegalStateException("Auth failed")) }
|
||||
)
|
||||
val manager = DefaultQrCodeLoginManager(authenticationService)
|
||||
val result = manager.authenticate(FakeMatrixQrCodeLoginData())
|
||||
|
||||
assertThat(result.isFailure).isTrue()
|
||||
assertThat(result.exceptionOrNull()).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticate - emits the auth steps`() = runTest {
|
||||
val authenticationService = FakeMatrixAuthenticationService(
|
||||
loginWithQrCodeResult = { _, progressListener ->
|
||||
progressListener(QrCodeLoginStep.EstablishingSecureChannel("00"))
|
||||
progressListener(QrCodeLoginStep.Starting)
|
||||
progressListener(QrCodeLoginStep.WaitingForToken("000000"))
|
||||
progressListener(QrCodeLoginStep.Finished)
|
||||
Result.success(A_SESSION_ID)
|
||||
}
|
||||
)
|
||||
val manager = DefaultQrCodeLoginManager(authenticationService)
|
||||
manager.currentLoginStep.test {
|
||||
manager.authenticate(FakeMatrixQrCodeLoginData())
|
||||
|
||||
assertThat(awaitItem()).isEqualTo(QrCodeLoginStep.Uninitialized)
|
||||
assertThat(awaitItem()).isEqualTo(QrCodeLoginStep.EstablishingSecureChannel("00"))
|
||||
assertThat(awaitItem()).isEqualTo(QrCodeLoginStep.Starting)
|
||||
assertThat(awaitItem()).isEqualTo(QrCodeLoginStep.WaitingForToken("000000"))
|
||||
assertThat(awaitItem()).isEqualTo(QrCodeLoginStep.Finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.qrcode
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FakeQrCodeLoginManager(
|
||||
var authenticateResult: (MatrixQrCodeLoginData) -> Result<SessionId> =
|
||||
lambdaRecorder<MatrixQrCodeLoginData, Result<SessionId>> { Result.success(A_SESSION_ID) },
|
||||
var resetAction: () -> Unit = lambdaRecorder<Unit> { },
|
||||
) : QrCodeLoginManager {
|
||||
override val currentLoginStep: MutableStateFlow<QrCodeLoginStep> =
|
||||
MutableStateFlow(QrCodeLoginStep.Uninitialized)
|
||||
|
||||
override suspend fun authenticate(qrCodeLoginData: MatrixQrCodeLoginData): Result<SessionId> {
|
||||
return authenticateResult(qrCodeLoginData)
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
resetAction()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.qrcode
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.bumble.appyx.core.modality.AncestryInfo
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.di.FakeQrCodeLoginComponent
|
||||
import io.element.android.features.login.impl.screens.qrcode.confirmation.QrCodeConfirmationStep
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class QrCodeLoginFlowNodeTest {
|
||||
@Test
|
||||
fun `backstack changes when confirmation steps are received`() = runTest {
|
||||
val qrCodeLoginManager = FakeQrCodeLoginManager()
|
||||
val flowNode = createLoginFlowNode(qrCodeLoginManager = qrCodeLoginManager)
|
||||
flowNode.observeLoginStep()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Initial)
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.EstablishingSecureChannel("12")
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.QrCodeConfirmation(QrCodeConfirmationStep.DisplayCheckCode("12")))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.WaitingForToken("123456")
|
||||
assertThat(flowNode.currentNavTarget())
|
||||
.isEqualTo(QrCodeLoginFlowNode.NavTarget.QrCodeConfirmation(QrCodeConfirmationStep.DisplayVerificationCode("123456")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `backstack changes when failure step is received`() = runTest {
|
||||
val qrCodeLoginManager = FakeQrCodeLoginManager()
|
||||
val flowNode = createLoginFlowNode(qrCodeLoginManager = qrCodeLoginManager)
|
||||
flowNode.observeLoginStep()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Initial)
|
||||
|
||||
// Only case when this doesn't happen, since it's handled by the already displayed UI
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.OtherDeviceNotSignedIn)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Initial)
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Expired)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Expired))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Declined)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Declined))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Cancelled)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.Cancelled))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.SlidingSyncNotAvailable)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.SlidingSyncNotAvailable))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.LinkingNotSupported)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.ProtocolNotSupported))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.ConnectionInsecure)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.InsecureChannelDetected))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.OidcMetadataInvalid)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Unknown)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.UnknownError))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `backstack doesn't change when other steps are received`() = runTest {
|
||||
val qrCodeLoginManager = FakeQrCodeLoginManager()
|
||||
val flowNode = createLoginFlowNode(qrCodeLoginManager = qrCodeLoginManager)
|
||||
flowNode.observeLoginStep()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Initial)
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Starting
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Initial)
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Finished
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Initial)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `startAuthentication - success marks the login flow as done`() = runTest {
|
||||
val fakeAuthenticationService = FakeMatrixAuthenticationService(
|
||||
loginWithQrCodeResult = { _, progress ->
|
||||
progress(QrCodeLoginStep.Finished)
|
||||
Result.success(A_SESSION_ID)
|
||||
}
|
||||
)
|
||||
// Test with a real manager to ensure the flow is correctly done
|
||||
val qrCodeLoginManager = DefaultQrCodeLoginManager(fakeAuthenticationService)
|
||||
val defaultLoginUserStory = DefaultLoginUserStory().apply {
|
||||
loginFlowIsDone.value = false
|
||||
}
|
||||
val flowNode = createLoginFlowNode(
|
||||
qrCodeLoginManager = qrCodeLoginManager,
|
||||
defaultLoginUserStory = defaultLoginUserStory,
|
||||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
)
|
||||
|
||||
flowNode.run { startAuthentication(FakeMatrixQrCodeLoginData()) }
|
||||
assertThat(flowNode.isLoginInProgress()).isTrue()
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(qrCodeLoginManager.currentLoginStep.value).isEqualTo(QrCodeLoginStep.Finished)
|
||||
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isTrue()
|
||||
assertThat(flowNode.isLoginInProgress()).isFalse()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `startAuthentication - failure is correctly handled`() = runTest {
|
||||
val fakeAuthenticationService = FakeMatrixAuthenticationService(
|
||||
loginWithQrCodeResult = { _, progress ->
|
||||
progress(QrCodeLoginStep.Failed(QrLoginException.Unknown))
|
||||
Result.failure(IllegalStateException("Failed"))
|
||||
}
|
||||
)
|
||||
// Test with a real manager to ensure the flow is correctly done
|
||||
val qrCodeLoginManager = DefaultQrCodeLoginManager(fakeAuthenticationService)
|
||||
val defaultLoginUserStory = DefaultLoginUserStory().apply {
|
||||
loginFlowIsDone.value = false
|
||||
}
|
||||
val flowNode = createLoginFlowNode(
|
||||
qrCodeLoginManager = qrCodeLoginManager,
|
||||
defaultLoginUserStory = defaultLoginUserStory,
|
||||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
)
|
||||
|
||||
flowNode.run { startAuthentication(FakeMatrixQrCodeLoginData()) }
|
||||
assertThat(flowNode.isLoginInProgress()).isTrue()
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(qrCodeLoginManager.currentLoginStep.value).isEqualTo(QrCodeLoginStep.Failed(QrLoginException.Unknown))
|
||||
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
|
||||
assertThat(flowNode.isLoginInProgress()).isFalse()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `startAuthentication - then reset`() = runTest {
|
||||
val fakeAuthenticationService = FakeMatrixAuthenticationService(
|
||||
loginWithQrCodeResult = { _, progress ->
|
||||
progress(QrCodeLoginStep.Finished)
|
||||
Result.success(A_SESSION_ID)
|
||||
}
|
||||
)
|
||||
// Test with a real manager to ensure the flow is correctly done
|
||||
val qrCodeLoginManager = DefaultQrCodeLoginManager(fakeAuthenticationService)
|
||||
val defaultLoginUserStory = DefaultLoginUserStory().apply {
|
||||
loginFlowIsDone.value = false
|
||||
}
|
||||
val flowNode = createLoginFlowNode(
|
||||
qrCodeLoginManager = qrCodeLoginManager,
|
||||
defaultLoginUserStory = defaultLoginUserStory,
|
||||
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
)
|
||||
|
||||
flowNode.run { startAuthentication(FakeMatrixQrCodeLoginData()) }
|
||||
assertThat(flowNode.isLoginInProgress()).isTrue()
|
||||
flowNode.reset()
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(qrCodeLoginManager.currentLoginStep.value).isEqualTo(QrCodeLoginStep.Uninitialized)
|
||||
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
|
||||
assertThat(flowNode.isLoginInProgress()).isFalse()
|
||||
}
|
||||
|
||||
private fun TestScope.createLoginFlowNode(
|
||||
qrCodeLoginManager: QrCodeLoginManager = FakeQrCodeLoginManager(),
|
||||
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
|
||||
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers()
|
||||
): QrCodeLoginFlowNode {
|
||||
val buildContext = BuildContext(
|
||||
ancestryInfo = AncestryInfo.Root,
|
||||
savedStateMap = null,
|
||||
customisations = NodeCustomisationDirectoryImpl()
|
||||
)
|
||||
return QrCodeLoginFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = emptyList(),
|
||||
qrCodeLoginComponentBuilder = FakeQrCodeLoginComponent.Builder(qrCodeLoginManager),
|
||||
defaultLoginUserStory = defaultLoginUserStory,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
private fun QrCodeLoginFlowNode.currentNavTarget() = backstack.elements.value.last().key.navTarget
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.confirmation
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class QrCodeConfirmationViewTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `on back pressed - calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setQrCodeConfirmationView(
|
||||
step = QrCodeConfirmationStep.DisplayCheckCode("12"),
|
||||
onCancel = callback
|
||||
)
|
||||
rule.pressBackKey()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Cancel button clicked - calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setQrCodeConfirmationView(
|
||||
step = QrCodeConfirmationStep.DisplayVerificationCode("123456"),
|
||||
onCancel = callback
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeConfirmationView(
|
||||
step: QrCodeConfirmationStep,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
setContent {
|
||||
QrCodeConfirmationView(
|
||||
step = step,
|
||||
onCancel = onCancel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.error
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class QrCodeErrorViewTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `on back pressed - calls the onRetry callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setQrCodeErrorView(
|
||||
onRetry = callback
|
||||
)
|
||||
rule.pressBackKey()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on try again button clicked - calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setQrCodeErrorView(
|
||||
onRetry = callback
|
||||
)
|
||||
rule.clickOn(R.string.screen_qr_code_login_start_over_button)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeErrorView(
|
||||
onRetry: () -> Unit,
|
||||
errorScreenType: QrCodeErrorScreenType = QrCodeErrorScreenType.UnknownError,
|
||||
appName: String = "Element X",
|
||||
) {
|
||||
setContent {
|
||||
QrCodeErrorView(
|
||||
errorScreenType = errorScreenType,
|
||||
appName = appName,
|
||||
onRetry = onRetry
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.intro
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class QrCodeIntroPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createQrCodeIntroPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().run {
|
||||
assertThat(desktopAppName).isEmpty()
|
||||
assertThat(cameraPermissionState.permission).isEqualTo("android.permission.POST_NOTIFICATIONS")
|
||||
assertThat(canContinue).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Continue with camera permissions can continue`() = runTest {
|
||||
val permissionsPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
|
||||
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
|
||||
val presenter = createQrCodeIntroPresenter(permissionsPresenterFactory = permissionsPresenterFactory)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(QrCodeIntroEvents.Continue)
|
||||
assertThat(awaitItem().canContinue).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Continue with unknown camera permissions opens permission dialog`() = runTest {
|
||||
val permissionsPresenter = FakePermissionsPresenter()
|
||||
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
|
||||
val presenter = createQrCodeIntroPresenter(permissionsPresenterFactory = permissionsPresenterFactory)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(QrCodeIntroEvents.Continue)
|
||||
assertThat(awaitItem().cameraPermissionState.showDialog).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createQrCodeIntroPresenter(
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
permissionsPresenterFactory: FakePermissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||
): QrCodeIntroPresenter {
|
||||
return QrCodeIntroPresenter(
|
||||
buildMeta = buildMeta,
|
||||
permissionsPresenterFactory = permissionsPresenterFactory,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.intro
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class QrCodeIntroViewTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `on back pressed - calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setQrCodeIntroView(
|
||||
state = aQrCodeIntroState(),
|
||||
onBackClicked = callback
|
||||
)
|
||||
rule.pressBackKey()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on back button clicked - calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setQrCodeIntroView(
|
||||
state = aQrCodeIntroState(),
|
||||
onBackClicked = callback
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when can continue - calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setQrCodeIntroView(
|
||||
state = aQrCodeIntroState(canContinue = true),
|
||||
onContinue = callback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on continue button clicked - emits the Continue event`() {
|
||||
val eventRecorder = EventsRecorder<QrCodeIntroEvents>()
|
||||
rule.setQrCodeIntroView(
|
||||
state = aQrCodeIntroState(eventSink = eventRecorder),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_continue)
|
||||
eventRecorder.assertSingle(QrCodeIntroEvents.Continue)
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeIntroView(
|
||||
state: QrCodeIntroState,
|
||||
onBackClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onContinue: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
QrCodeIntroView(
|
||||
state = state,
|
||||
onBackClick = onBackClicked,
|
||||
onContinue = onContinue,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.login.impl.qrcode.FakeQrCodeLoginManager
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginDataFactory
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class QrCodeScanPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createQrCodeScanPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().run {
|
||||
assertThat(isScanning).isTrue()
|
||||
assertThat(authenticationAction.isUninitialized()).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - scanned QR code successfully`() = runTest {
|
||||
val presenter = createQrCodeScanPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf()))
|
||||
assertThat(awaitItem().isScanning).isFalse()
|
||||
assertThat(awaitItem().authenticationAction.isLoading()).isTrue()
|
||||
assertThat(awaitItem().authenticationAction.isSuccess()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - scanned QR code failed and can be retried`() = runTest {
|
||||
val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(
|
||||
parseQrCodeLoginDataResult = { Result.failure(Exception("Failed to parse QR code")) }
|
||||
)
|
||||
val presenter = createQrCodeScanPresenter(qrCodeLoginDataFactory = qrCodeLoginDataFactory)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf()))
|
||||
assertThat(awaitItem().isScanning).isFalse()
|
||||
assertThat(awaitItem().authenticationAction.isLoading()).isTrue()
|
||||
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.authenticationAction.isFailure()).isTrue()
|
||||
|
||||
errorState.eventSink(QrCodeScanEvents.TryAgain)
|
||||
assertThat(awaitItem().isScanning).isTrue()
|
||||
assertThat(awaitItem().authenticationAction.isUninitialized()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - login failed with so we display the error and recover from it`() = runTest {
|
||||
val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory()
|
||||
val qrCodeLoginManager = FakeQrCodeLoginManager()
|
||||
val resetAction = lambdaRecorder<Unit> {
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Uninitialized
|
||||
}
|
||||
qrCodeLoginManager.resetAction = resetAction
|
||||
val presenter = createQrCodeScanPresenter(qrCodeLoginDataFactory = qrCodeLoginDataFactory, qrCodeLoginManager = qrCodeLoginManager)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
// Skip initial item
|
||||
skipItems(1)
|
||||
|
||||
qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.OtherDeviceNotSignedIn)
|
||||
|
||||
val errorState = awaitItem()
|
||||
// The state for this screen is failure
|
||||
assertThat(errorState.authenticationAction.isFailure()).isTrue()
|
||||
// However, the QrCodeLoginManager is reset
|
||||
resetAction.assertions().isCalledOnce()
|
||||
assertThat(qrCodeLoginManager.currentLoginStep.value).isEqualTo(QrCodeLoginStep.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createQrCodeScanPresenter(
|
||||
qrCodeLoginDataFactory: FakeMatrixQrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(),
|
||||
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
qrCodeLoginManager: FakeQrCodeLoginManager = FakeQrCodeLoginManager(),
|
||||
) = QrCodeScanPresenter(
|
||||
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
||||
qrCodeLoginManager = qrCodeLoginManager,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class QrCodeScanViewTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `on back pressed - calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setQrCodeScanView(
|
||||
state = aQrCodeScanState(),
|
||||
onBackClick = callback
|
||||
)
|
||||
rule.pressBackKey()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on QR code data ready - calls the expected callback`() {
|
||||
val data = FakeMatrixQrCodeLoginData()
|
||||
ensureCalledOnceWithParam<MatrixQrCodeLoginData>(data) { callback ->
|
||||
rule.setQrCodeScanView(
|
||||
state = aQrCodeScanState(authenticationAction = AsyncAction.Success(data)),
|
||||
onQrCodeDataReady = callback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setQrCodeScanView(
|
||||
state: QrCodeScanState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onQrCodeDataReady: (MatrixQrCodeLoginData) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
setContent {
|
||||
QrCodeScanView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onQrCodeDataReady = onQrCodeDataReady
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue