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:
committed by
GitHub
parent
e0c55ff4c8
commit
35702c04e9
@@ -1,6 +1,6 @@
|
||||
appId: ${MAESTRO_APP_ID}
|
||||
---
|
||||
- tapOn: "Continue"
|
||||
- tapOn: "Sign in manually"
|
||||
- runFlow: ../assertions/assertLoginDisplayed.yaml
|
||||
- takeScreenshot: build/maestro/100-SignIn
|
||||
- runFlow: changeServer.yaml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* 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.
|
||||
@@ -14,9 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.onboarding.impl
|
||||
package io.element.android.appconfig
|
||||
|
||||
object OnBoardingConfig {
|
||||
/** Whether the user can use QR code login. */
|
||||
const val CAN_LOGIN_WITH_QR_CODE = false
|
||||
|
||||
/** Whether the user can create an account using the app. */
|
||||
const val CAN_CREATE_ACCOUNT = false
|
||||
}
|
||||
@@ -31,10 +31,13 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.login.api.LoginEntryPoint
|
||||
import io.element.android.features.login.api.LoginFlowType
|
||||
import io.element.android.features.onboarding.api.OnBoardingEntryPoint
|
||||
import io.element.android.features.preferences.api.ConfigureTracingEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
|
||||
import io.element.android.libraries.designsystem.utils.ScreenOrientation
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.ui.media.NotLoggedInImageLoaderFactory
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -73,9 +76,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||
data object OnBoarding : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LoginFlow(
|
||||
val isAccountCreation: Boolean,
|
||||
) : NavTarget
|
||||
data class LoginFlow(val type: LoginFlowType) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ConfigureTracing : NavTarget
|
||||
@@ -86,11 +87,15 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||
NavTarget.OnBoarding -> {
|
||||
val callback = object : OnBoardingEntryPoint.Callback {
|
||||
override fun onSignUp() {
|
||||
backstack.push(NavTarget.LoginFlow(isAccountCreation = true))
|
||||
backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_UP))
|
||||
}
|
||||
|
||||
override fun onSignIn() {
|
||||
backstack.push(NavTarget.LoginFlow(isAccountCreation = false))
|
||||
backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_IN_MANUAL))
|
||||
}
|
||||
|
||||
override fun onSignInWithQrCode() {
|
||||
backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_IN_QR_CODE))
|
||||
}
|
||||
|
||||
override fun onOpenDeveloperSettings() {
|
||||
@@ -108,7 +113,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||
}
|
||||
is NavTarget.LoginFlow -> {
|
||||
loginEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(LoginEntryPoint.Params(isAccountCreation = navTarget.isAccountCreation))
|
||||
.params(LoginEntryPoint.Params(flowType = navTarget.type))
|
||||
.build()
|
||||
}
|
||||
NavTarget.ConfigureTracing -> {
|
||||
@@ -119,6 +124,9 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
// The login flow doesn't support landscape mode on mobile devices yet
|
||||
ForceOrientationInMobileDevices(orientation = ScreenOrientation.PORTRAIT)
|
||||
|
||||
BackstackView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
<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_subtitle">"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_title">"Other device not signed in"</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 request on your other device was not accepted."</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>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@@ -16,13 +16,15 @@
|
||||
|
||||
package io.element.android.features.login.api
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
interface LoginEntryPoint : FeatureEntryPoint {
|
||||
data class Params(
|
||||
val isAccountCreation: Boolean,
|
||||
val flowType: LoginFlowType
|
||||
)
|
||||
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
@@ -32,3 +34,10 @@ interface LoginEntryPoint : FeatureEntryPoint {
|
||||
fun build(): Node
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
enum class LoginFlowType : Parcelable {
|
||||
SIGN_IN_MANUAL,
|
||||
SIGN_IN_QR_CODE,
|
||||
SIGN_UP
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.qrcode)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(platform(libs.network.retrofit.bom))
|
||||
implementation(libs.network.retrofit)
|
||||
@@ -57,10 +59,15 @@ dependencies {
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import io.element.android.features.logout.impl.tools.isBackingUp
|
||||
import io.element.android.features.logout.impl.ui.LogoutActionDialog
|
||||
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.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
@@ -62,7 +63,7 @@ fun LogoutView(
|
||||
onBackClick = onBackClick,
|
||||
title = title(state),
|
||||
subTitle = subtitle(state),
|
||||
iconVector = CompoundIcons.KeySolid(),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
|
||||
modifier = modifier,
|
||||
buttons = {
|
||||
Buttons(
|
||||
|
||||
@@ -32,6 +32,7 @@ interface OnBoardingEntryPoint : FeatureEntryPoint {
|
||||
interface Callback : Plugin {
|
||||
fun onSignUp()
|
||||
fun onSignIn()
|
||||
fun onSignInWithQrCode()
|
||||
fun onOpenDeveloperSettings()
|
||||
fun onReportProblem()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.onboarding.impl"
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anvil {
|
||||
@@ -32,21 +38,28 @@ anvil {
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.androidutils)
|
||||
api(projects.features.onboarding.api)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,10 @@ class OnBoardingNode @AssistedInject constructor(
|
||||
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onSignUp() }
|
||||
}
|
||||
|
||||
private fun onSignInWithQrCode() {
|
||||
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onSignInWithQrCode() }
|
||||
}
|
||||
|
||||
private fun onOpenDeveloperSettings() {
|
||||
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onOpenDeveloperSettings() }
|
||||
}
|
||||
@@ -61,7 +65,7 @@ class OnBoardingNode @AssistedInject constructor(
|
||||
modifier = modifier,
|
||||
onSignIn = ::onSignIn,
|
||||
onCreateAccount = ::onSignUp,
|
||||
onSignInWithQrCode = { /* Not supported yet */ },
|
||||
onSignInWithQrCode = ::onSignInWithQrCode,
|
||||
onOpenDeveloperSettings = ::onOpenDeveloperSettings,
|
||||
onReportProblem = ::onReportProblem,
|
||||
)
|
||||
|
||||
@@ -17,9 +17,14 @@
|
||||
package io.element.android.features.onboarding.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import io.element.android.appconfig.OnBoardingConfig
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -28,13 +33,17 @@ import javax.inject.Inject
|
||||
*/
|
||||
class OnBoardingPresenter @Inject constructor(
|
||||
private val buildMeta: BuildMeta,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<OnBoardingState> {
|
||||
@Composable
|
||||
override fun present(): OnBoardingState {
|
||||
return OnBoardingState(
|
||||
val canLoginWithQrCode by produceState(initialValue = false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
||||
}
|
||||
return OnBoardingState(
|
||||
isDebugBuild = buildMeta.buildType != BuildType.RELEASE,
|
||||
productionApplicationName = buildMeta.productionApplicationName,
|
||||
canLoginWithQrCode = OnBoardingConfig.CAN_LOGIN_WITH_QR_CODE,
|
||||
canLoginWithQrCode = canLoginWithQrCode,
|
||||
canCreateAccount = OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -34,31 +36,38 @@ class OnBoardingPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = OnBoardingPresenter(
|
||||
aBuildMeta(
|
||||
buildMeta = aBuildMeta(
|
||||
applicationName = "A",
|
||||
productionApplicationName = "B",
|
||||
desktopApplicationName = "C",
|
||||
)
|
||||
),
|
||||
featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.QrCodeLogin.name to true)),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isDebugBuild).isTrue()
|
||||
assertThat(initialState.productionApplicationName).isEqualTo("B")
|
||||
assertThat(initialState.canLoginWithQrCode).isFalse()
|
||||
assertThat(initialState.productionApplicationName).isEqualTo("B")
|
||||
assertThat(initialState.canCreateAccount).isFalse()
|
||||
|
||||
assertThat(awaitItem().canLoginWithQrCode).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state release`() = runTest {
|
||||
val presenter = OnBoardingPresenter(aBuildMeta(buildType = BuildType.RELEASE))
|
||||
val presenter = OnBoardingPresenter(
|
||||
buildMeta = aBuildMeta(buildType = BuildType.RELEASE),
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isDebugBuild).isFalse()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.onboarding.impl
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.performClick
|
||||
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.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class OnboardingViewTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `when can create account - clicking on create account calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setOnboardingView(
|
||||
state = anOnBoardingState(canCreateAccount = true),
|
||||
onCreateAccount = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_onboarding_sign_up)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setOnboardingView(
|
||||
state = anOnBoardingState(canLoginWithQrCode = true),
|
||||
onSignInWithQrCode = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_onboarding_sign_in_with_qr_code)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when can login with QR code - clicking on sign in manually calls the expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setOnboardingView(
|
||||
state = anOnBoardingState(canLoginWithQrCode = true),
|
||||
onSignIn = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_onboarding_sign_in_manually)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setOnboardingView(
|
||||
state = anOnBoardingState(
|
||||
canLoginWithQrCode = false,
|
||||
canCreateAccount = false,
|
||||
),
|
||||
onSignIn = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_continue)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when on debug build - clicking on the settings icon opens the developer settings`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setOnboardingView(
|
||||
state = anOnBoardingState(isDebugBuild = true),
|
||||
onOpenDeveloperSettings = callback
|
||||
)
|
||||
rule.onNode(hasContentDescription(rule.activity.getString(CommonStrings.common_settings))).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on report a problem calls the sign in callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setOnboardingView(
|
||||
state = anOnBoardingState(),
|
||||
onReportProblem = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_report_a_problem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOnboardingView(
|
||||
state: OnBoardingState,
|
||||
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
|
||||
onSignIn: () -> Unit = EnsureNeverCalled(),
|
||||
onCreateAccount: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenDeveloperSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onReportProblem: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
OnBoardingView(
|
||||
state = state,
|
||||
onSignInWithQrCode = onSignInWithQrCode,
|
||||
onSignIn = onSignIn,
|
||||
onCreateAccount = onCreateAccount,
|
||||
onOpenDeveloperSettings = onOpenDeveloperSettings,
|
||||
onReportProblem = onReportProblem,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,35 +16,26 @@
|
||||
|
||||
package io.element.android.features.securebackup.impl.createkey
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.PageTitle
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
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.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -74,55 +65,19 @@ fun CreateNewRecoveryKeyView(
|
||||
|
||||
@Composable
|
||||
private fun Content(desktopApplicationName: String) {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
Item(index = 1, text = AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_1, desktopApplicationName)))
|
||||
Item(index = 2, text = AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_2)))
|
||||
Item(
|
||||
index = 3,
|
||||
text = buildAnnotatedString {
|
||||
val resetAllAction = stringResource(R.string.screen_create_new_recovery_key_list_item_3_reset_all)
|
||||
val text = stringResource(R.string.screen_create_new_recovery_key_list_item_3, resetAllAction)
|
||||
append(text)
|
||||
val start = text.indexOf(resetAllAction)
|
||||
val end = start + resetAllAction.length
|
||||
if (start in text.indices && end in text.indices) {
|
||||
addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
|
||||
}
|
||||
}
|
||||
)
|
||||
Item(index = 4, text = AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_4)))
|
||||
Item(index = 5, text = AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_5)))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Item(index: Int, text: AnnotatedString) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ItemNumber(index = index)
|
||||
Text(text = text, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemNumber(
|
||||
index: Int,
|
||||
) {
|
||||
val color = ElementTheme.colors.textPlaceholder
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(1.dp, color, CircleShape)
|
||||
.squareSize()
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(1.5.dp),
|
||||
text = index.toString(),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = color,
|
||||
textAlign = TextAlign.Center,
|
||||
val listItems = buildList {
|
||||
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_1, desktopApplicationName)))
|
||||
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_2)))
|
||||
add(
|
||||
annotatedTextWithBold(
|
||||
text = stringResource(R.string.screen_create_new_recovery_key_list_item_3),
|
||||
boldText = stringResource(R.string.screen_create_new_recovery_key_list_item_3_reset_all)
|
||||
)
|
||||
)
|
||||
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_4)))
|
||||
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_5)))
|
||||
}
|
||||
NumberedListOrganism(modifier = Modifier.padding(horizontal = 16.dp), items = listItems.toImmutableList())
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
|
||||
@@ -32,6 +32,7 @@ import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securebackup.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.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
@@ -52,7 +53,7 @@ fun SecureBackupDisableView(
|
||||
onBackClick = onBackClick,
|
||||
title = stringResource(id = R.string.screen_key_backup_disable_title),
|
||||
subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
|
||||
iconVector = CompoundIcons.KeyOffSolid(),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeyOffSolid()),
|
||||
buttons = { Buttons(state = state) },
|
||||
) {
|
||||
Content(state = state)
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securebackup.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.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -41,7 +42,7 @@ fun SecureBackupEnableView(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
|
||||
iconVector = CompoundIcons.KeySolid(),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
|
||||
buttons = { Buttons(state = state) }
|
||||
)
|
||||
AsyncActionView(
|
||||
|
||||
@@ -28,6 +28,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView
|
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -55,7 +56,7 @@ fun SecureBackupEnterRecoveryKeyView(
|
||||
FlowStepPage(
|
||||
modifier = modifier,
|
||||
onBackClick = onBackClick,
|
||||
iconVector = CompoundIcons.KeySolid(),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
|
||||
title = stringResource(id = R.string.screen_recovery_key_confirm_title),
|
||||
subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description),
|
||||
buttons = { Buttons(state = state, onCreateRecoveryKey = onCreateNewRecoveryKey) }
|
||||
|
||||
@@ -31,6 +31,7 @@ import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView
|
||||
import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -51,7 +52,7 @@ fun SecureBackupSetupView(
|
||||
onBackClick = onBackClick.takeIf { state.canGoBack() },
|
||||
title = title(state),
|
||||
subTitle = subtitle(state),
|
||||
iconVector = CompoundIcons.KeySolid(),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
|
||||
buttons = { Buttons(state, onFinish = onSuccess) },
|
||||
) {
|
||||
Content(state = state)
|
||||
|
||||
@@ -21,6 +21,7 @@ constraintlayout_compose = "1.0.1"
|
||||
lifecycle = "2.7.0"
|
||||
activity = "1.8.2"
|
||||
media3 = "1.3.1"
|
||||
camera = "1.3.2"
|
||||
|
||||
# Compose
|
||||
compose_bom = "2024.05.00"
|
||||
@@ -80,6 +81,9 @@ androidx_datastore_datastore = { module = "androidx.datastore:datastore", versio
|
||||
androidx_exifinterface = "androidx.exifinterface:exifinterface:1.3.7"
|
||||
androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
|
||||
androidx_constraintlayout_compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayout_compose" }
|
||||
androidx_camera_lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" }
|
||||
androidx_camera_view = { module = "androidx.camera:camera-view", version.ref = "camera" }
|
||||
androidx_camera_camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" }
|
||||
|
||||
androidx_recyclerview = "androidx.recyclerview:recyclerview:1.3.2"
|
||||
androidx_browser = "androidx.browser:browser:1.8.0"
|
||||
@@ -98,7 +102,9 @@ androidx_preference = "androidx.preference:preference:1.2.1"
|
||||
androidx_webkit = "androidx.webkit:webkit:1.11.0"
|
||||
|
||||
androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" }
|
||||
androidx_compose_material3 = "androidx.compose.material3:material3:1.2.1"
|
||||
androidx_compose_material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx_compose_material3_windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class" }
|
||||
androidx_compose_material3_adaptive = "androidx.compose.material3:material3-adaptive-android:1.0.0-alpha06"
|
||||
androidx_compose_ui = { module = "androidx.compose.ui:ui" }
|
||||
androidx_compose_ui_tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx_compose_ui_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
@@ -175,6 +181,7 @@ maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.0"
|
||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.0"
|
||||
opusencoder = "io.element.android:opusencoder:1.1.0"
|
||||
kotlinpoet = "com.squareup:kotlinpoet:1.17.0"
|
||||
zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
|
||||
|
||||
# Analytics
|
||||
posthog = "com.posthog:posthog-android:3.3.0"
|
||||
|
||||
@@ -36,7 +36,9 @@ android {
|
||||
|
||||
dependencies {
|
||||
api(libs.compound)
|
||||
// Should not be there, but this is a POC
|
||||
|
||||
implementation(libs.androidx.compose.material3.windowsizeclass)
|
||||
implementation(libs.androidx.compose.material3.adaptive)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.vanniktech.blurhash)
|
||||
implementation(projects.libraries.architecture)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.libraries.designsystem.atomic.molecules
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.modifiers.squareSize
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
fun NumberedListMolecule(
|
||||
index: Int,
|
||||
text: AnnotatedString,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ItemNumber(index = index)
|
||||
Text(text = text, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemNumber(
|
||||
index: Int,
|
||||
) {
|
||||
val color = ElementTheme.colors.textPlaceholder
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(1.dp, color, CircleShape)
|
||||
.squareSize()
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(1.5.dp),
|
||||
text = index.toString(),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = color,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.libraries.designsystem.atomic.organisms
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.NumberedListMolecule
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun NumberedListOrganism(
|
||||
items: ImmutableList<AnnotatedString>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
itemsIndexed(items) { index, item ->
|
||||
NumberedListMolecule(index = index + 1, text = item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
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.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.PageTitle
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -49,7 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FlowStepPage(
|
||||
iconVector: ImageVector?,
|
||||
iconStyle: BigIcon.Style,
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackClick: (() -> Unit)? = null,
|
||||
@@ -73,10 +73,10 @@ fun FlowStepPage(
|
||||
)
|
||||
},
|
||||
header = {
|
||||
IconTitleSubtitleMolecule(
|
||||
iconImageVector = iconVector,
|
||||
PageTitle(
|
||||
title = title,
|
||||
subTitle = subTitle,
|
||||
subtitle = subTitle,
|
||||
iconStyle = iconStyle,
|
||||
)
|
||||
},
|
||||
content = content,
|
||||
@@ -97,7 +97,7 @@ internal fun FlowStepPagePreview() = ElementPreview {
|
||||
onBackClick = {},
|
||||
title = "Title",
|
||||
subTitle = "Subtitle",
|
||||
iconVector = CompoundIcons.Computer(),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
|
||||
buttons = {
|
||||
TextButton(text = "A button", onClick = { })
|
||||
Button(text = "Continue", onClick = { })
|
||||
|
||||
@@ -44,6 +44,7 @@ fun ConfirmationDialog(
|
||||
thirdButtonText: String? = null,
|
||||
onCancelClick: () -> Unit = onDismiss,
|
||||
onThirdButtonClick: () -> Unit = {},
|
||||
icon: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
BasicAlertDialog(modifier = modifier, onDismissRequest = onDismiss) {
|
||||
ConfirmationDialogContent(
|
||||
@@ -56,6 +57,7 @@ fun ConfirmationDialog(
|
||||
onSubmitClick = onSubmitClick,
|
||||
onCancelClick = onCancelClick,
|
||||
onThirdButtonClick = onThirdButtonClick,
|
||||
icon = icon,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.libraries.designsystem.modifiers
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import io.element.android.libraries.designsystem.text.toPx
|
||||
|
||||
/**
|
||||
* Draw a border on corners around the content.
|
||||
*/
|
||||
@Suppress("ModifierComposed")
|
||||
fun Modifier.cornerBorder(
|
||||
strokeWidth: Dp,
|
||||
color: Color,
|
||||
cornerSizeDp: Dp,
|
||||
) = composed(
|
||||
factory = {
|
||||
val strokeWidthPx = strokeWidth.toPx()
|
||||
val cornerSize = cornerSizeDp.toPx()
|
||||
drawWithContent {
|
||||
drawContent()
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
drawPath(
|
||||
path = Path().apply {
|
||||
// Top left corner
|
||||
moveTo(0f, cornerSize)
|
||||
lineTo(0f, 0f)
|
||||
lineTo(cornerSize, 0f)
|
||||
// Top right corner
|
||||
moveTo(width - cornerSize, 0f)
|
||||
lineTo(width, 0f)
|
||||
lineTo(width, cornerSize)
|
||||
// Bottom right corner
|
||||
moveTo(width, height - cornerSize)
|
||||
lineTo(width, height)
|
||||
lineTo(width - cornerSize, height)
|
||||
// Bottom left corner
|
||||
moveTo(cornerSize, height)
|
||||
lineTo(0f, height)
|
||||
lineTo(0f, height - cornerSize)
|
||||
},
|
||||
color = color,
|
||||
style = Stroke(
|
||||
width = strokeWidthPx,
|
||||
pathEffect = PathEffect.cornerPathEffect(strokeWidthPx / 2),
|
||||
cap = StrokeCap.Round,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -150,6 +151,7 @@ internal fun SimpleAlertDialogContent(
|
||||
Text(
|
||||
text = titleText,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.libraries.designsystem.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
@Composable
|
||||
fun annotatedTextWithBold(text: String, boldText: String): AnnotatedString {
|
||||
return buildAnnotatedString {
|
||||
append(text)
|
||||
val start = text.indexOf(boldText)
|
||||
val end = start + boldText.length
|
||||
val textRange = 0..text.length
|
||||
if (start in textRange && end in textRange) {
|
||||
addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.libraries.designsystem.utils
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
fun ForceOrientation(orientation: ScreenOrientation) {
|
||||
val activity = LocalContext.current as? ComponentActivity ?: return
|
||||
val orientationFlags = when (orientation) {
|
||||
ScreenOrientation.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
ScreenOrientation.LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
DisposableEffect(orientation) {
|
||||
activity.requestedOrientation = orientationFlags
|
||||
onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED }
|
||||
}
|
||||
}
|
||||
|
||||
enum class ScreenOrientation {
|
||||
PORTRAIT,
|
||||
LANDSCAPE
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.libraries.designsystem.utils
|
||||
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun ForceOrientationInMobileDevices(orientation: ScreenOrientation) {
|
||||
val windowAdaptiveInfo = currentWindowAdaptiveInfo()
|
||||
if (windowAdaptiveInfo.windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
|
||||
windowAdaptiveInfo.windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact
|
||||
) {
|
||||
ForceOrientation(orientation = orientation)
|
||||
}
|
||||
}
|
||||
@@ -89,4 +89,11 @@ enum class FeatureFlags(
|
||||
defaultValue = false,
|
||||
isFinished = false,
|
||||
),
|
||||
QrCodeLogin(
|
||||
key = "feature.qrCodeLogin",
|
||||
title = "Enable login using QR code",
|
||||
description = "Allow the user to login using the QR code flow",
|
||||
defaultValue = true,
|
||||
isFinished = false,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies {
|
||||
api(projects.libraries.featureflag.api)
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(libs.coroutines.core)
|
||||
|
||||
@@ -39,7 +39,7 @@ class DefaultFeatureFlagService @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean {
|
||||
return providers.filterIsInstance(MutableFeatureFlagProvider::class.java)
|
||||
return providers.filterIsInstance<MutableFeatureFlagProvider>()
|
||||
.sortedBy(FeatureFlagProvider::priority)
|
||||
.firstOrNull()
|
||||
?.setFeatureEnabled(feature, enabled)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.libraries.featureflag.impl
|
||||
|
||||
import io.element.android.appconfig.OnBoardingConfig
|
||||
import io.element.android.libraries.featureflag.api.Feature
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -42,6 +43,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
||||
FeatureFlags.MarkAsUnread -> true
|
||||
FeatureFlags.RoomDirectorySearch -> false
|
||||
FeatureFlags.ShowBlockedUsersDetails -> false
|
||||
FeatureFlags.QrCodeLogin -> OnBoardingConfig.CAN_LOGIN_WITH_QR_CODE
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package io.element.android.libraries.matrix.api.auth
|
||||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
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.sessionstorage.api.LoggedInState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -53,4 +55,6 @@ interface MatrixAuthenticationService {
|
||||
* Attempt to login using the [callbackUrl] provided by the Oidc page.
|
||||
*/
|
||||
suspend fun loginWithOidc(callbackUrl: String): Result<SessionId>
|
||||
|
||||
suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit): Result<SessionId>
|
||||
}
|
||||
|
||||
@@ -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.libraries.matrix.api.auth.qrlogin
|
||||
|
||||
interface MatrixQrCodeLoginData
|
||||
@@ -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.libraries.matrix.api.auth.qrlogin
|
||||
|
||||
interface MatrixQrCodeLoginDataFactory {
|
||||
fun parseQrCodeData(data: ByteArray): Result<MatrixQrCodeLoginData>
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.libraries.matrix.api.auth.qrlogin
|
||||
|
||||
sealed class QrCodeDecodeException(message: String) : Exception(message) {
|
||||
class Crypto(
|
||||
message: String,
|
||||
// val reason: Reason
|
||||
) : QrCodeDecodeException(message) {
|
||||
// We plan to restore it in the future when UniFFi can process them
|
||||
// enum class Reason {
|
||||
// NOT_ENOUGH_DATA,
|
||||
// NOT_UTF8,
|
||||
// URL_PARSE,
|
||||
// INVALID_MODE,
|
||||
// INVALID_VERSION,
|
||||
// BASE64,
|
||||
// INVALID_PREFIX
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -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.libraries.matrix.api.auth.qrlogin
|
||||
|
||||
sealed interface QrCodeLoginStep {
|
||||
data object Uninitialized : QrCodeLoginStep
|
||||
data class EstablishingSecureChannel(val checkCode: String) : QrCodeLoginStep
|
||||
data object Starting : QrCodeLoginStep
|
||||
data class WaitingForToken(val userCode: String) : QrCodeLoginStep
|
||||
data class Failed(val error: QrLoginException) : QrCodeLoginStep
|
||||
data object Finished : QrCodeLoginStep
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.libraries.matrix.api.auth.qrlogin
|
||||
|
||||
sealed class QrLoginException : Exception() {
|
||||
data object Cancelled : QrLoginException()
|
||||
data object ConnectionInsecure : QrLoginException()
|
||||
data object Declined : QrLoginException()
|
||||
data object Expired : QrLoginException()
|
||||
data object LinkingNotSupported : QrLoginException()
|
||||
data object OidcMetadataInvalid : QrLoginException()
|
||||
data object SlidingSyncNotAvailable : QrLoginException()
|
||||
data object OtherDeviceNotSignedIn : QrLoginException()
|
||||
data object Unknown : QrLoginException()
|
||||
}
|
||||
@@ -46,25 +46,11 @@ class RustMatrixClientFactory @Inject constructor(
|
||||
private val utdTracker: UtdTracker,
|
||||
) {
|
||||
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
|
||||
val client = ClientBuilder()
|
||||
.basePath(baseDirectory.absolutePath)
|
||||
val client = getBaseClientBuilder()
|
||||
.homeserverUrl(sessionData.homeserverUrl)
|
||||
.username(sessionData.userId)
|
||||
.passphrase(sessionData.passphrase)
|
||||
.userAgent(userAgentProvider.provide())
|
||||
.let {
|
||||
// Sadly ClientBuilder.proxy() does not accept null :/
|
||||
// Tracked by https://github.com/matrix-org/matrix-rust-sdk/issues/3159
|
||||
val proxy = proxyProvider.provides()
|
||||
if (proxy != null) {
|
||||
it.proxy(proxy)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.addRootCertificates(userCertificatesProvider.provides())
|
||||
// FIXME Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
|
||||
.serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"))
|
||||
.use { it.build() }
|
||||
|
||||
client.restoreSession(sessionData.toSession())
|
||||
@@ -84,6 +70,24 @@ class RustMatrixClientFactory @Inject constructor(
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun getBaseClientBuilder(): ClientBuilder {
|
||||
return ClientBuilder()
|
||||
.basePath(baseDirectory.absolutePath)
|
||||
.userAgent(userAgentProvider.provide())
|
||||
.addRootCertificates(userCertificatesProvider.provides())
|
||||
.serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"))
|
||||
.let {
|
||||
// Sadly ClientBuilder.proxy() does not accept null :/
|
||||
// Tracked by https://github.com/matrix-org/matrix-rust-sdk/issues/3159
|
||||
val proxy = proxyProvider.provides()
|
||||
if (proxy != null) {
|
||||
it.proxy(proxy)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SessionData.toSession() = Session(
|
||||
|
||||
@@ -25,8 +25,13 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
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.impl.RustMatrixClientFactory
|
||||
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
|
||||
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
|
||||
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
|
||||
@@ -36,11 +41,16 @@ import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.HumanQrLoginException
|
||||
import org.matrix.rustcomponents.sdk.OidcAuthenticationData
|
||||
import org.matrix.rustcomponents.sdk.QrCodeDecodeException
|
||||
import org.matrix.rustcomponents.sdk.QrLoginProgress
|
||||
import org.matrix.rustcomponents.sdk.QrLoginProgressListener
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
@@ -197,4 +207,43 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
|
||||
withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val client = rustMatrixClientFactory.getBaseClientBuilder()
|
||||
.passphrase(pendingPassphrase)
|
||||
.buildWithQrCode(
|
||||
qrCodeData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData,
|
||||
oidcConfiguration = oidcConfiguration,
|
||||
progressListener = object : QrLoginProgressListener {
|
||||
override fun onUpdate(state: QrLoginProgress) {
|
||||
Timber.d("QR Code login progress: $state")
|
||||
progress(state.toStep())
|
||||
}
|
||||
}
|
||||
)
|
||||
client.use { rustClient ->
|
||||
val sessionData = rustClient.session()
|
||||
.toSessionData(
|
||||
isTokenValid = true,
|
||||
loginType = LoginType.QR,
|
||||
passphrase = pendingPassphrase,
|
||||
)
|
||||
sessionStore.storeData(sessionData)
|
||||
SessionId(sessionData.userId)
|
||||
}
|
||||
}.mapFailure {
|
||||
when (it) {
|
||||
is QrCodeDecodeException -> QrErrorMapper.map(it)
|
||||
is HumanQrLoginException -> QrErrorMapper.map(it)
|
||||
else -> it
|
||||
}
|
||||
}.onFailure { throwable ->
|
||||
if (throwable is CancellationException) {
|
||||
throw throwable
|
||||
}
|
||||
Timber.e(throwable, "Failed to login with QR code")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.libraries.matrix.impl.auth.qrlogin
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeDecodeException
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||
import org.matrix.rustcomponents.sdk.HumanQrLoginException as RustHumanQrLoginException
|
||||
import org.matrix.rustcomponents.sdk.QrCodeDecodeException as RustQrCodeDecodeException
|
||||
|
||||
object QrErrorMapper {
|
||||
fun map(qrCodeDecodeException: RustQrCodeDecodeException): QrCodeDecodeException = when (qrCodeDecodeException) {
|
||||
is RustQrCodeDecodeException.Crypto -> {
|
||||
// We plan to restore it in the future when UniFFi can process them
|
||||
// val reason = when (qrCodeDecodeException.error) {
|
||||
// LoginQrCodeDecodeError.NOT_ENOUGH_DATA -> QrCodeDecodeException.Crypto.Reason.NOT_ENOUGH_DATA
|
||||
// LoginQrCodeDecodeError.NOT_UTF8 -> QrCodeDecodeException.Crypto.Reason.NOT_UTF8
|
||||
// LoginQrCodeDecodeError.URL_PARSE -> QrCodeDecodeException.Crypto.Reason.URL_PARSE
|
||||
// LoginQrCodeDecodeError.INVALID_MODE -> QrCodeDecodeException.Crypto.Reason.INVALID_MODE
|
||||
// LoginQrCodeDecodeError.INVALID_VERSION -> QrCodeDecodeException.Crypto.Reason.INVALID_VERSION
|
||||
// LoginQrCodeDecodeError.BASE64 -> QrCodeDecodeException.Crypto.Reason.BASE64
|
||||
// LoginQrCodeDecodeError.INVALID_PREFIX -> QrCodeDecodeException.Crypto.Reason.INVALID_PREFIX
|
||||
// }
|
||||
QrCodeDecodeException.Crypto(
|
||||
qrCodeDecodeException.message.orEmpty(),
|
||||
// reason
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun map(humanQrLoginError: RustHumanQrLoginException): QrLoginException = when (humanQrLoginError) {
|
||||
is RustHumanQrLoginException.Cancelled -> QrLoginException.Cancelled
|
||||
is RustHumanQrLoginException.ConnectionInsecure -> QrLoginException.ConnectionInsecure
|
||||
is RustHumanQrLoginException.Declined -> QrLoginException.Declined
|
||||
is RustHumanQrLoginException.Expired -> QrLoginException.Expired
|
||||
is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn
|
||||
is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported
|
||||
is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown
|
||||
is RustHumanQrLoginException.OidcMetadataInvalid -> QrLoginException.OidcMetadataInvalid
|
||||
is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.libraries.matrix.impl.auth.qrlogin
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import org.matrix.rustcomponents.sdk.QrLoginProgress
|
||||
|
||||
fun QrLoginProgress.toStep(): QrCodeLoginStep {
|
||||
return when (this) {
|
||||
is QrLoginProgress.EstablishingSecureChannel -> QrCodeLoginStep.EstablishingSecureChannel(checkCodeString)
|
||||
is QrLoginProgress.Starting -> QrCodeLoginStep.Starting
|
||||
is QrLoginProgress.WaitingForToken -> QrCodeLoginStep.WaitingForToken(userCode)
|
||||
is QrLoginProgress.Done -> QrCodeLoginStep.Finished
|
||||
}
|
||||
}
|
||||
@@ -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.libraries.matrix.impl.auth.qrlogin
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory
|
||||
import org.matrix.rustcomponents.sdk.QrCodeData
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class RustQrCodeLoginDataFactory @Inject constructor() : MatrixQrCodeLoginDataFactory {
|
||||
override fun parseQrCodeData(data: ByteArray): Result<MatrixQrCodeLoginData> {
|
||||
return runCatching { SdkQrCodeLoginData(QrCodeData.fromBytes(data)) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.libraries.matrix.impl.auth.qrlogin
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import org.matrix.rustcomponents.sdk.QrCodeData as RustQrCodeData
|
||||
|
||||
class SdkQrCodeLoginData(
|
||||
internal val rustQrCodeData: RustQrCodeData,
|
||||
) : MatrixQrCodeLoginData
|
||||
@@ -20,9 +20,13 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
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.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -32,7 +36,9 @@ import kotlinx.coroutines.flow.flowOf
|
||||
val A_OIDC_DATA = OidcDetails(url = "a-url")
|
||||
|
||||
class FakeMatrixAuthenticationService(
|
||||
private val matrixClientResult: ((SessionId) -> Result<MatrixClient>)? = null
|
||||
var matrixClientResult: ((SessionId) -> Result<MatrixClient>)? = null,
|
||||
var loginWithQrCodeResult: (qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) -> Result<SessionId> =
|
||||
lambdaRecorder<MatrixQrCodeLoginData, (QrCodeLoginStep) -> Unit, Result<SessionId>> { _, _ -> Result.success(A_SESSION_ID) },
|
||||
) : MatrixAuthenticationService {
|
||||
private val homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||
private var oidcError: Throwable? = null
|
||||
@@ -50,8 +56,8 @@ class FakeMatrixAuthenticationService(
|
||||
override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda()
|
||||
|
||||
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> {
|
||||
if (matrixClientResult != null) {
|
||||
return matrixClientResult.invoke(sessionId)
|
||||
matrixClientResult?.let {
|
||||
return it.invoke(sessionId)
|
||||
}
|
||||
return if (matrixClient != null) {
|
||||
Result.success(matrixClient!!)
|
||||
@@ -88,6 +94,10 @@ class FakeMatrixAuthenticationService(
|
||||
loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID)
|
||||
}
|
||||
|
||||
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit): Result<SessionId> = simulateLongTask {
|
||||
loginWithQrCodeResult(qrCodeData, progress)
|
||||
}
|
||||
|
||||
fun givenOidcError(throwable: Throwable?) {
|
||||
oidcError = throwable
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user