diff --git a/.maestro/tests/account/login.yaml b/.maestro/tests/account/login.yaml
index 69833a985e..18054297d6 100644
--- a/.maestro/tests/account/login.yaml
+++ b/.maestro/tests/account/login.yaml
@@ -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
diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt
similarity index 78%
rename from features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingConfig.kt
rename to appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt
index 969ac382a4..e2a78fc522 100644
--- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/OnBoardingConfig.kt
@@ -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
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt
index fae6d586aa..72d2402eef 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt
@@ -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()
}
}
diff --git a/features/ftue/impl/src/main/res/values/localazy.xml b/features/ftue/impl/src/main/res/values/localazy.xml
index 4a48b517da..7a2e02514f 100644
--- a/features/ftue/impl/src/main/res/values/localazy.xml
+++ b/features/ftue/impl/src/main/res/values/localazy.xml
@@ -11,11 +11,11 @@
"Connection not secure"
"You’ll be asked to enter the two digits shown on this device."
"Enter the number below on your other device"
- "Sign in to your other device and then try again, or use another device that’s already signed in."
- "Other device not signed in"
+ "Sign in to your other device and then try again, or use another device that’s already signed in."
+ "Other device not signed in"
"The sign in was cancelled on the other device."
"Sign in request cancelled"
- "The request on your other device was not accepted."
+ "The sign in was declined on the other device."
"Sign in declined"
"Sign in expired. Please try again."
"The sign in was not completed in time"
diff --git a/features/login/api/build.gradle.kts b/features/login/api/build.gradle.kts
index 5b7bddb15f..97fb3bec04 100644
--- a/features/login/api/build.gradle.kts
+++ b/features/login/api/build.gradle.kts
@@ -15,6 +15,7 @@
*/
plugins {
id("io.element.android-library")
+ id("kotlin-parcelize")
}
android {
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt
index 07a546192d..545dbb9f45 100644
--- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt
@@ -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
+}
diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts
index 47ddd893fa..25bf1dc1b3 100644
--- a/features/login/impl/build.gradle.kts
+++ b/features/login/impl/build.gradle.kts
@@ -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)
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt
index 633fc43b5c..914b60756e 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt
@@ -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
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
index 6b7f9de6f4..0af102a403 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
@@ -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(
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(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) {
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt
new file mode 100644
index 0000000000..d16a69f75f
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt
@@ -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
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt
new file mode 100644
index 0000000000..f92e192e40
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt
@@ -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
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginScope.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginScope.kt
new file mode 100644
index 0000000000..12d4973c2d
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginScope.kt
@@ -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()
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt
index fb86ae18a5..c07078cdff 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt
@@ -89,6 +89,6 @@ fun OidcView(
internal fun OidcViewPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) = ElementPreview {
OidcView(
state = state,
- onNavigateBack = { },
+ onNavigateBack = {},
)
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt
new file mode 100644
index 0000000000..9ab6494d95
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt
@@ -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.Uninitialized)
+ override val currentLoginStep: StateFlow = _currentLoginStep
+
+ override suspend fun authenticate(qrCodeLoginData: MatrixQrCodeLoginData): Result {
+ 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
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt
new file mode 100644
index 0000000000..4d5f0e6a95
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt
@@ -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,
+ qrCodeLoginComponentBuilder: QrCodeLoginComponent.Builder,
+ private val defaultLoginUserStory: DefaultLoginUserStory,
+ private val coroutineDispatchers: CoroutineDispatchers,
+) : BaseFlowNode(
+ 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().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(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(buildContext, plugins = listOf(callback))
+ }
+ is NavTarget.QrCodeConfirmation -> {
+ val callback = object : QrCodeConfirmationNode.Callback {
+ override fun onCancel() = reset()
+ }
+ createNode(buildContext, plugins = listOf(navTarget.step, callback))
+ }
+ is NavTarget.Error -> {
+ val callback = object : QrCodeErrorNode.Callback {
+ override fun onRetry() = reset()
+ }
+ createNode(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
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginManager.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginManager.kt
new file mode 100644
index 0000000000..cd0010dc01
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginManager.kt
@@ -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
+
+ /**
+ * 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
+
+ fun reset()
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationNode.kt
new file mode 100644
index 0000000000..038d9b1998
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationNode.kt
@@ -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,
+) : Node(buildContext = buildContext, plugins = plugins) {
+ interface Callback : Plugin {
+ fun onCancel()
+ }
+
+ private val step = inputs()
+
+ private fun onCancel() {
+ plugins().forEach { it.onCancel() }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ QrCodeConfirmationView(
+ step = step,
+ onCancel = ::onCancel,
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStep.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStep.kt
new file mode 100644
index 0000000000..b41433d5c8
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStep.kt
@@ -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
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStepPreviewProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStepPreviewProvider.kt
new file mode 100644
index 0000000000..cd6125fee8
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStepPreviewProvider.kt
@@ -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 {
+ override val values: Sequence
+ get() = sequenceOf(
+ QrCodeConfirmationStep.DisplayCheckCode("12"),
+ QrCodeConfirmationStep.DisplayVerificationCode("123456"),
+ QrCodeConfirmationStep.DisplayVerificationCode("123456789"),
+ )
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationView.kt
new file mode 100644
index 0000000000..825dbe3ad1
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationView.kt
@@ -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 = {},
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorNode.kt
new file mode 100644
index 0000000000..66ddd8bfbe
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorNode.kt
@@ -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,
+ private val buildMeta: BuildMeta,
+) : Node(buildContext = buildContext, plugins = plugins) {
+ interface Callback : Plugin {
+ fun onRetry()
+ }
+
+ private fun onRetry() {
+ plugins().forEach { it.onRetry() }
+ }
+
+ private val qrCodeErrorScreenType = inputs()
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ QrCodeErrorView(
+ modifier = modifier,
+ errorScreenType = qrCodeErrorScreenType,
+ appName = buildMeta.productionApplicationName,
+ onRetry = ::onRetry,
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt
new file mode 100644
index 0000000000..04aac518e1
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt
@@ -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 {
+ override val values: Sequence = sequenceOf(
+ QrCodeErrorScreenType.Cancelled,
+ QrCodeErrorScreenType.Declined,
+ QrCodeErrorScreenType.Expired,
+ QrCodeErrorScreenType.ProtocolNotSupported,
+ QrCodeErrorScreenType.InsecureChannelDetected,
+ QrCodeErrorScreenType.SlidingSyncNotAvailable,
+ QrCodeErrorScreenType.UnknownError
+ )
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroEvents.kt
new file mode 100644
index 0000000000..982d7ac1af
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroEvents.kt
@@ -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
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroNode.kt
new file mode 100644
index 0000000000..b207462bd1
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroNode.kt
@@ -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,
+ private val presenter: QrCodeIntroPresenter,
+) : Node(buildContext, plugins = plugins) {
+ interface Callback : Plugin {
+ fun onCancelClicked()
+ fun onContinue()
+ }
+
+ private fun onCancelClicked() {
+ plugins().forEach { it.onCancelClicked() }
+ }
+
+ private fun onContinue() {
+ plugins().forEach { it.onContinue() }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = presenter.present()
+ QrCodeIntroView(
+ state = state,
+ onBackClick = ::onCancelClicked,
+ onContinue = ::onContinue,
+ modifier = modifier
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt
new file mode 100644
index 0000000000..fbe951ebb1
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt
@@ -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 {
+ 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
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroState.kt
new file mode 100644
index 0000000000..9d9609acdf
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroState.kt
@@ -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
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroStateProvider.kt
new file mode 100644
index 0000000000..7f7c62cd03
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroStateProvider.kt
@@ -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 {
+ override val values: Sequence
+ 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
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroView.kt
new file mode 100644
index 0000000000..12e8de01f0
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroView.kt
@@ -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 = {},
+ )
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanEvents.kt
new file mode 100644
index 0000000000..d4b24d68d6
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanEvents.kt
@@ -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
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanNode.kt
new file mode 100644
index 0000000000..d2c7a418b0
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanNode.kt
@@ -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,
+ private val presenter: QrCodeScanPresenter,
+) : Node(buildContext, plugins = plugins) {
+ interface Callback : Plugin {
+ fun onScannedCode(qrCodeLoginData: MatrixQrCodeLoginData)
+ fun onCancelClicked()
+ }
+
+ private fun onQrCodeDataReady(qrCodeLoginData: MatrixQrCodeLoginData) {
+ plugins().forEach { it.onScannedCode(qrCodeLoginData) }
+ }
+
+ private fun onCancelClicked() {
+ plugins().forEach { it.onCancelClicked() }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = presenter.present()
+ QrCodeScanView(
+ state = state,
+ onQrCodeDataReady = ::onQrCodeDataReady,
+ onBackClick = ::onCancelClicked,
+ modifier = modifier
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt
new file mode 100644
index 0000000000..aeedc32542
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt
@@ -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 {
+ private var isScanning by mutableStateOf(true)
+
+ private val isProcessingCode = AtomicBoolean(false)
+
+ @Composable
+ override fun present(): QrCodeScanState {
+ val coroutineScope = rememberCoroutineScope()
+ val authenticationAction: MutableState> = 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>, 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)
+ }
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanState.kt
new file mode 100644
index 0000000000..45657c0226
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanState.kt
@@ -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,
+ val eventSink: (QrCodeScanEvents) -> Unit
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt
new file mode 100644
index 0000000000..764c46643a
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt
@@ -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 {
+ override val values: Sequence
+ 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 = AsyncAction.Uninitialized,
+ eventSink: (QrCodeScanEvents) -> Unit = {},
+) = QrCodeScanState(
+ isScanning = isScanning,
+ authenticationAction = authenticationAction,
+ eventSink = eventSink
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt
new file mode 100644
index 0000000000..97925c11ad
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt
@@ -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 = {},
+ )
+}
diff --git a/features/login/impl/src/main/res/values-be/translations.xml b/features/login/impl/src/main/res/values-be/translations.xml
index 228deb957c..f393c527ae 100644
--- a/features/login/impl/src/main/res/values-be/translations.xml
+++ b/features/login/impl/src/main/res/values-be/translations.xml
@@ -30,6 +30,33 @@
"Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі."
"Сардэчна запрашаем!"
"Увайдзіце ў %1$s"
+ "Ўсталяванне бяспечнага злучэння"
+ "Не атрымалася ўсталяваць бяспечнае злучэнне з новай прыладай. Існуючыя прылады па-ранейшаму ў бяспецы, і вам не трэба турбавацца пра іх."
+ "Што зараз?"
+ "Паспрабуйце зноў увайсці ў сістэму з дапамогай QR-кода, калі гэта была сеткавая праблема"
+ "Калі вы сутыкнуліся з той жа праблемай, паспрабуйце іншую сетку Wi-Fi або скарыстайцеся мабільнымі дадзенымі замест Wi-Fi."
+ "Калі гэта не дапамагло, увайдзіце ўручную"
+ "Злучэнне небяспечнае"
+ "Вам будзе прапанавана ўвесці дзве лічбы, паказаныя на гэтай прыладзе."
+ "Увядзіце наступны нумар на іншай прыладзе."
+ "Адкрыйце %1$s на настольнай прыладзе"
+ "Націсніце на свой аватар"
+ "Выберыце %1$s"
+ "“Звязаць новую прыладу”"
+ "Выконвайце паказаныя інструкцыі"
+ "Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"
+ "Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."
+ "Паўтарыць спробу"
+ "Няправільны QR-код"
+ "Перайсці ў налады камеры"
+ "Каб працягнуць, вам неабходна дазволіць %1$s выкарыстоўваць камеру вашай прылады."
+ "Дазвольце доступ да камеры для сканавання QR-кода"
+ "Сканаваць QR-код"
+ "Пачаць спачатку"
+ "Адбылася нечаканая памылка. Калі ласка, паспрабуйце яшчэ раз."
+ "У чаканні іншай прылады"
+ "Ваш правайдэр уліковага запісу можа запытаць наступны код для праверкі ўваходу."
+ "Ваш код спраўджання"
"Змяніць правайдара ўліковага запісу"
"Прыватны сервер для супрацоўнікаў Element."
"Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі."
diff --git a/features/login/impl/src/main/res/values-bg/translations.xml b/features/login/impl/src/main/res/values-bg/translations.xml
index f4e52f7ac6..cfc51c3f8c 100644
--- a/features/login/impl/src/main/res/values-bg/translations.xml
+++ b/features/login/impl/src/main/res/values-bg/translations.xml
@@ -18,6 +18,7 @@
"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."
"Добре дошли отново!"
"Влизане в %1$s"
+ "Повторен опит"
"Промяна на доставчика на акаунт"
"Matrix е отворена мрежа за сигурна, децентрализирана комуникация."
"Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."
diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml
index 0616f621b1..ae5981e3c0 100644
--- a/features/login/impl/src/main/res/values-cs/translations.xml
+++ b/features/login/impl/src/main/res/values-cs/translations.xml
@@ -30,6 +30,33 @@
"Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci."
"Vítejte zpět!"
"Přihlaste se k %1$s"
+ "Navazování zabezpečeného spojení"
+ "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."
+ "Co teď?"
+ "Zkuste se znovu přihlásit pomocí QR kódu v případě, že se jednalo o problém se sítí"
+ "Pokud narazíte na stejný problém, zkuste jinou síť wifi nebo použijte mobilní data místo wifi"
+ "Pokud to nefunguje, přihlaste se ručně"
+ "Připojení není zabezpečené"
+ "Budete požádáni o zadání dvou níže uvedených číslic."
+ "Zadejte níže uvedené číslo na svém dalším zařízení"
+ "Otevřete %1$s na stolním počítači"
+ "Klikněte na svůj avatar"
+ "Vybrat %1$s"
+ "\"Připojit nové zařízení\""
+ "Postupujte podle uvedených pokynů"
+ "Otevřete %1$s na jiném zařízení pro získání QR kódu"
+ "Použijte QR kód zobrazený na druhém zařízení."
+ "Zkusit znovu"
+ "Špatný QR kód"
+ "Přejděte na nastavení fotoaparátu"
+ "Abyste mohli pokračovat, musíte aplikaci %1$s udělit povolení k použití kamery vašeho zařízení."
+ "Povolte přístup k fotoaparátu a naskenujte QR kód"
+ "Naskenujte QR kód"
+ "Začít znovu"
+ "Vyskytla se neočekávaná chyba. Prosím zkuste to znovu."
+ "Čekání na vaše další zařízení"
+ "Váš poskytovatel účtu může požádat o následující kód pro ověření přihlášení."
+ "Váš ověřovací kód"
"Změnit poskytovatele účtu"
"Soukromý server pro zaměstnance Elementu."
"Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci."
diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml
index 27fd226517..6873dd8fde 100644
--- a/features/login/impl/src/main/res/values-de/translations.xml
+++ b/features/login/impl/src/main/res/values-de/translations.xml
@@ -30,6 +30,33 @@
"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."
"Willkommen zurück!"
"Anmelden bei %1$s"
+ "Sichere Verbindung aufbauen"
+ "Es konnte keine sichere Verbindung zu dem neuen Gerät hergestellt werden."
+ "Und jetzt?"
+ "Versuche, dich erneut mit einem QR-Code anzumelden, falls dies ein Netzwerkproblem war."
+ "Wenn das Problem bestehen bleibt, versuche es mit einem anderen WLAN-Netzwerk oder verwende deine mobilen Daten statt WLAN."
+ "Wenn das nicht funktioniert, melde dich manuell an"
+ "Die Verbindung ist nicht sicher"
+ "Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben."
+ "Trage die unten angezeigte Zahl auf einem anderen Device ein"
+ "%1$s auf einem Desktop-Gerät öffnen"
+ "Klick auf deinen Avatar"
+ "Wähle %1$s"
+ "\"Neues Gerät verknüpfen\""
+ "Befolge die angezeigten Anweisungen"
+ "Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"
+ "Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."
+ "Erneut versuchen"
+ "Falscher QR-Code"
+ "Gehe zu den Kameraeinstellungen"
+ "Du musst %1$s die Erlaubnis erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren."
+ "Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes"
+ "QR-Code scannen"
+ "Neu beginnen"
+ "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut."
+ "Warten auf dein anderes Gerät"
+ "Dein Account-Provider kann nach dem folgenden Code fragen, um die Anmeldung zu bestätigen."
+ "Dein Verifizierungscode"
"Kontoanbieter wechseln"
"Ein privater Server für die Mitarbeiter von Element."
"Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."
diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml
index 31df1a3d04..c7537c9d7e 100644
--- a/features/login/impl/src/main/res/values-es/translations.xml
+++ b/features/login/impl/src/main/res/values-es/translations.xml
@@ -30,6 +30,7 @@
"Matrix es una red abierta para una comunicación segura y descentralizada."
"¡Hola de nuevo!"
"Iniciar sesión en %1$s"
+ "Inténtalo de nuevo"
"Cambiar el proveedor de la cuenta"
"Un servidor privado para los empleados de Element."
"Matrix es una red abierta para una comunicación segura y descentralizada."
diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml
index 01f11edaf6..eb94c8b0f0 100644
--- a/features/login/impl/src/main/res/values-fr/translations.xml
+++ b/features/login/impl/src/main/res/values-fr/translations.xml
@@ -30,6 +30,33 @@
"Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."
"Content de vous revoir !"
"Connectez-vous à %1$s"
+ "Établissement d’une connexion sécurisée"
+ "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."
+ "Et maintenant ?"
+ "Essayez de vous connecter à nouveau à l’aide du code QR au cas où il s’agirait d’un problème réseau"
+ "Si vous rencontrez le même problème, essayez un autre réseau wifi ou utilisez vos données mobiles au lieu du wifi"
+ "Si cela ne fonctionne pas, connectez-vous manuellement"
+ "La connexion n’est pas sécurisée"
+ "Il vous sera demandé de saisir les deux chiffres affichés sur cet appareil."
+ "Saisissez le nombre ci-dessous sur votre autre appareil"
+ "Ouvrez %1$s sur un ordinateur"
+ "Cliquez sur votre image de profil"
+ "Choisissez %1$s"
+ "“Associer une nouvelle session”"
+ "Suivez les instructions affichées"
+ "Ouvrez %1$s sur un autre appareil pour obtenir le QR code"
+ "Scannez le QR code affiché sur l’autre appareil."
+ "Essayer à nouveau"
+ "QR code erroné"
+ "Accéder aux paramètres de l’appareil photo"
+ "Vous devez autoriser %1$s à utiliser la camera de votre appareil pour continuer."
+ "Autoriser l’usage de la caméra pour scanner le code QR"
+ "Scannez le QR code"
+ "Recommencer"
+ "Une erreur inattendue s’est produite. Veuillez réessayer."
+ "En attente de votre autre session"
+ "Votre fournisseur de compte peut vous demander le code suivant pour vérifier la connexion."
+ "Votre code de vérification"
"Changer de fournisseur de compte"
"Un serveur privé pour les employés d’Element."
"Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."
diff --git a/features/login/impl/src/main/res/values-hu/translations.xml b/features/login/impl/src/main/res/values-hu/translations.xml
index f81e84a86f..9d5d99d46e 100644
--- a/features/login/impl/src/main/res/values-hu/translations.xml
+++ b/features/login/impl/src/main/res/values-hu/translations.xml
@@ -30,6 +30,33 @@
"A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz."
"Örülünk, hogy visszatért!"
"Bejelentkezés ide: %1$s"
+ "Biztonságos kapcsolat létesítése"
+ "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."
+ "Most mi lesz?"
+ "Próbáljon meg újra bejelentkezni egy QR-kóddal, ha ez hálózati probléma volt."
+ "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"
+ "Ha ez nem működik, jelentkezzen be kézileg"
+ "A kapcsolat nem biztonságos"
+ "A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet az eszközén."
+ "Adja meg az alábbi számot a másik eszközén"
+ "Nyissa meg az %1$set egy asztali eszközön"
+ "Kattintson a profilképére"
+ "Válassza ezt: %1$s"
+ "„Új eszköz összekapcsolása”"
+ "Kövesse a látható utasításokat"
+ "Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."
+ "Használja a másik eszközön látható QR-kódot."
+ "Próbálja újra"
+ "Hibás QR-kód"
+ "Ugrás a kamerabeállításokhoz"
+ "A folytatáshoz engedélyeznie kell, hogy az %1$s használhassa az eszköz kameráját."
+ "Engedélyezze a kamera elérését a QR-kód beolvasásához"
+ "Olvassa be a QR-kódot"
+ "Újrakezdés"
+ "Váratlan hiba történt. Próbálja meg újra."
+ "Várakozás a másik eszközre"
+ "A fiókszolgáltatója kérheti a következő kódot a bejelentkezése ellenőrzéséhez."
+ "Az Ön ellenőrzőkódja"
"Fiókszolgáltató módosítása"
"Egy privát kiszolgáló az Element alkalmazottai számára."
"A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz."
diff --git a/features/login/impl/src/main/res/values-in/translations.xml b/features/login/impl/src/main/res/values-in/translations.xml
index 17d61faae2..4f32383f20 100644
--- a/features/login/impl/src/main/res/values-in/translations.xml
+++ b/features/login/impl/src/main/res/values-in/translations.xml
@@ -30,6 +30,32 @@
"Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."
"Selamat datang kembali!"
"Masuk ke %1$s"
+ "Membuat koneksi"
+ "Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka."
+ "Apa sekarang?"
+ "Coba masuk lagi dengan kode QR jika ini adalah masalah jaringan"
+ "Jika Anda mengalami masalah yang sama, coba jaringan Wi-Fi yang berbeda atau gunakan data seluler Anda daripada Wi-Fi"
+ "Jika tidak berhasil, masuk secara manual"
+ "Koneksi tidak aman"
+ "Anda akan diminta untuk memasukkan dua digit yang ditunjukkan di bawah ini."
+ "Masukkan nomor di perangkat Anda"
+ "Buka %1$s di perangkat desktop"
+ "Klik pada avatar Anda"
+ "Pilih %1$s"
+ "“Tautkan perangkat baru”"
+ "Buka %1$s di perangkat lain untuk mendapatkan kode QR"
+ "Gunakan kode QR yang ditampilkan di perangkat lain."
+ "Coba lagi"
+ "Kode QR salah"
+ "Pergi ke pengaturan kamera"
+ "Anda perlu memberikan izin ke %1$s untuk menggunakan kamera perangkat Anda untuk melanjutkan."
+ "Izinkan akses kamera untuk memindai kode QR"
+ "Pindai kode QR"
+ "Mulai dari awal"
+ "Terjadi kesalahan tak terduga. Silakan coba lagi."
+ "Menunggu perangkat Anda yang lain"
+ "Penyedia akun Anda mungkin meminta kode berikut untuk memverifikasi proses masuk."
+ "Kode verifikasi Anda"
"Ubah penyedia akun"
"Server pribadi untuk karyawan Element."
"Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."
diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml
index dcfc5c7b08..59753ed926 100644
--- a/features/login/impl/src/main/res/values-it/translations.xml
+++ b/features/login/impl/src/main/res/values-it/translations.xml
@@ -30,6 +30,7 @@
"Matrix è una rete aperta per comunicazioni sicure e decentralizzate."
"Bentornato!"
"Accedi a %1$s"
+ "Riprova"
"Cambia fornitore dell\'account"
"Un server privato per i dipendenti di Element."
"Matrix è una rete aperta per comunicazioni sicure e decentralizzate."
diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml
index 8087fc7c44..af3740fe6c 100644
--- a/features/login/impl/src/main/res/values-ro/translations.xml
+++ b/features/login/impl/src/main/res/values-ro/translations.xml
@@ -30,6 +30,7 @@
"Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată."
"Bine ați revenit!"
"Conectați-vă la %1$s"
+ "Încercați din nou"
"Schimbați furnizorul contului"
"Un server privat pentru angajații Element."
"Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată."
diff --git a/features/login/impl/src/main/res/values-ru/translations.xml b/features/login/impl/src/main/res/values-ru/translations.xml
index ef643cd8a1..f1d6b4f214 100644
--- a/features/login/impl/src/main/res/values-ru/translations.xml
+++ b/features/login/impl/src/main/res/values-ru/translations.xml
@@ -30,6 +30,32 @@
"Matrix — это открытая сеть для безопасной децентрализованной связи."
"Рады видеть вас снова!"
"Войти в %1$s"
+ "Установление соединения"
+ "Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."
+ "Что теперь?"
+ "Попробуйте снова войти в систему с помощью QR-кода, если это была сетевая проблема"
+ "Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"
+ "Если это не помогло, войдите вручную"
+ "Соединение не защищено"
+ "Вам будет предложено ввести две цифры, показанные ниже."
+ "Введите номер на своем устройстве"
+ "Откройте %1$s на настольном устройстве"
+ "Нажмите на свое изображение"
+ "Выбрать %1$s"
+ "\"Привязать новое устройство\""
+ "Откройте %1$s на другом устройстве, чтобы получить QR-код"
+ "Используйте QR-код, показанный на другом устройстве."
+ "Повторить попытку"
+ "Неверный QR-код"
+ "Перейдите в настройки камеры"
+ "Чтобы продолжить, вам необходимо разрешить %1$s использовать камеру вашего устройства."
+ "Разрешите доступ к камере для сканирования QR-кода"
+ "Сканировать QR-код"
+ "Начать заново"
+ "Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз."
+ "В ожидании другого устройства"
+ "Поставщик учетной записи может запросить следующий код для подтверждения входа."
+ "Ваш код подтверждения"
"Сменить учетную запись"
"Частный сервер для сотрудников Element."
"Matrix — это открытая сеть для безопасной децентрализованной связи."
diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml
index fcc55b038e..156891980e 100644
--- a/features/login/impl/src/main/res/values-sk/translations.xml
+++ b/features/login/impl/src/main/res/values-sk/translations.xml
@@ -30,6 +30,33 @@
"Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."
"Vitajte späť!"
"Prihlásiť sa do %1$s"
+ "Nadväzovanie bezpečného spojenia"
+ "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ť."
+ "Čo teraz?"
+ "Skúste sa znova prihlásiť pomocou QR kódu v prípade, že ide o problém so sieťou"
+ "Ak narazíte na rovnaký problém, vyskúšajte inú sieť Wi-Fi alebo namiesto siete Wi-Fi použite mobilné dáta"
+ "Ak to nefunguje, prihláste sa manuálne"
+ "Pripojenie nie je bezpečené"
+ "Budete požiadaní o zadanie dvoch číslic zobrazených na tomto zariadení."
+ "Zadajte nižšie uvedené číslo na vašom druhom zariadení"
+ "Otvorte %1$s na stolnom zariadení"
+ "Kliknite na svoj obrázok"
+ "Vyberte %1$s"
+ "„Prepojiť nové zariadenie“"
+ "Postupujte podľa zobrazených pokynov"
+ "Ak chcete získať QR kód, otvorte %1$s na inom zariadení"
+ "Použite QR kód zobrazený na druhom zariadení."
+ "Skúste to znova"
+ "Nesprávny QR kód"
+ "Prejsť na nastavenia fotoaparátu"
+ "Ak chcete pokračovať, musíte udeliť povolenie aplikácii %1$s používať fotoaparát vášho zariadenia."
+ "Povoľte prístup k fotoaparátu na naskenovanie QR kódu"
+ "Naskenovať QR kód"
+ "Začať odznova"
+ "Vyskytla sa neočakávaná chyba. Prosím, skúste to znova."
+ "Čaká sa na vaše druhé zariadenie"
+ "Váš poskytovateľ účtu môže požiadať o nasledujúci kód na overenie prihlásenia."
+ "Váš overovací kód"
"Zmeniť poskytovateľa účtu"
"Súkromný server pre zamestnancov spoločnosti Element."
"Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."
diff --git a/features/login/impl/src/main/res/values-sv/translations.xml b/features/login/impl/src/main/res/values-sv/translations.xml
index 800af345fd..0362c6c0d0 100644
--- a/features/login/impl/src/main/res/values-sv/translations.xml
+++ b/features/login/impl/src/main/res/values-sv/translations.xml
@@ -30,6 +30,7 @@
"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."
"Välkommen tillbaka!"
"Logga in på %1$s"
+ "Försök igen"
"Byt kontoleverantör"
"En privat server för Element-anställda."
"Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."
diff --git a/features/login/impl/src/main/res/values-uk/translations.xml b/features/login/impl/src/main/res/values-uk/translations.xml
index df952171fc..1e01a8eba0 100644
--- a/features/login/impl/src/main/res/values-uk/translations.xml
+++ b/features/login/impl/src/main/res/values-uk/translations.xml
@@ -30,6 +30,7 @@
"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."
"З поверненням!"
"Увійти в %1$s"
+ "Спробуйте ще раз"
"Змінити провайдера облікового запису"
"Приватний сервер для співробітників Element."
"Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."
diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml
index 0127d21f8f..d345095c4b 100644
--- a/features/login/impl/src/main/res/values-zh-rTW/translations.xml
+++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml
@@ -23,6 +23,7 @@
"Matrix 是一個開放網路,為了安全且去中心化的通訊而生。"
"歡迎回來!"
"登入 %1$s"
+ "再試一次"
"更改帳號提供者"
"Matrix 是一個開放網路,為了安全且去中心化的通訊而生。"
"您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"
diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml
index f268b464f1..cade81c4f2 100644
--- a/features/login/impl/src/main/res/values/localazy.xml
+++ b/features/login/impl/src/main/res/values/localazy.xml
@@ -30,6 +30,48 @@
"Matrix is an open network for secure, decentralised communication."
"Welcome back!"
"Sign in to %1$s"
+ "Establishing a secure connection"
+ "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."
+ "What now?"
+ "Try signing in again with a QR code in case this was a network problem"
+ "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"
+ "If that doesn’t work, sign in manually"
+ "Connection not secure"
+ "You’ll be asked to enter the two digits shown on this device."
+ "Enter the number below on your other device"
+ "Sign in to your other device and then try again, or use another device that’s already signed in."
+ "Other device not signed in"
+ "The sign in was cancelled on the other device."
+ "Sign in request cancelled"
+ "The sign in was declined on the other device."
+ "Sign in declined"
+ "Sign in expired. Please try again."
+ "The sign in was not completed in time"
+ "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."
+ "QR code not supported"
+ "Your account provider does not support %1$s."
+ "%1$s not supported"
+ "Ready to scan"
+ "Open %1$s on a desktop device"
+ "Click on your avatar"
+ "Select %1$s"
+ "“Link new device”"
+ "Scan the QR code with this device"
+ "Open %1$s on another device to get the QR code"
+ "Use the QR code shown on the other device."
+ "Try again"
+ "Wrong QR code"
+ "Go to camera settings"
+ "You need to give permission for %1$s to use your device’s camera in order to continue."
+ "Allow camera access to scan the QR code"
+ "Scan the QR code"
+ "Start over"
+ "An unexpected error occurred. Please try again."
+ "Waiting for your other device"
+ "Your account provider may ask for the following code to verify the sign in."
+ "Your verification code"
"Change account provider"
"A private server for Element employees."
"Matrix is an open network for secure, decentralised communication."
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginComponent.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginComponent.kt
new file mode 100644
index 0000000000..f3a8dbd694
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginComponent.kt
@@ -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, AssistedNodeFactory<*>> {
+ return mapOf(
+ QrCodeLoginFlowNode::class.java to object : AssistedNodeFactory {
+ override fun create(buildContext: BuildContext, plugins: List): QrCodeLoginFlowNode {
+ return createNode(buildContext, plugins)
+ }
+ }
+ )
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManagerTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManagerTest.kt
new file mode 100644
index 0000000000..a3ad568cf5
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManagerTest.kt
@@ -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)
+ }
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/FakeQrCodeLoginManager.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/FakeQrCodeLoginManager.kt
new file mode 100644
index 0000000000..cc15a80e46
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/FakeQrCodeLoginManager.kt
@@ -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 =
+ lambdaRecorder> { Result.success(A_SESSION_ID) },
+ var resetAction: () -> Unit = lambdaRecorder { },
+) : QrCodeLoginManager {
+ override val currentLoginStep: MutableStateFlow =
+ MutableStateFlow(QrCodeLoginStep.Uninitialized)
+
+ override suspend fun authenticate(qrCodeLoginData: MatrixQrCodeLoginData): Result {
+ return authenticateResult(qrCodeLoginData)
+ }
+
+ override fun reset() {
+ resetAction()
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt
new file mode 100644
index 0000000000..f53e9c74f7
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt
@@ -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
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt
new file mode 100644
index 0000000000..78eae62377
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt
@@ -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()
+
+ @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 AndroidComposeTestRule.setQrCodeConfirmationView(
+ step: QrCodeConfirmationStep,
+ onCancel: () -> Unit
+ ) {
+ setContent {
+ QrCodeConfirmationView(
+ step = step,
+ onCancel = onCancel
+ )
+ }
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt
new file mode 100644
index 0000000000..f68e686f1a
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt
@@ -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()
+
+ @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 AndroidComposeTestRule.setQrCodeErrorView(
+ onRetry: () -> Unit,
+ errorScreenType: QrCodeErrorScreenType = QrCodeErrorScreenType.UnknownError,
+ appName: String = "Element X",
+ ) {
+ setContent {
+ QrCodeErrorView(
+ errorScreenType = errorScreenType,
+ appName = appName,
+ onRetry = onRetry
+ )
+ }
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenterTest.kt
new file mode 100644
index 0000000000..336e8d2114
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenterTest.kt
@@ -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,
+ )
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt
new file mode 100644
index 0000000000..6e258a4602
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt
@@ -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()
+
+ @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()
+ rule.setQrCodeIntroView(
+ state = aQrCodeIntroState(eventSink = eventRecorder),
+ )
+ rule.clickOn(CommonStrings.action_continue)
+ eventRecorder.assertSingle(QrCodeIntroEvents.Continue)
+ }
+
+ private fun AndroidComposeTestRule.setQrCodeIntroView(
+ state: QrCodeIntroState,
+ onBackClicked: () -> Unit = EnsureNeverCalled(),
+ onContinue: () -> Unit = EnsureNeverCalled(),
+ ) {
+ setContent {
+ QrCodeIntroView(
+ state = state,
+ onBackClick = onBackClicked,
+ onContinue = onContinue,
+ )
+ }
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt
new file mode 100644
index 0000000000..df79a51a41
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt
@@ -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 {
+ 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,
+ )
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt
new file mode 100644
index 0000000000..aece85f2de
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt
@@ -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()
+
+ @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(data) { callback ->
+ rule.setQrCodeScanView(
+ state = aQrCodeScanState(authenticationAction = AsyncAction.Success(data)),
+ onQrCodeDataReady = callback
+ )
+ }
+ }
+
+ private fun AndroidComposeTestRule.setQrCodeScanView(
+ state: QrCodeScanState,
+ onBackClick: () -> Unit = EnsureNeverCalled(),
+ onQrCodeDataReady: (MatrixQrCodeLoginData) -> Unit = EnsureNeverCalledWithParam(),
+ ) {
+ setContent {
+ QrCodeScanView(
+ state = state,
+ onBackClick = onBackClick,
+ onQrCodeDataReady = onQrCodeDataReady
+ )
+ }
+ }
+}
diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt
index 9ccd7de985..3549956844 100644
--- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt
+++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt
@@ -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(
diff --git a/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt b/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt
index a70f14dc43..ddb3643dc1 100644
--- a/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt
+++ b/features/onboarding/api/src/main/kotlin/io/element/android/features/onboarding/api/OnBoardingEntryPoint.kt
@@ -32,6 +32,7 @@ interface OnBoardingEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onSignUp()
fun onSignIn()
+ fun onSignInWithQrCode()
fun onOpenDeveloperSettings()
fun onReportProblem()
}
diff --git a/features/onboarding/impl/build.gradle.kts b/features/onboarding/impl/build.gradle.kts
index 9994eacf81..a8c7b0db00 100644
--- a/features/onboarding/impl/build.gradle.kts
+++ b/features/onboarding/impl/build.gradle.kts
@@ -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)
}
diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt
index f7d829d5fe..faf3e3be8d 100644
--- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt
+++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt
@@ -45,6 +45,10 @@ class OnBoardingNode @AssistedInject constructor(
plugins().forEach { it.onSignUp() }
}
+ private fun onSignInWithQrCode() {
+ plugins().forEach { it.onSignInWithQrCode() }
+ }
+
private fun onOpenDeveloperSettings() {
plugins().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,
)
diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt
index bfe6c06e00..ed7200310f 100644
--- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt
+++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt
@@ -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 {
@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,
)
}
diff --git a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt
index 1eaf60a8f9..651d8c5cf4 100644
--- a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt
+++ b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt
@@ -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()
}
}
}
diff --git a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt
new file mode 100644
index 0000000000..9a83573807
--- /dev/null
+++ b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt
@@ -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()
+
+ @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 AndroidComposeTestRule.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,
+ )
+ }
+ }
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt
index 420c6f5675..202766e4ac 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt
@@ -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
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt
index 845de55e0c..c1ce30724e 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt
@@ -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)
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt
index b2b1e2a396..190f423bfe 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt
@@ -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(
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt
index f18d013014..cbb46849a9 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt
@@ -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) }
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt
index 2cefff29b4..6ade84be20 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt
@@ -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)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2617aa077e..133edcf126 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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"
diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts
index 6cb1f4f7d2..13d5e18e1a 100644
--- a/libraries/designsystem/build.gradle.kts
+++ b/libraries/designsystem/build.gradle.kts
@@ -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)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/NumberedListMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/NumberedListMolecule.kt
new file mode 100644
index 0000000000..596995a2af
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/NumberedListMolecule.kt
@@ -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,
+ )
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/NumberedListOrganism.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/NumberedListOrganism.kt
new file mode 100644
index 0000000000..ccdd875939
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/NumberedListOrganism.kt
@@ -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,
+ modifier: Modifier = Modifier,
+) {
+ LazyColumn(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(24.dp),
+ ) {
+ itemsIndexed(items) { index, item ->
+ NumberedListMolecule(index = index + 1, text = item)
+ }
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt
index 789e4a3a18..9218061638 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt
@@ -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 = { })
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt
index 20f2396b7a..13e514c63a 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt
@@ -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,
)
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CornerBorder.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CornerBorder.kt
new file mode 100644
index 0000000000..b8d60f9acf
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CornerBorder.kt
@@ -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,
+ ),
+ )
+ }
+ }
+)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt
index fa35864c45..7f27927030 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt
@@ -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,
)
}
},
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/AnnotatedString.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/AnnotatedString.kt
new file mode 100644
index 0000000000..562b0033b4
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/AnnotatedString.kt
@@ -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)
+ }
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientation.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientation.kt
new file mode 100644
index 0000000000..e3a349d479
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientation.kt
@@ -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
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientationInMobileDevices.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientationInMobileDevices.kt
new file mode 100644
index 0000000000..0cab1f406a
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientationInMobileDevices.kt
@@ -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)
+ }
+}
diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
index 1a0602fd15..5c3bd8efd4 100644
--- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
+++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
@@ -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,
+ ),
}
diff --git a/libraries/featureflag/impl/build.gradle.kts b/libraries/featureflag/impl/build.gradle.kts
index 64fcd1eece..76b6836eb3 100644
--- a/libraries/featureflag/impl/build.gradle.kts
+++ b/libraries/featureflag/impl/build.gradle.kts
@@ -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)
diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt
index ac5e0d56cf..0a6b97e13d 100644
--- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt
+++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt
@@ -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()
.sortedBy(FeatureFlagProvider::priority)
.firstOrNull()
?.setFeatureEnabled(feature, enabled)
diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt
index d8b485871a..139877500b 100644
--- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt
+++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt
@@ -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
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt
index 501b40508e..c7d3e79144 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt
@@ -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
+
+ suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit): Result
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt
new file mode 100644
index 0000000000..541675b0a8
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt
@@ -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
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginDataFactory.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginDataFactory.kt
new file mode 100644
index 0000000000..0258d55106
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginDataFactory.kt
@@ -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
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrCodeDecodeException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrCodeDecodeException.kt
new file mode 100644
index 0000000000..a0719fa8f0
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrCodeDecodeException.kt
@@ -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
+// }
+ }
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrCodeLoginStep.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrCodeLoginStep.kt
new file mode 100644
index 0000000000..4ecc7a6cd6
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrCodeLoginStep.kt
@@ -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
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrLoginException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrLoginException.kt
new file mode 100644
index 0000000000..0a31a46236
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrLoginException.kt
@@ -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()
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
index 80302933d5..cb0fbbefa4 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
@@ -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(
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
index 50d82c1e3a..d992c7fc1c 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
@@ -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")
+ }
+ }
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapper.kt
new file mode 100644
index 0000000000..e55b662b72
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapper.kt
@@ -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
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrLoginProgressExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrLoginProgressExtensions.kt
new file mode 100644
index 0000000000..1dc6029700
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrLoginProgressExtensions.kt
@@ -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
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt
new file mode 100644
index 0000000000..9cf55e018a
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt
@@ -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 {
+ return runCatching { SdkQrCodeLoginData(QrCodeData.fromBytes(data)) }
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginData.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginData.kt
new file mode 100644
index 0000000000..da24fbf4bb
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginData.kt
@@ -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
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt
index 9cec3cbecf..920ad130df 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt
@@ -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)? = null
+ var matrixClientResult: ((SessionId) -> Result)? = null,
+ var loginWithQrCodeResult: (qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) -> Result =
+ lambdaRecorder Unit, Result> { _, _ -> Result.success(A_SESSION_ID) },
) : MatrixAuthenticationService {
private val homeserver = MutableStateFlow(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 {
- 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 = simulateLongTask {
+ loginWithQrCodeResult(qrCodeData, progress)
+ }
+
fun givenOidcError(throwable: Throwable?) {
oidcError = throwable
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/qrlogin/FakeMatrixQrCodeLoginDataFactory.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/qrlogin/FakeMatrixQrCodeLoginDataFactory.kt
new file mode 100644
index 0000000000..6e542dd739
--- /dev/null
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/qrlogin/FakeMatrixQrCodeLoginDataFactory.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.test.auth.qrlogin
+
+import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
+import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+
+class FakeMatrixQrCodeLoginDataFactory(
+ var parseQrCodeLoginDataResult: () -> Result =
+ lambdaRecorder> { Result.success(FakeMatrixQrCodeLoginData()) },
+) : MatrixQrCodeLoginDataFactory {
+ override fun parseQrCodeData(data: ByteArray): Result {
+ return parseQrCodeLoginDataResult()
+ }
+}
+
+class FakeMatrixQrCodeLoginData : MatrixQrCodeLoginData
diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt
index 93800c55be..790084f193 100644
--- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt
+++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt
@@ -30,18 +30,22 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun PermissionsView(
state: PermissionsState,
modifier: Modifier = Modifier,
+ title: String = stringResource(id = CommonStrings.common_permission),
+ content: String? = null,
+ icon: @Composable (() -> Unit)? = null,
) {
if (state.showDialog.not()) return
ConfirmationDialog(
modifier = modifier,
- title = stringResource(id = CommonStrings.common_permission),
- content = state.permission.toDialogContent(),
+ title = title,
+ content = content ?: state.permission.toDialogContent(),
submitText = stringResource(id = CommonStrings.action_open_settings),
onSubmitClick = {
state.eventSink.invoke(PermissionsEvents.OpenSystemSettingAndCloseDialog)
},
onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) },
+ icon = icon,
)
}
diff --git a/libraries/qrcode/build.gradle.kts b/libraries/qrcode/build.gradle.kts
new file mode 100644
index 0000000000..65aa597bf1
--- /dev/null
+++ b/libraries/qrcode/build.gradle.kts
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.libraries.qrcode"
+}
+
+dependencies {
+ implementation(projects.libraries.designsystem)
+ implementation(libs.androidx.camera.lifecycle)
+ implementation(libs.androidx.camera.view)
+ implementation(libs.androidx.camera.camera2)
+ implementation(libs.zxing.cpp)
+}
diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt
new file mode 100644
index 0000000000..be83581642
--- /dev/null
+++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.qrcode
+
+import android.graphics.ImageFormat
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageProxy
+import timber.log.Timber
+import zxingcpp.BarcodeReader
+
+internal class QRCodeAnalyzer(
+ private val onScanQrCode: (result: ByteArray?) -> Unit
+) : ImageAnalysis.Analyzer {
+ private val reader by lazy { BarcodeReader() }
+
+ override fun analyze(image: ImageProxy) {
+ if (image.format in SUPPORTED_IMAGE_FORMATS) {
+ try {
+ val bytes = reader.read(image).firstNotNullOfOrNull { it.bytes }
+ bytes?.let { onScanQrCode(it) }
+ } catch (e: Exception) {
+ Timber.w(e, "Error decoding QR code")
+ } finally {
+ image.close()
+ }
+ }
+ }
+
+ companion object {
+ private val SUPPORTED_IMAGE_FORMATS = listOf(
+ ImageFormat.YUV_420_888,
+ ImageFormat.YUV_422_888,
+ ImageFormat.YUV_444_888,
+ )
+ }
+}
diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt
new file mode 100644
index 0000000000..428ff0e3d4
--- /dev/null
+++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.qrcode
+
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+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.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.designsystem.theme.components.Text
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+@Composable
+fun QrCodeCameraView(
+ onScanQrCode: (ByteArray) -> Unit,
+ modifier: Modifier = Modifier,
+ renderPreview: Boolean = true,
+) {
+ if (LocalInspectionMode.current) {
+ Box(
+ modifier = modifier
+ .background(color = ElementTheme.colors.bgSubtlePrimary),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text("CameraView")
+ }
+ } else {
+ val coroutineScope = rememberCoroutineScope()
+ val localContext = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ var cameraProvider by remember { mutableStateOf(null) }
+ val previewUseCase = remember { Preview.Builder().build() }
+ var lastFrame by remember { mutableStateOf(null) }
+ val imageAnalysis = ImageAnalysis.Builder()
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+ .build()
+
+ LaunchedEffect(Unit) {
+ cameraProvider = localContext.getCameraProvider()
+ }
+
+ suspend fun startQRCodeAnalysis(cameraProvider: ProcessCameraProvider, previewView: PreviewView, attempt: Int = 1) {
+ lastFrame = null
+ val cameraSelector = CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+ .build()
+ imageAnalysis.setAnalyzer(
+ ContextCompat.getMainExecutor(previewView.context),
+ QRCodeAnalyzer { result ->
+ result?.let {
+ Timber.d("QR code scanned!")
+ onScanQrCode(it)
+ }
+ }
+ )
+ try {
+ // Make sure we unbind all use cases before binding them again
+ cameraProvider.unbindAll()
+
+ cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ previewUseCase,
+ imageAnalysis
+ )
+ lastFrame = null
+ } catch (e: Exception) {
+ val maxAttempts = 3
+ if (attempt > maxAttempts) {
+ Timber.e(e, "Use case binding failed after $maxAttempts attempts. Giving up.")
+ } else {
+ Timber.e(e, "Use case binding failed (attempt #$attempt). Retrying after a delay...")
+ delay(100)
+ startQRCodeAnalysis(cameraProvider, previewView, attempt + 1)
+ }
+ }
+ }
+
+ fun stopQRCodeAnalysis(previewView: PreviewView) {
+ // Stop analyzer
+ imageAnalysis.clearAnalyzer()
+
+ // Save last frame to display it as the 'frozen' preview
+ if (lastFrame == null) {
+ lastFrame = previewView.bitmap
+ Timber.d("Saving last frame for frozen preview.")
+ }
+
+ // Unbind preview use case
+ cameraProvider?.unbindAll()
+ }
+
+ Box(modifier.clipToBounds()) {
+ AndroidView(
+ factory = { context ->
+ val previewView = PreviewView(context)
+ previewUseCase.setSurfaceProvider(previewView.surfaceProvider)
+ previewView.previewStreamState.observe(lifecycleOwner) { state ->
+ previewView.alpha = if (state == PreviewView.StreamState.STREAMING) 1f else 0f
+ }
+ previewView
+ },
+ update = { previewView ->
+ if (renderPreview) {
+ cameraProvider?.let { provider ->
+ coroutineScope.launch { startQRCodeAnalysis(provider, previewView) }
+ }
+ } else {
+ stopQRCodeAnalysis(previewView)
+ }
+ },
+ onRelease = {
+ cameraProvider?.unbindAll()
+ cameraProvider = null
+ },
+ )
+ lastFrame?.let {
+ Image(bitmap = it.asImageBitmap(), contentDescription = null)
+ }
+ }
+ }
+}
+
+@Suppress("BlockingMethodInNonBlockingContext")
+private suspend fun Context.getCameraProvider(): ProcessCameraProvider =
+ suspendCoroutine { continuation ->
+ ProcessCameraProvider.getInstance(this).also { cameraProvider ->
+ cameraProvider.addListener({
+ continuation.resume(cameraProvider.get())
+ }, ContextCompat.getMainExecutor(this))
+ }
+ }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index dd48d5d4af..b154f4585e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -36,9 +36,15 @@ dependencyResolutionManagement {
includeModule("io.element.android", "wysiwyg-compose")
}
}
+ // To have immediate access to Rust SDK versions without a sync with Maven Central
+ maven {
+ url = URI("https://s01.oss.sonatype.org/content/repositories/releases")
+ content {
+ includeModule("org.matrix.rustcomponents", "sdk-android")
+ }
+ }
google()
mavenCentral()
- maven { url = URI("https://oss.sonatype.org/content/repositories/snapshots/") }
maven {
url = URI("https://www.jitpack.io")
content {
@@ -46,10 +52,6 @@ dependencyResolutionManagement {
includeModule("com.github.matrix-org", "matrix-analytics-events")
}
}
- // To have immediate access to Rust SDK versions
- maven {
- url = URI("https://s01.oss.sonatype.org/content/repositories/releases")
- }
flatDir {
dirs("libraries/matrix/libs")
}
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_RootView_null_RootView-Day-3_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_RootView_null_RootView-Day-3_3_null_2,NEXUS_5,1.0,en].png
index e1103596c2..17904a637b 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_RootView_null_RootView-Day-3_3_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_RootView_null_RootView-Day-3_3_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f606c73de0ccec703eb7a6eb374d014438ed6fda829fafe30a6757402786b59f
-size 25590
+oid sha256:316dabc0ae87bc30ae039f60a7e51975137b590184954cf805f5faa919dcf833
+size 25586
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_RootView_null_RootView-Night-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_RootView_null_RootView-Night-3_4_null_2,NEXUS_5,1.0,en].png
index c12b0c44e8..18f05bf6ce 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_RootView_null_RootView-Night-3_4_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_RootView_null_RootView-Night-3_4_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:70e4e94aad224d9c3d71e40bb080030455d07d87c495ac4bdc3f54aff28a4eb4
-size 21916
+oid sha256:0ae32b3f2dee34fb5da208d3052dc92adb21df04c61667fb7aa397bb5056f551
+size 21909
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_LeaveRoomView_null_LeaveRoomView-Day-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_LeaveRoomView_null_LeaveRoomView-Day-0_1_null_6,NEXUS_5,1.0,en].png
index 00ac5ac158..831f415574 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_LeaveRoomView_null_LeaveRoomView-Day-0_1_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_LeaveRoomView_null_LeaveRoomView-Day-0_1_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:925d6129884511860505ed7ba3cec9f2ddb50105aa1f74d9ff2dc8bbed22f8d4
-size 37706
+oid sha256:f933a8b29c082f1db2cd3ea7b41b4c4d61c450a5e55b4ff9e13b0b50334764cc
+size 37729
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_LeaveRoomView_null_LeaveRoomView-Night-0_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_LeaveRoomView_null_LeaveRoomView-Night-0_2_null_6,NEXUS_5,1.0,en].png
index c6ddfe89fc..687b00511e 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_LeaveRoomView_null_LeaveRoomView-Night-0_2_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_LeaveRoomView_null_LeaveRoomView-Night-0_2_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a46f95f72addd2775c6dfd4dcbfe2bfb2f68eb0eaa4fe2676c3d17b285390e27
-size 32718
+oid sha256:6874f456b41979ad743f99423a4fce35d188c55e3f8f23d9da02d7335207e2b8
+size 32698
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_LockScreenSettingsView_null_LockScreenSettingsView-Day-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_LockScreenSettingsView_null_LockScreenSettingsView-Day-1_2_null_2,NEXUS_5,1.0,en].png
index 3c8fe9a75f..6fd25ee44c 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_LockScreenSettingsView_null_LockScreenSettingsView-Day-1_2_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_LockScreenSettingsView_null_LockScreenSettingsView-Day-1_2_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3f6e3444e39ff0954d9a1b82257754cbb2950d3054681433378d3fc6a5c69f31
-size 32042
+oid sha256:93b933f74b0d6c41d907cf86f79e8df3d1c18b6b45772b2f6c8fcb3714c796b1
+size 32038
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_LockScreenSettingsView_null_LockScreenSettingsView-Night-1_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_LockScreenSettingsView_null_LockScreenSettingsView-Night-1_3_null_2,NEXUS_5,1.0,en].png
index 14d8ea60c5..6c2d4625ef 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_LockScreenSettingsView_null_LockScreenSettingsView-Night-1_3_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_LockScreenSettingsView_null_LockScreenSettingsView-Night-1_3_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:78cf15e91facec9b09d80df595575ace80b167be17ee014115ddb461610effa0
-size 28371
+oid sha256:491522503cf99e052f5740abe6e73511e5eab7fd464011501aeecccb8f033def
+size 28325
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Day-3_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Day-3_4_null_3,NEXUS_5,1.0,en].png
index b7f6aac717..a0ce47097a 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Day-3_4_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Day-3_4_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:93263a32ec800656943f59f3d733c1b816c8f5c386b677584e58c525b4e79d6d
-size 28626
+oid sha256:e724044b995b58ea7f5be8e28f081344d5a9c440f90c82f1a45dbd7e039564aa
+size 28539
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Day-3_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Day-3_4_null_4,NEXUS_5,1.0,en].png
index 66635f0482..27f6e91dc2 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Day-3_4_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Day-3_4_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6d7564e74b4dac299a5a864179df71aa957ed7fd21f9e5a6c4f3ca0741538c5e
-size 35605
+oid sha256:4a2e72394e1a959423839b9975abe459fe9a3e682fa3da9d940a60caaffb0e71
+size 35600
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Night-3_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Night-3_5_null_3,NEXUS_5,1.0,en].png
index 69e4040c24..55a038e571 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Night-3_5_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Night-3_5_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9b23435a077d09cc2e2b514b071510078b885045e0b9404cc2b27577d4323367
-size 25185
+oid sha256:e3ac390e401966f7004a59378d998d7f073bba1c5f0675f6fdcf476652a77fa3
+size 25173
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Night-3_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Night-3_5_null_4,NEXUS_5,1.0,en].png
index 804737dd4f..a176f8bbb5 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Night-3_5_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.pin_SetupPinView_null_SetupPinView-Night-3_5_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8d3458428cc9c96f4c24a8c071984eef3914e5714c2be5667eb718411f256c31
-size 30917
+oid sha256:8ee9a3f3e8b1de862fe2dce49f9d0616cb694fab31e214e304b96550be283005
+size 30820
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Day-7_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Day-7_8_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..9fb63483f2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Day-7_8_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bff5d329fe526b6f015c3bd6159b989c40ffa09b61dab8a715b4e98a58cbcfa
+size 35052
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Day-7_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Day-7_8_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..03544f53dc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Day-7_8_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:41f9ad3c73698d296962f66c6333a2db9c7f60f5ae3d46a26a4684350f63a6ec
+size 35668
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Day-7_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Day-7_8_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..2994746199
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Day-7_8_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:870600305d6dcc3aa83d340fc4d13eeba9291e63dc1d0cf2b9e54d4cc99fefb7
+size 37968
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Night-7_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Night-7_9_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..217e36b246
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Night-7_9_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d72ee11bb2f772760bf0b3f2f1bb54ffc6796533b78445a95409364aa4e6a6d9
+size 33145
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Night-7_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Night-7_9_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..4bf8f86621
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Night-7_9_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f7041a310e2be3c6c5374c5c337d444881f85649f29869e9539a94c89c826a21
+size 34634
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Night-7_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Night-7_9_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..14b7d0575c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_null_QrCodeConfirmationView-Night-7_9_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:49df438d7a9d3bce6b73558740270fa68a223018c373575f958590b8ad3168fb
+size 37051
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..44563689e1
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a6be99dde0529bf92ec6f0a4a2d61e22b8e3aac94dae9c343f2670692c433569
+size 23296
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..22d425c759
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7985ff4afe7ed4497dffd2e6e0143520584dca3db99858a7ace76ef77ecfaead
+size 20796
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..a3db4551e8
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ef4f42d13fa6cb50ba2179509ae0118125c7a95152d1d4bc5127868c65386121
+size 24221
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..521857e706
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4c74c21762dbd1c6dee55efeb7b9d6f90828da9b86a0d237a0466a3b16a8bfd5
+size 35997
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..29f66103ef
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5085fc11390e591242da68d7b5f79caa1c1f9f5b3af9516c68dc42c006b02318
+size 70434
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_5,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..93556f727c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_5,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb34b8a91864a13af81f871df3d50494b28e2daf3a48123c1a21c076991310b1
+size 23704
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..353f84555c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Day-8_9_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de65b622e9286884ab9ffe3102f4af5507932776f3c844089268aaf6f91b15a0
+size 23401
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..5fc511d67b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c9c08983edf7bbdc1d520769e9f22050e0b9c690de094f86db2748255582002d
+size 22218
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..f227e6a15e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ba3ea3558bf942fdeab54b93852042eac7359a53cfe1a1244e6f406caca2d082
+size 20105
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..2218673d89
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c8910d2d6b46f6eb414be78b71d32019e8c624b0bda139897d1daf81b1817778
+size 23096
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..67bbda3f54
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f039fa26a67e9ad8c8ac6631408a8cfc63bc1189cfb35dae4bbf6a47b714d0a3
+size 34445
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..e7117a9b2f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8db66d0f64278d3175ab64cd60649ceb7a8f46341d67459b1918846036d318c9
+size 64676
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_5,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..b6871bed02
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_5,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8b4bffea0e3a76f5bb6a781acc37c017675f2ff35b0844961f8a8ffb3b92ab34
+size 22717
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..eaeed57bcb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.error_QrCodeErrorView_null_QrCodeErrorView-Night-8_10_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fe6fca5fe35bb1df5453215df74c0cc8c2580c6b1d1e71db11c7a34da4bb609e
+size 22166
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Day-9_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Day-9_10_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..e509e677a6
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Day-9_10_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ded6f11e88e85a42700d1d9b88c4571853d440dcbb7d64b2d24a93a5fe0784d9
+size 45757
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Day-9_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Day-9_10_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..a366c22203
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Day-9_10_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:08f5c3928218cbb35e0edcc5ec710633bb20e120fe32c680e7e1d00503dd6895
+size 48635
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Night-9_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Night-9_11_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..2c643f5c7b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Night-9_11_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cf4ef82ef2bc40198befb73d1531b63fdcff8ce3e71a7669b97571caa94eae1a
+size 41611
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Night-9_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Night-9_11_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..3daf90cabb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.intro_QrCodeIntroView_null_QrCodeIntroView-Night-9_11_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e7d145e6ba56418918934621d4f77d13f48c5bd868938d9fdd225e2048434edf
+size 42503
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..99b1262c48
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2ba081e025288682e8044658fdc2fc3ae0e4b56e90b506850cfa6fa885fe6932
+size 18171
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..e5308fe0d7
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b747fe68c4773b6a11e744caa850a57512234b07955cef9bd9c37184345ad093
+size 23224
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..8e6de754d7
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f920d0b049006623290955fdf34103a80255bbc9bfbbf6a86fcd2d550cffdd6
+size 30170
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..56383562e5
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Day-10_11_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:38b0f09fed7ae29ea83bcec1aa00a8a0310bf493165a81d7ef0675d13df8b558
+size 36296
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..54de03b166
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:02dfb8229d4f9b28ed990d685da67dce4607a2ca1c068f2e62364bdd33403eee
+size 16948
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d3d7c03856
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:410ae4231767c9d0a1ce2347537217411ea5b24ef9483658df7f5afa489d45c1
+size 21905
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..49e60cbc16
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e426c97ea160a5adf752399e705aa4712af8308ac34205fc165ab9db741f3910
+size 28433
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..666b8bdd1a
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.qrcode.scan_QrCodeScanView_null_QrCodeScanView-Night-10_12_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5c6f46334975e38a13e63d5b1f2cc9ae9f1d8f8e04c264068be8d239fd376108
+size 34328
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Day-7_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Day-11_12_null_0,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Day-7_8_null_0,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Day-11_12_null_0,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Day-7_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Day-11_12_null_1,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Day-7_8_null_1,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Day-11_12_null_1,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Night-7_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Night-11_13_null_0,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Night-7_9_null_0,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Night-11_13_null_0,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Night-7_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Night-11_13_null_1,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Night-7_9_null_1,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Night-11_13_null_1,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_0,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-8_9_null_0,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_0,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_1,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-8_9_null_1,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_1,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-8_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_2,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-8_9_null_2,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_2,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-8_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_3,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-8_9_null_3,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_3,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-8_9_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_4,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-8_9_null_4,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_4,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-8_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_0,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-8_10_null_0,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_0,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-8_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_1,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-8_10_null_1,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_1,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-8_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_2,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-8_10_null_2,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_2,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-8_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_3,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-8_10_null_3,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_3,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-8_10_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_4,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-8_10_null_4,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_4,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_0,NEXUS_5,1.0,en].png
index f92b8d8673..3e8df831cc 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f670200a658c07e4f1a0502f4915652bb116974674fa8cb3a2abdfd15b9caac6
-size 13499
+oid sha256:06859aba2434bd7048b13551d97407ebddf77e0285e6c164f97adb296653e181
+size 13024
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_1,NEXUS_5,1.0,en].png
index ffdb837650..4c0a94eb93 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:633c3b27df8fb915f3d47deaab3a58b1c83df510d5b01f592b889c84fedea242
-size 40951
+oid sha256:6572c6d03ba8e06d72ac5cf16268e826a996340b8dba6019116e5133884ea5fa
+size 40074
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_2,NEXUS_5,1.0,en].png
index 6ecda3527c..27382bbcf5 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8ff9a8a91455b5beddd59cc1523f3182d28e2652230653f12f088e322a651ddf
-size 30433
+oid sha256:4f7ba30ddac1f1c0ecc9cf977a802a7943aae86f1360b25700f1db3dedcc19eb
+size 29865
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_3,NEXUS_5,1.0,en].png
index ffdb837650..4c0a94eb93 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:633c3b27df8fb915f3d47deaab3a58b1c83df510d5b01f592b889c84fedea242
-size 40951
+oid sha256:6572c6d03ba8e06d72ac5cf16268e826a996340b8dba6019116e5133884ea5fa
+size 40074
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_4,NEXUS_5,1.0,en].png
index 405a4695e2..d7b6cd0118 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7b835b9719f3ead626b1338875bc48e7f3e93969334221663d51917135e45d5f
-size 27524
+oid sha256:58e8a238d093ddd2869ac03cf644fecd93e509814bc930af8037d19f66250a2b
+size 27170
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_5,NEXUS_5,1.0,en].png
index f0bb97209d..a139e97e94 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0febe80eece401e8c293ae626b1641db9eeafbb65d90724d71c1fe1f4b3e7e5a
-size 21712
+oid sha256:6181c50e8937f6b3ed8209d2b693472d05c7dadc238bc8021adf46d3bf065964
+size 21350
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_6,NEXUS_5,1.0,en].png
index cfaebd5f22..94890ff13f 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:63516117fe1073f55d10d1db57c034b2a4d8849718e1f284cdfa7b17be13e444
-size 26255
+oid sha256:52b5818f5c807cf13c0cb98033a1f6631e7e71b2b5f0f5cc41eb2725a95a5de6
+size 25869
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_7,NEXUS_5,1.0,en].png
index edef3eb405..480ab7657d 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:db5b7f5ea356ab4285198bb0a150ede07d69d37006b5b347bd36574f55d5ce25
-size 38833
+oid sha256:9dc2b5a9cb4bdef8d22b3019f5458dafa14765c32524ac10785039f81439a0fd
+size 38286
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_8,NEXUS_5,1.0,en].png
index aa3f9abd52..78c9bfbdb0 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_8,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_8,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8d6aaa2a3500e1c484f84da6217a129439773deef187a5a6a6e02762b8cf7d4d
-size 36370
+oid sha256:98991b5c29d54c2f092a7250dc649099ed30328a7a0e58fb3278c125eb1fcec5
+size 35918
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_9,NEXUS_5,1.0,en].png
index 0c8b719319..b8c4f1add4 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_9,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Day-0_1_null_9,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f42ca076978903806d9e760eff37d207d7b59598093625db2803cb7390df271f
-size 37673
+oid sha256:f282e13a384916514c965ce6fded078f279918556b21e705f6fd570f8a8c826a
+size 36969
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_0,NEXUS_5,1.0,en].png
index f407d44930..1faa021c78 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6281c219f7f637d24e09f6f74c2443fe5d9e4b432404cd1434e02ee87fd94f26
-size 12892
+oid sha256:689253e0c68a31a7e9904588c69763ec10ab0de7da2e71dcd13f1a8873b4e9c3
+size 12414
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_1,NEXUS_5,1.0,en].png
index b7242acf11..6985a68ade 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4e50c6a70b8122e48fe7786a6f7b108cf78a6085f1abf77271980f28768ed9bd
-size 38404
+oid sha256:ecf872759a271301a687f2443218a8ce5491ea570966eeacec908d95225271cc
+size 37630
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_2,NEXUS_5,1.0,en].png
index 5353902d59..39583240cd 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3f81464b6bad7c990313dcbd460312c42ddc0fbbbb49711aded2ee5b7f7785e5
-size 28786
+oid sha256:a74e605de002f9f4cc540cbc6595cda476c8e6c517b13facc2013e2da3fedfa2
+size 28064
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_3,NEXUS_5,1.0,en].png
index b7242acf11..6985a68ade 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4e50c6a70b8122e48fe7786a6f7b108cf78a6085f1abf77271980f28768ed9bd
-size 38404
+oid sha256:ecf872759a271301a687f2443218a8ce5491ea570966eeacec908d95225271cc
+size 37630
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_4,NEXUS_5,1.0,en].png
index 0acd91562b..99c4509f30 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3b8415e487f9c7b33ef33d212d481b807aa11cd635a316c833a7e11f68081995
-size 23926
+oid sha256:4301bebd628ca4e9110455a87e40ae59986e0e2c388212cd7d092e218042d18b
+size 23359
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_5,NEXUS_5,1.0,en].png
index 62b78ef268..e286d8ef97 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:624f48f1f45ce3908a2fa4f9c5ff7d77e01b4dcd8992a2c4d69d5be6e76a5913
-size 18964
+oid sha256:8c8820183a52df7021d16139f1a465814d253acd8db08394680361ec209c293e
+size 18390
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_6,NEXUS_5,1.0,en].png
index 8629678a19..1c7d58d1c3 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2b92ab3a5309dc3feec01f521b7037d95a70b8e53a01cafb77b09e71d1a942ef
-size 22622
+oid sha256:da211b8fb87fbaad51fef0a6b46c33e80a2596b9273712b4e62b1c23cd607956
+size 22041
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_7,NEXUS_5,1.0,en].png
index 6f9a01acc6..8770331a3e 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7e46c2e311c3df4e8ce44eb899b8d52b21b7ce22e5e0027abf02d78270caa683
-size 36926
+oid sha256:e448f8ca835f75dcaf24d470d3c1b323f618eef1816d5463f0f6a7c0390d128a
+size 36103
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_8,NEXUS_5,1.0,en].png
index 8274ac92d7..f51db01f62 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_8,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_8,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:83f28c6bc246143085757af44d0be250c13f1799dfcab115680479dea46a953a
-size 34720
+oid sha256:90ce627d784aa1057ac8f665c384c450f296efa17596666fd1ae12556f016d1d
+size 34130
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_9,NEXUS_5,1.0,en].png
index e6ba55728e..f984aa8621 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_9,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_LogoutView_null_LogoutView-Night-0_2_null_9,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:95de2a832bf54e7586564e29c675bd97c0d3be62d65a5bdbe6ed489be2b49c28
-size 35628
+oid sha256:f0a12298a5c27be387f653c107512c8fa4009bed2f9c7efacbbc5eca025633e9
+size 35037
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_6,NEXUS_5,1.0,en].png
index 5ae2b8073f..75b5e7fe46 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cb04bfe4a65161812715d39c5c1804a376280811d6acb4cefef70453cbec6b74
-size 44791
+oid sha256:7178bf29291631942fab67ba3258ded0cbe0569909d03ef30cb07e1a2d7b573f
+size 44835
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_6,NEXUS_5,1.0,en].png
index fe2926ec48..58e5c0861e 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c729ce75714b35fe84346e72f551b0a4e63e19b1d35285b398e69258d07da99f
-size 39134
+oid sha256:86d95a745c62021f4314c3d9d20767d8b84d7e5a55cbba37534326ca812624be
+size 39181
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-7_7_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-7_7_null_7,NEXUS_5,1.0,en].png
index f2df014dda..24eca80a51 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-7_7_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-7_7_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:80fd575cdce74e271d92fb49258b79af0cfba2826320225830061a2be9b738d4
-size 30470
+oid sha256:5b3fd9f221df0df05655fb72624978d251cbd6336d1795bad9a731cae727d270
+size 30414
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-7_8_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-7_8_null_7,NEXUS_5,1.0,en].png
index b0f81d6edf..a04c52e653 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-7_8_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Night-7_8_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e10ed25980d51632b8e6dc0c5b0f81c9b137d1882d28f3d74caee0fc852beb8a
-size 25419
+oid sha256:12b50b494ade128c82cdc0c8854f3575312800b9c78eef5bdd7d68b59549884e
+size 25309
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-12_12_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-12_12_null_6,NEXUS_5,1.0,en].png
index 0618e2e36e..d945c5a6e0 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-12_12_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-12_12_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8bb41968ae644485c0e3dd51f23ffee1cfabf62d2c72df9efc577545913c87fb
-size 64304
+oid sha256:307d7ed2cfd0574fecc3f75d74562d21f956f45ed0441b12f4ee0acede8d7833
+size 64290
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-12_13_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-12_13_null_6,NEXUS_5,1.0,en].png
index 6aa63a4433..f1544ef1d9 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-12_13_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-12_13_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:29027fe574ca4681e532b127d609e0e71a9f6095a56ee81db33dd258cbb4701b
-size 60085
+oid sha256:91dfa35e176ffd7663f6a8999dd89c4f11edf051683d8327fce0ca95a695598f
+size 60067
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-14_14_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-14_14_null_6,NEXUS_5,1.0,en].png
index 1e8a0d2813..f1efceae0e 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-14_14_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-14_14_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:eee33a3534c66fb6a6ecebfdfe3518a7446880905da5bb0aae11ff260f7470eb
-size 47999
+oid sha256:9f7d1c724bff62e1dbbf6dba747efcbb60d4c9e679d67795c95fe4f97c615e47
+size 47965
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-14_15_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-14_15_null_6,NEXUS_5,1.0,en].png
index 41b61c015c..56b4f0f032 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-14_15_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-14_15_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2d332b41b0a566848e81376e335706e822a25cbd3c40233cedef3f21eba7dbf0
-size 42811
+oid sha256:7ba021673b32199838c80f1b862361c6469b9487970c4603dd428ed4a0ec3424
+size 42760
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png
index ecac9d32ec..fc13309605 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2e7892eb99a1307f2c34968f76e994e6b8e3916ff5c218eb9cbc81424001fd54
-size 64251
+oid sha256:4f8d909d70e89b2a07b349cfafb6e8d1766d7d3fd992bb1e113a73f4cdba1600
+size 63194
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png
index 6be4090def..ebd91c50fa 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dbcf18bb210a68b0e02512abd949845a8c5a705a177ce0875599734ce46da7ff
-size 57525
+oid sha256:13e59a91adb0795ddd033e3a4f666ff046317337ab06eba112a0d429c70179de
+size 56153
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_0,NEXUS_5,1.0,en].png
index f81f879d4c..abfaefd6a7 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4a44e4e825613b9074dd3540b855e04d60be9b2c92800bf8b59318bacf7e9e13
-size 58837
+oid sha256:50b7fada6e3c2d30e2cefa4d61a1b2fe48fc91e4d191e26037213a8f91188beb
+size 58244
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_1,NEXUS_5,1.0,en].png
index 9b0bab3802..ef4185de49 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a9836c41a609a4c2b6a8f4399e6eaf3a728fd8525f2e2799578e0c2a2920ee7d
-size 47750
+oid sha256:c43db1b16c8a9b4d9904fad5cd045b50ffb418b584e876019a9820f0dd4e0506
+size 48135
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_2,NEXUS_5,1.0,en].png
index 13bdef5f01..b79b386b26 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c4dd71e75099f447127611d17ba3370fad1142880b8c2cee869ae4de072a2c3a
-size 59496
+oid sha256:8c4adb79e797c3f2ff2f3363d0b39995f7f8be7429ab30b8d75c359fa3b70b07
+size 58902
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_3,NEXUS_5,1.0,en].png
index d4374e318a..98e8b74c6e 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c06a6bfa6b4633d1e621b153c33f4797ce810447b6333b16c7903f93a44b7cbc
-size 30441
+oid sha256:86b015ad42c3591afa5514e3d172115acd0c8ab97dbb2f6c40e0ce63293f6024
+size 31138
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_0,NEXUS_5,1.0,en].png
index c5610061ab..ef54fc8ce9 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:064d3ba3a36fbb3cda2f3e52a93769611f9ceb525cee57e13c93f863f02d2375
-size 57116
+oid sha256:80ada0400cb5d6324880c3d87172671142d225269b02612a7ab90d6217848339
+size 56315
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_1,NEXUS_5,1.0,en].png
index fc92b3b603..f3a78d6ddb 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:052915f0797ab55a15fa919b90c0afa0d59cb3d660be9e216d1e159def4e3289
-size 41876
+oid sha256:7a254134c8371a3619fbcbb20817d82a2bdac99b5c3478cc6b87d58f5c47fbda
+size 41985
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_2,NEXUS_5,1.0,en].png
index 95652f882d..69acbaae9f 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8725ea0ff263ffa4c679cae4353f58edaf59b9cd442c3090b3e6274e5bd5caf4
-size 57599
+oid sha256:e205d30cff1eb4da41c0969e036b79fbb5a7eb1684a3c8ff0fc3cfe8dcb3d3f0
+size 56790
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_3,NEXUS_5,1.0,en].png
index 1f9bc16a8c..ae4fd5f24a 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:afd0b803fdd2a04a73c16f7aa2e9c1b0f2b8a41ad050ba3cc753e5eef56b1047
-size 26807
+oid sha256:85f14968e422f1b82f1b6ac9cffe20fee1cdc39875da393e1a50f9f96490be34
+size 27041
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_0,NEXUS_5,1.0,en].png
index edc379c1d8..2801b31417 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d06540794a9891e734ed28540e5e090a57f83d3158827144f1e0d96ef1e169fc
-size 15499
+oid sha256:ce7c875e682e35c8d4f3ab445d4606ff3a7748012c4de45578d53891facee1c6
+size 15006
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_1,NEXUS_5,1.0,en].png
index e870073ed0..9067a84cfe 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7fbfb1d5f9040a97ae47b211b81103f77df799be1eaab12c5aa9ffd4409cb439
-size 16052
+oid sha256:04cacf7b4074b9466b56dc94bf2a3f6246c1a323fc7f823687e2c0cbd3a11084
+size 15564
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_2,NEXUS_5,1.0,en].png
index 417e7f1f47..1b030e472a 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3836c247c21c7db517fe6f5f5e07f81a22dbaeb0ba8a9b8600f0aa06b99948bf
-size 22110
+oid sha256:ef55f84387ca8c61bc5e365a00c3ec654d31feb72177a76cd6836467c03b8890
+size 21627
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_0,NEXUS_5,1.0,en].png
index d6112d12e0..d178a1240a 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fdc5662cebd3b521128523e4afa25bfa685fd60791a3d7105cdbf453821901f5
-size 14625
+oid sha256:62ffd4bf9f63a8c50feec1fa7f2f4eb8e774b671b8def15d16beb7dc45391f4f
+size 14042
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_1,NEXUS_5,1.0,en].png
index eddca87094..8996c25dbc 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:19a5d55cc8d6b234f71aaca7a052ccc167abf2e47ac7240b31cc9bcd81de0312
-size 15160
+oid sha256:a4c60dcd25ee44b5c342a70a611639303d1b4a421c8c9cfb7d215d620c7569c6
+size 14568
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_2,NEXUS_5,1.0,en].png
index 8a0ff142f9..0d7cc6779f 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6648d1d6eec0a5b18ce264435be16379aaa7422d0c5f7ca7fcfe76a9fce33e56
-size 18845
+oid sha256:691a9cff6c88d7a25a08cc31a330cdddd5b98214b7a1774278732fa1e12f6e26
+size 18252
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_0,NEXUS_5,1.0,en].png
index d8682a6a15..d8b8316e1b 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5584b334e0ca80d3619d00d1168b2a15ced299e529adb8bd53bfc9bc9e495ecf
-size 41339
+oid sha256:eeba7841b7f7bc296e20430cea930f93b99624f3f966a0246b6327ecb830050f
+size 40694
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_1,NEXUS_5,1.0,en].png
index aa07ae2e60..a0f7e7d6f2 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3ffbd8973d7370f0a992dc148b73bcc243b77b4e80d9a7ce8a49aec1312ec451
-size 52942
+oid sha256:7db21c3a4b92b7212538cb34be12ea6b59188bff5729d309c3186bc638e26bfc
+size 52335
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_2,NEXUS_5,1.0,en].png
index 5eba9096cd..74802f62b8 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8154d4dfb395001e30dfb0807e796996abb812aa40523a3ec2c7a996345a1cb0
-size 50651
+oid sha256:d276e2d2eea5624107266aed4da45bebb02f08501ea939422ceb5af98d9e5f82
+size 50167
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_3,NEXUS_5,1.0,en].png
index d9f174c0e9..e314ac25cb 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ed4451219724c9b5dfb151a3d138dd54dfba6c78b961881bf56b602813dae13b
-size 44523
+oid sha256:99ba716acde7b4ccc038b0cb7fd80acaf1ee27f016a50c783d8bede2ac35d6fe
+size 46882
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_0,NEXUS_5,1.0,en].png
index 9dd8bd3bfb..b7ee516a91 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:73b5a7598ca2255e88ef2098168e326aa1d23952ca8a43360380a6bfc2b36cce
-size 38770
+oid sha256:5b3c0ea92ed7ea695d2627c285c96b6c3aa3f2cd165ddf4b78619599a41ad7f3
+size 38016
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_1,NEXUS_5,1.0,en].png
index a5ed227845..831429ef29 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:92ec6f85b19f217c9b5242e183e868d7288c0aca3578e6e563907910197113fe
-size 49105
+oid sha256:2700e9c46eea5787fced440065d14d99eb5f6165372db114ead4b2bafbadd163
+size 48327
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_2,NEXUS_5,1.0,en].png
index ef11d24386..26f366861d 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:857fe8d13c9801edffb6ae9e36317a279addee2ea1729fc4b0be84d45e06d023
-size 47449
+oid sha256:d4d0f01db3f1bb77cdadad0c45c4589b160306d8f37729bdfb26b25294ce18f9
+size 46726
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_3,NEXUS_5,1.0,en].png
index f9e00a548e..7bc915f532 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2731b003df8b067a1d8f04ccd8209ce8f6a3de0c6469c29ea3cc1025390e378a
-size 39419
+oid sha256:157a00be84cc8636ea21ada02719b57bffdf89a7789751628c618ae15f856685
+size 41389
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_0,NEXUS_5,1.0,en].png
index 6980beec6b..96cffb1496 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dee9a6df9bffe9b49a445bdfb2c2cd75c930459300edabab15b186b62df49f72
-size 50395
+oid sha256:cb4369e39322b3a0242556041ee83d32944cdb0479afa45a0c4fb2fec09b3627
+size 49884
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_1,NEXUS_5,1.0,en].png
index e1cf5691da..2842bdafeb 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fe12143605284f0a31edd382c3a123c63f54c12c86c71557c2774d8250ed97c9
-size 48242
+oid sha256:54d7d1261502a98be76f8052c86246bd5ab145cbe15ab5361b2d09dfd81491af
+size 47617
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_2,NEXUS_5,1.0,en].png
index 57c3222d34..c08d423eca 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:77890f379265cc8feb4f3226c36da0ad29472ebcef34fee0a54bc8017855050f
-size 53139
+oid sha256:f9939e254965ce6741bb4ab3245f55553f0661d7d65fdc23f6b374f18c671a50
+size 52584
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_3,NEXUS_5,1.0,en].png
index 57c3222d34..c08d423eca 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:77890f379265cc8feb4f3226c36da0ad29472ebcef34fee0a54bc8017855050f
-size 53139
+oid sha256:f9939e254965ce6741bb4ab3245f55553f0661d7d65fdc23f6b374f18c671a50
+size 52584
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_4,NEXUS_5,1.0,en].png
index 7ffddf4401..ee16bf0bfd 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f238fc53089dcf207da554545eafec5d19a4698f63be40ea1082075d52448e4a
-size 48398
+oid sha256:e55709e69a2e7f0969e10490cc47863e988c32680880ce0d0ee91535f62811ca
+size 52951
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_0,NEXUS_5,1.0,en].png
index 1eb1581dd4..b510cf5f41 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2054149330036c4a98af731238d0d5f8d8ef9026f99a228a63ef881345489bba
-size 48472
+oid sha256:2b280312e91bd453220f05e636ec91a389c7df7a1bb6786109e38b1af99ead26
+size 47785
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_1,NEXUS_5,1.0,en].png
index 4abfdd267d..d1b6e47b92 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f098868403417e9813c2e1bf0ac318ab9d06f16c8982c6d0dffcdf70b3e77d78
-size 46227
+oid sha256:ec46aad434e4fc7c1618ac2ea658c009888afd5bedad0bc0a1259c2008f61b12
+size 45593
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_2,NEXUS_5,1.0,en].png
index cc4d7d7dbf..8c1c1af9fc 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:10b9f6d872eef32250436b7ef6461aca53350fb43eca917e884e8ed1d76b7953
-size 50507
+oid sha256:33082691eb1c24d3eb0a8c6114e01333f9e8ddd1a9e92e7806d58a53fc66ed96
+size 49810
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_3,NEXUS_5,1.0,en].png
index cc4d7d7dbf..8c1c1af9fc 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:10b9f6d872eef32250436b7ef6461aca53350fb43eca917e884e8ed1d76b7953
-size 50507
+oid sha256:33082691eb1c24d3eb0a8c6114e01333f9e8ddd1a9e92e7806d58a53fc66ed96
+size 49810
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_4,NEXUS_5,1.0,en].png
index 9dd2af19fa..c2fd162798 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:140332a81d31d039587587ee4e7f3b3ed64d12710a5ff91694faa8c267e213f8
-size 42444
+oid sha256:2780a50b42f70bbd97e20674477e4fbc61a490fc1a82910ecdd2e411fbdd7cfd
+size 46930
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_0,NEXUS_5,1.0,en].png
index dc74ff7aa7..1a88bcbb74 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1995b9571c96bc7594e3985ef4f94e76a27eecba1354be512f16e16f09c3321b
-size 51662
+oid sha256:b93fea4fce889f57554495f501dbbff862f3a2c3e2521268a33e34b491208675
+size 51127
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_1,NEXUS_5,1.0,en].png
index 3601a11e5a..c85f292761 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fad5cb07054c859fa5aae98c3c4cea992aabd8122f02a081700bba2b93a23d1f
-size 49797
+oid sha256:100cc145f871223543145127070a8a4c4e214649feb21eb0381c568872dee2a4
+size 49264
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_2,NEXUS_5,1.0,en].png
index 57c3222d34..c08d423eca 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:77890f379265cc8feb4f3226c36da0ad29472ebcef34fee0a54bc8017855050f
-size 53139
+oid sha256:f9939e254965ce6741bb4ab3245f55553f0661d7d65fdc23f6b374f18c671a50
+size 52584
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_3,NEXUS_5,1.0,en].png
index 57c3222d34..c08d423eca 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:77890f379265cc8feb4f3226c36da0ad29472ebcef34fee0a54bc8017855050f
-size 53139
+oid sha256:f9939e254965ce6741bb4ab3245f55553f0661d7d65fdc23f6b374f18c671a50
+size 52584
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_4,NEXUS_5,1.0,en].png
index 7ffddf4401..ee16bf0bfd 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f238fc53089dcf207da554545eafec5d19a4698f63be40ea1082075d52448e4a
-size 48398
+oid sha256:e55709e69a2e7f0969e10490cc47863e988c32680880ce0d0ee91535f62811ca
+size 52951
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_0,NEXUS_5,1.0,en].png
index 7cbb1cc72b..978f67cd0a 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:19c68e5ce4e45e27aa4c9fa4ef78798067131a8450a16e0193397a7bb106bd25
-size 49879
+oid sha256:c4aa4792a1719d1e0775500e58d042c140b89ffe814d6ce28604b20533c73450
+size 49183
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_1,NEXUS_5,1.0,en].png
index 4a41ae3ae3..cf21ef2df6 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:083211e979412ec942655551fdb08d94a5567775c05ea6ec7529a1e3710af5a6
-size 47882
+oid sha256:65fdd84aaee3d828f3e29f5abf79b992642b8e206534febd2c9cf9967add702b
+size 47219
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_2,NEXUS_5,1.0,en].png
index cc4d7d7dbf..8c1c1af9fc 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:10b9f6d872eef32250436b7ef6461aca53350fb43eca917e884e8ed1d76b7953
-size 50507
+oid sha256:33082691eb1c24d3eb0a8c6114e01333f9e8ddd1a9e92e7806d58a53fc66ed96
+size 49810
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_3,NEXUS_5,1.0,en].png
index cc4d7d7dbf..8c1c1af9fc 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:10b9f6d872eef32250436b7ef6461aca53350fb43eca917e884e8ed1d76b7953
-size 50507
+oid sha256:33082691eb1c24d3eb0a8c6114e01333f9e8ddd1a9e92e7806d58a53fc66ed96
+size 49810
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_4,NEXUS_5,1.0,en].png
index 9dd2af19fa..c2fd162798 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:140332a81d31d039587587ee4e7f3b3ed64d12710a5ff91694faa8c267e213f8
-size 42444
+oid sha256:2780a50b42f70bbd97e20674477e4fbc61a490fc1a82910ecdd2e411fbdd7cfd
+size 46930
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Day_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Day_0_null,NEXUS_5,1.0,en].png
index 794b0b8d25..68456e62f8 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Day_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Day_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d63d314c0e00239ab889f840abedd4ad24d49d483bd677652baf82b0b40b1c80
-size 18516
+oid sha256:b0fc699def2a690015b618b6bb3dd8363cac9ee388d580bed4ba6317c4047685
+size 18341
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Night_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Night_1_null,NEXUS_5,1.0,en].png
index c62fade2e1..94c7a8d0f2 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Night_1_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.pages_FlowStepPage_null_FlowStepPage-Night_1_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:915a7bf622b194b00bd419894b08dd025b60bdf69db6c4b253349e8eb0145698
-size 17673
+oid sha256:0108031310bbaae157a3ae39f958edd6458aa7733b772c200c68f635b6b530be
+size 17476
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_0,NEXUS_5,1.0,en].png
index a578d69128..3f5d84ad6f 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ac49f8798ffa60757fcda059c52dffe9f16271bf507f917dffadd20e461acd2a
-size 31866
+oid sha256:205fb4655ad2b32d8e882814569d4d184facbf88e4753e641c94d1f22fe36991
+size 31861
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_1,NEXUS_5,1.0,en].png
index cffb3cf5d8..86af853296 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:aa94619e6f4022ee84aca4e31393de336545b7cf6d73c0a025a1cca68ce713e0
-size 31099
+oid sha256:a4103bd575ff198ccb3e180b8f729c2c063a67f1fa1fff787a32e4fc761fca02
+size 31089
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_2,NEXUS_5,1.0,en].png
index 19e14046b1..2912f88793 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d0c1dff252324c71845437bd7fd261e7faacf98030365f3cca29763a1bf8ce24
-size 31645
+oid sha256:005c8a574e3df607d9ab177b73823e17bf1ec6fd1b3485d200dbbae6d9c22514
+size 31641
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_3,NEXUS_5,1.0,en].png
index 956d853953..1795cf1d23 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Day-0_1_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fcacc94648c996c477eb4b0179df4eeffab6d1f7106cc656c548048ae33d4476
-size 24572
+oid sha256:e03f6dd20b2004eeddc290c0a0055b612db0f9ceb7051ebedcdbc7b2c5ee0608
+size 24573
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_0,NEXUS_5,1.0,en].png
index 3a963029b6..b8f53d1705 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5ad31f0c4f91caae057ee5b5256a1fa108c998c829c00fe8a1274ea757bf4793
-size 27348
+oid sha256:7eb681677028793b13c38ebf10e63fe6d2d1c2698845bdf3a69c662fa8b9ed13
+size 27343
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_1,NEXUS_5,1.0,en].png
index 7ba9368ad2..915ba6d08c 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:18f63eb6d1891ebf4efc5679adf52ca1e3d7cdbcbc30f36be5db8e55c36589b4
-size 26530
+oid sha256:2d88a7b56daf54b0484d9481df9facddcf7ae3ae09dbf0d4533ada5ca35a9450
+size 26525
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_2,NEXUS_5,1.0,en].png
index 700e8ddcae..7e84567026 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c694d1867634284d946cc0ebcbe91b9997a523ab7ebfb9923f113d8c07753f95
-size 27044
+oid sha256:cf677e3f0b0253e254f1cae2f272c1cd7395a6976dcd4678af7085c5c8b79c49
+size 27052
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_3,NEXUS_5,1.0,en].png
index a810217e45..b4a27083a2 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_PermissionsView_null_PermissionsView-Night-0_2_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9239138fd8813cb7b28baa93a8ab7ccc60c5b9bb1f330b7e4e2ec92d7012648f
-size 20730
+oid sha256:93ab66ef80557e6e2f2ebcea91ea941e1ab9aceddc6abee6148d15bb5de6318e
+size 20735
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_null_TextComposerLinkDialogCreateLinkWithoutText-Day-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_null_TextComposerLinkDialogCreateLinkWithoutText-Day-7_8_null,NEXUS_5,1.0,en].png
index c89936dda5..e9152092f3 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_null_TextComposerLinkDialogCreateLinkWithoutText-Day-7_8_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_null_TextComposerLinkDialogCreateLinkWithoutText-Day-7_8_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:734a60dc37f2c3f5841ab477d38db034703654c531722d7560a431d5f060f113
-size 17281
+oid sha256:fd54bcdc9581bf22754248ecb29913d3f6290b57c6a6e2536a3430d037687342
+size 17238
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_null_TextComposerLinkDialogCreateLinkWithoutText-Night-7_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_null_TextComposerLinkDialogCreateLinkWithoutText-Night-7_9_null,NEXUS_5,1.0,en].png
index 5ff6a58533..4c6538a975 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_null_TextComposerLinkDialogCreateLinkWithoutText-Night-7_9_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_null_TextComposerLinkDialogCreateLinkWithoutText-Night-7_9_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b723aef3136b03ade441ebbc26b3c6705185a3d2f4918841d57f3db5ccac5c91
-size 13174
+oid sha256:95319bccc331014a3aa788fafac550e28ed2d774c0b11d4ecf98b7092c51391b
+size 13142
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLink_null_TextComposerLinkDialogCreateLink-Day-6_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLink_null_TextComposerLinkDialogCreateLink-Day-6_7_null,NEXUS_5,1.0,en].png
index df00761695..1a247f193a 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLink_null_TextComposerLinkDialogCreateLink-Day-6_7_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLink_null_TextComposerLinkDialogCreateLink-Day-6_7_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0bbbbce7dc7176979fad367430efc344da3669918a9bc4762519a9a419dff192
-size 18805
+oid sha256:1807ee3822bf8e57edd2c1fe515340529249192d268c65c4392ceecc7a5a4afe
+size 18797
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLink_null_TextComposerLinkDialogCreateLink-Night-6_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLink_null_TextComposerLinkDialogCreateLink-Night-6_8_null,NEXUS_5,1.0,en].png
index 1eee8a30f6..196d5ac08a 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLink_null_TextComposerLinkDialogCreateLink-Night-6_8_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogCreateLink_null_TextComposerLinkDialogCreateLink-Night-6_8_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:deaef5e0582fa46d67f96ce5b50fb669e59ac5f76ffd079a93c9476cd1d4fc90
-size 14417
+oid sha256:c0178e566a44b061416b71db9688816d2726df87d227d517fa203dfa3e37945d
+size 14461
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogEditLink_null_TextComposerLinkDialogEditLink-Day-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogEditLink_null_TextComposerLinkDialogEditLink-Day-8_9_null,NEXUS_5,1.0,en].png
index 32afaaec23..5deb60eb78 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogEditLink_null_TextComposerLinkDialogEditLink-Day-8_9_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogEditLink_null_TextComposerLinkDialogEditLink-Day-8_9_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d82d7f94ddedd035fba88b75360aefad2789030a60d6f291863a9ee238fc0f94
-size 21190
+oid sha256:09f36b3dcaabdc17f7fb3f95b68d8077f6bdceff539c13d67e611b6d92e3c0e3
+size 21180
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogEditLink_null_TextComposerLinkDialogEditLink-Night-8_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogEditLink_null_TextComposerLinkDialogEditLink-Night-8_10_null,NEXUS_5,1.0,en].png
index c2a890e33c..1613f1718b 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogEditLink_null_TextComposerLinkDialogEditLink-Night-8_10_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerLinkDialogEditLink_null_TextComposerLinkDialogEditLink-Night-8_10_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:787cc5ee6d40cb33b2e462a9c5a679e3bf6bd178f68f8bdaf83fc9d790dc57b1
-size 17121
+oid sha256:8ba53a3064eded7911a05a4a9ab5534dcd1398839b998c308745f3cac8805da5
+size 17123
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_AppErrorView_null_AppErrorView-Day-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_AppErrorView_null_AppErrorView-Day-0_1_null,NEXUS_5,1.0,en].png
index 5acca03c1c..730902ab3f 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_AppErrorView_null_AppErrorView-Day-0_1_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_AppErrorView_null_AppErrorView-Day-0_1_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e77894690c1423b67e975d551f019ba43e9d73899c2c75def814cfe1531e9253
-size 24040
+oid sha256:30683f023338227297991536d339326894f19835ed99ba13a21d0bba40cd2609
+size 24035
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_AppErrorView_null_AppErrorView-Night-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_AppErrorView_null_AppErrorView-Night-0_2_null,NEXUS_5,1.0,en].png
index 28c7fb7aac..cf3b168ecd 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_AppErrorView_null_AppErrorView-Night-0_2_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_AppErrorView_null_AppErrorView-Night-0_2_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e2fc386d065ab90b1f28b2fe4974546db4faf3c015c20668be5db137385209bd
+oid sha256:33e77f92a239c4ca4e5ee6b1aff07ebf27d0c5173349df21b4996b7eb7f8f7ea
size 20111
diff --git a/tools/localazy/config.json b/tools/localazy/config.json
index fa5c456e4b..380649fc3c 100644
--- a/tools/localazy/config.json
+++ b/tools/localazy/config.json
@@ -120,7 +120,8 @@
"screen_change_server_.*",
"screen_change_account_provider_.*",
"screen_account_provider_.*",
- "screen_waitlist_.*"
+ "screen_waitlist_.*",
+ "screen_qr_code_login_.*"
]
},
{