From c8bd04ceb15db1625f81b7a7b297c90970a24950 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 31 May 2024 14:38:27 +0200 Subject: [PATCH] Sign in with QR code (#2793) * Add QR code login. * Add FF to disable it in release mode. * Force portrait orientation on the login flow. * Create `NumberedList` UI components. * Improve camera permission dialog. * Make nodes in qrcode feature use `QrCodeLoginScope` instead of `AppScope` * Bump SDK version. * Fix maestro tests --------- Co-authored-by: Benoit Marty Co-authored-by: ElementBot --- .maestro/tests/account/login.yaml | 2 +- .../android/appconfig}/OnBoardingConfig.kt | 7 +- .../android/appnav/NotLoggedInFlowNode.kt | 20 +- .../impl/src/main/res/values/localazy.xml | 6 +- features/login/api/build.gradle.kts | 1 + .../features/login/api/LoginEntryPoint.kt | 11 +- features/login/impl/build.gradle.kts | 7 + .../login/impl/DefaultLoginEntryPoint.kt | 2 +- .../features/login/impl/LoginFlowNode.kt | 18 +- .../login/impl/di/QrCodeLoginBindings.kt | 25 ++ .../login/impl/di/QrCodeLoginComponent.kt | 38 +++ .../login/impl/di/QrCodeLoginScope.kt | 19 ++ .../login/impl/oidc/webview/OidcView.kt | 2 +- .../impl/qrcode/DefaultQrCodeLoginManager.kt | 54 ++++ .../login/impl/qrcode/QrCodeLoginFlowNode.kt | 248 ++++++++++++++++++ .../login/impl/qrcode/QrCodeLoginManager.kt | 41 +++ .../confirmation/QrCodeConfirmationNode.kt | 53 ++++ .../confirmation/QrCodeConfirmationStep.kt | 31 +++ .../QrCodeConfirmationStepPreviewProvider.kt | 28 ++ .../confirmation/QrCodeConfirmationView.kt | 167 ++++++++++++ .../screens/qrcode/error/QrCodeErrorNode.kt | 58 ++++ .../screens/qrcode/error/QrCodeErrorView.kt | 161 ++++++++++++ .../screens/qrcode/intro/QrCodeIntroEvents.kt | 21 ++ .../screens/qrcode/intro/QrCodeIntroNode.kt | 59 +++++ .../qrcode/intro/QrCodeIntroPresenter.kt | 68 +++++ .../screens/qrcode/intro/QrCodeIntroState.kt | 26 ++ .../qrcode/intro/QrCodeIntroStateProvider.kt | 46 ++++ .../screens/qrcode/intro/QrCodeIntroView.kt | 115 ++++++++ .../screens/qrcode/scan/QrCodeScanEvents.kt | 22 ++ .../screens/qrcode/scan/QrCodeScanNode.kt | 60 +++++ .../qrcode/scan/QrCodeScanPresenter.kt | 110 ++++++++ .../screens/qrcode/scan/QrCodeScanState.kt | 26 ++ .../qrcode/scan/QrCodeScanStateProvider.kt | 43 +++ .../screens/qrcode/scan/QrCodeScanView.kt | 214 +++++++++++++++ .../src/main/res/values-be/translations.xml | 27 ++ .../src/main/res/values-bg/translations.xml | 1 + .../src/main/res/values-cs/translations.xml | 27 ++ .../src/main/res/values-de/translations.xml | 27 ++ .../src/main/res/values-es/translations.xml | 1 + .../src/main/res/values-fr/translations.xml | 27 ++ .../src/main/res/values-hu/translations.xml | 27 ++ .../src/main/res/values-in/translations.xml | 26 ++ .../src/main/res/values-it/translations.xml | 1 + .../src/main/res/values-ro/translations.xml | 1 + .../src/main/res/values-ru/translations.xml | 26 ++ .../src/main/res/values-sk/translations.xml | 27 ++ .../src/main/res/values-sv/translations.xml | 1 + .../src/main/res/values-uk/translations.xml | 1 + .../main/res/values-zh-rTW/translations.xml | 1 + .../impl/src/main/res/values/localazy.xml | 42 +++ .../login/impl/di/FakeQrCodeLoginComponent.kt | 49 ++++ .../qrcode/DefaultQrCodeLoginManagerTest.kt | 75 ++++++ .../impl/qrcode/FakeQrCodeLoginManager.kt | 41 +++ .../impl/qrcode/QrCodeLoginFlowNodeTest.kt | 219 ++++++++++++++++ .../QrCodeConfirmationViewTest.kt | 70 +++++ .../qrcode/error/QrCodeErrorViewTest.kt | 71 +++++ .../qrcode/intro/QrCodeIntroPresenterTest.kt | 80 ++++++ .../qrcode/intro/QrCodeIntroViewTest.kt | 95 +++++++ .../qrcode/scan/QrCodeScanPresenterTest.kt | 120 +++++++++ .../screens/qrcode/scan/QrCodeScanViewTest.kt | 76 ++++++ .../features/logout/impl/LogoutView.kt | 3 +- .../onboarding/api/OnBoardingEntryPoint.kt | 1 + features/onboarding/impl/build.gradle.kts | 17 +- .../onboarding/impl/OnBoardingNode.kt | 6 +- .../onboarding/impl/OnBoardingPresenter.kt | 13 +- .../impl/OnBoardingPresenterTest.kt | 17 +- .../onboarding/impl/OnboardingViewTest.kt | 127 +++++++++ .../createkey/CreateNewRecoveryKeyView.kt | 73 +----- .../impl/disable/SecureBackupDisableView.kt | 3 +- .../impl/enable/SecureBackupEnableView.kt | 3 +- .../enter/SecureBackupEnterRecoveryKeyView.kt | 3 +- .../impl/setup/SecureBackupSetupView.kt | 3 +- gradle/libs.versions.toml | 9 +- libraries/designsystem/build.gradle.kts | 4 +- .../atomic/molecules/NumberedListMolecule.kt | 67 +++++ .../atomic/organisms/NumberedListOrganism.kt | 42 +++ .../designsystem/atomic/pages/FlowStepPage.kt | 14 +- .../components/dialogs/ConfirmationDialog.kt | 2 + .../designsystem/modifiers/CornerBorder.kt | 74 ++++++ .../theme/components/AlertDialogContent.kt | 2 + .../designsystem/utils/AnnotatedString.kt | 36 +++ .../designsystem/utils/ForceOrientation.kt | 41 +++ .../utils/ForceOrientationInMobileDevices.kt | 34 +++ .../libraries/featureflag/api/FeatureFlags.kt | 7 + libraries/featureflag/impl/build.gradle.kts | 1 + .../impl/DefaultFeatureFlagService.kt | 2 +- .../impl/StaticFeatureFlagProvider.kt | 2 + .../api/auth/MatrixAuthenticationService.kt | 4 + .../api/auth/qrlogin/MatrixQrCodeLoginData.kt | 19 ++ .../qrlogin/MatrixQrCodeLoginDataFactory.kt | 21 ++ .../api/auth/qrlogin/QrCodeDecodeException.kt | 35 +++ .../api/auth/qrlogin/QrCodeLoginStep.kt | 26 ++ .../api/auth/qrlogin/QrLoginException.kt | 29 ++ .../matrix/impl/RustMatrixClientFactory.kt | 34 +-- .../auth/RustMatrixAuthenticationService.kt | 49 ++++ .../matrix/impl/auth/qrlogin/QrErrorMapper.kt | 55 ++++ .../auth/qrlogin/QrLoginProgressExtensions.kt | 29 ++ .../qrlogin/RustQrCodeLoginDataFactory.kt | 31 +++ .../impl/auth/qrlogin/SdkQrCodeLoginData.kt | 24 ++ .../auth/FakeMatrixAuthenticationService.kt | 16 +- .../FakeMatrixQrCodeLoginDataFactory.kt | 32 +++ .../permissions/api/PermissionsView.kt | 8 +- libraries/qrcode/build.gradle.kts | 30 +++ .../libraries/qrcode/QRCodeAnalyzer.kt | 50 ++++ .../libraries/qrcode/QrCodeCameraView.kt | 172 ++++++++++++ settings.gradle.kts | 12 +- ...ootView-Day-3_3_null_2,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-3_4_null_2,NEXUS_5,1.0,en].png | 4 +- ...oomView-Day-0_1_null_6,NEXUS_5,1.0,en].png | 4 +- ...mView-Night-0_2_null_6,NEXUS_5,1.0,en].png | 4 +- ...ngsView-Day-1_2_null_2,NEXUS_5,1.0,en].png | 4 +- ...sView-Night-1_3_null_2,NEXUS_5,1.0,en].png | 4 +- ...PinView-Day-3_4_null_3,NEXUS_5,1.0,en].png | 4 +- ...PinView-Day-3_4_null_4,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-3_5_null_3,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-3_5_null_4,NEXUS_5,1.0,en].png | 4 +- ...ionView-Day-7_8_null_0,NEXUS_5,1.0,en].png | 3 + ...ionView-Day-7_8_null_1,NEXUS_5,1.0,en].png | 3 + ...ionView-Day-7_8_null_2,NEXUS_5,1.0,en].png | 3 + ...nView-Night-7_9_null_0,NEXUS_5,1.0,en].png | 3 + ...nView-Night-7_9_null_1,NEXUS_5,1.0,en].png | 3 + ...nView-Night-7_9_null_2,NEXUS_5,1.0,en].png | 3 + ...rorView-Day-8_9_null_0,NEXUS_5,1.0,en].png | 3 + ...rorView-Day-8_9_null_1,NEXUS_5,1.0,en].png | 3 + ...rorView-Day-8_9_null_2,NEXUS_5,1.0,en].png | 3 + ...rorView-Day-8_9_null_3,NEXUS_5,1.0,en].png | 3 + ...rorView-Day-8_9_null_4,NEXUS_5,1.0,en].png | 3 + ...rorView-Day-8_9_null_5,NEXUS_5,1.0,en].png | 3 + ...rorView-Day-8_9_null_6,NEXUS_5,1.0,en].png | 3 + ...View-Night-8_10_null_0,NEXUS_5,1.0,en].png | 3 + ...View-Night-8_10_null_1,NEXUS_5,1.0,en].png | 3 + ...View-Night-8_10_null_2,NEXUS_5,1.0,en].png | 3 + ...View-Night-8_10_null_3,NEXUS_5,1.0,en].png | 3 + ...View-Night-8_10_null_4,NEXUS_5,1.0,en].png | 3 + ...View-Night-8_10_null_5,NEXUS_5,1.0,en].png | 3 + ...View-Night-8_10_null_6,NEXUS_5,1.0,en].png | 3 + ...roView-Day-9_10_null_0,NEXUS_5,1.0,en].png | 3 + ...roView-Day-9_10_null_1,NEXUS_5,1.0,en].png | 3 + ...View-Night-9_11_null_0,NEXUS_5,1.0,en].png | 3 + ...View-Night-9_11_null_1,NEXUS_5,1.0,en].png | 3 + ...nView-Day-10_11_null_0,NEXUS_5,1.0,en].png | 3 + ...nView-Day-10_11_null_1,NEXUS_5,1.0,en].png | 3 + ...nView-Day-10_11_null_2,NEXUS_5,1.0,en].png | 3 + ...nView-Day-10_11_null_3,NEXUS_5,1.0,en].png | 3 + ...iew-Night-10_12_null_0,NEXUS_5,1.0,en].png | 3 + ...iew-Night-10_12_null_1,NEXUS_5,1.0,en].png | 3 + ...iew-Night-10_12_null_2,NEXUS_5,1.0,en].png | 3 + ...iew-Night-10_12_null_3,NEXUS_5,1.0,en].png | 3 + ...View-Day-11_12_null_0,NEXUS_5,1.0,en].png} | 0 ...View-Day-11_12_null_1,NEXUS_5,1.0,en].png} | 0 ...ew-Night-11_13_null_0,NEXUS_5,1.0,en].png} | 0 ...ew-Night-11_13_null_1,NEXUS_5,1.0,en].png} | 0 ...View-Day-12_13_null_0,NEXUS_5,1.0,en].png} | 0 ...View-Day-12_13_null_1,NEXUS_5,1.0,en].png} | 0 ...View-Day-12_13_null_2,NEXUS_5,1.0,en].png} | 0 ...View-Day-12_13_null_3,NEXUS_5,1.0,en].png} | 0 ...View-Day-12_13_null_4,NEXUS_5,1.0,en].png} | 0 ...ew-Night-12_14_null_0,NEXUS_5,1.0,en].png} | 0 ...ew-Night-12_14_null_1,NEXUS_5,1.0,en].png} | 0 ...ew-Night-12_14_null_2,NEXUS_5,1.0,en].png} | 0 ...ew-Night-12_14_null_3,NEXUS_5,1.0,en].png} | 0 ...ew-Night-12_14_null_4,NEXUS_5,1.0,en].png} | 0 ...outView-Day-0_1_null_0,NEXUS_5,1.0,en].png | 4 +- ...outView-Day-0_1_null_1,NEXUS_5,1.0,en].png | 4 +- ...outView-Day-0_1_null_2,NEXUS_5,1.0,en].png | 4 +- ...outView-Day-0_1_null_3,NEXUS_5,1.0,en].png | 4 +- ...outView-Day-0_1_null_4,NEXUS_5,1.0,en].png | 4 +- ...outView-Day-0_1_null_5,NEXUS_5,1.0,en].png | 4 +- ...outView-Day-0_1_null_6,NEXUS_5,1.0,en].png | 4 +- ...outView-Day-0_1_null_7,NEXUS_5,1.0,en].png | 4 +- ...outView-Day-0_1_null_8,NEXUS_5,1.0,en].png | 4 +- ...outView-Day-0_1_null_9,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-0_2_null_0,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-0_2_null_1,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-0_2_null_2,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-0_2_null_3,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-0_2_null_4,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-0_2_null_5,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-0_2_null_6,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-0_2_null_7,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-0_2_null_8,NEXUS_5,1.0,en].png | 4 +- ...tView-Night-0_2_null_9,NEXUS_5,1.0,en].png | 4 +- ...ngsView-Day-6_7_null_6,NEXUS_5,1.0,en].png | 4 +- ...sView-Night-6_8_null_6,NEXUS_5,1.0,en].png | 4 +- ...ionView-Day-7_7_null_7,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-7_8_null_7,NEXUS_5,1.0,en].png | 4 +- ...sView-Day-12_12_null_6,NEXUS_5,1.0,en].png | 4 +- ...iew-Night-12_13_null_6,NEXUS_5,1.0,en].png | 4 +- ...sView-Day-14_14_null_6,NEXUS_5,1.0,en].png | 4 +- ...iew-Night-14_15_null_6,NEXUS_5,1.0,en].png | 4 +- ...ryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png | 4 +- ...KeyView-Night-0_2_null,NEXUS_5,1.0,en].png | 4 +- ...bleView-Day-1_2_null_0,NEXUS_5,1.0,en].png | 4 +- ...bleView-Day-1_2_null_1,NEXUS_5,1.0,en].png | 4 +- ...bleView-Day-1_2_null_2,NEXUS_5,1.0,en].png | 4 +- ...bleView-Day-1_2_null_3,NEXUS_5,1.0,en].png | 4 +- ...eView-Night-1_3_null_0,NEXUS_5,1.0,en].png | 4 +- ...eView-Night-1_3_null_1,NEXUS_5,1.0,en].png | 4 +- ...eView-Night-1_3_null_2,NEXUS_5,1.0,en].png | 4 +- ...eView-Night-1_3_null_3,NEXUS_5,1.0,en].png | 4 +- ...bleView-Day-2_3_null_0,NEXUS_5,1.0,en].png | 4 +- ...bleView-Day-2_3_null_1,NEXUS_5,1.0,en].png | 4 +- ...bleView-Day-2_3_null_2,NEXUS_5,1.0,en].png | 4 +- ...eView-Night-2_4_null_0,NEXUS_5,1.0,en].png | 4 +- ...eView-Night-2_4_null_1,NEXUS_5,1.0,en].png | 4 +- ...eView-Night-2_4_null_2,NEXUS_5,1.0,en].png | 4 +- ...KeyView-Day-3_4_null_0,NEXUS_5,1.0,en].png | 4 +- ...KeyView-Day-3_4_null_1,NEXUS_5,1.0,en].png | 4 +- ...KeyView-Day-3_4_null_2,NEXUS_5,1.0,en].png | 4 +- ...KeyView-Day-3_4_null_3,NEXUS_5,1.0,en].png | 4 +- ...yView-Night-3_5_null_0,NEXUS_5,1.0,en].png | 4 +- ...yView-Night-3_5_null_1,NEXUS_5,1.0,en].png | 4 +- ...yView-Night-3_5_null_2,NEXUS_5,1.0,en].png | 4 +- ...yView-Night-3_5_null_3,NEXUS_5,1.0,en].png | 4 +- ...wChange-Day-6_7_null_0,NEXUS_5,1.0,en].png | 4 +- ...wChange-Day-6_7_null_1,NEXUS_5,1.0,en].png | 4 +- ...wChange-Day-6_7_null_2,NEXUS_5,1.0,en].png | 4 +- ...wChange-Day-6_7_null_3,NEXUS_5,1.0,en].png | 4 +- ...wChange-Day-6_7_null_4,NEXUS_5,1.0,en].png | 4 +- ...hange-Night-6_8_null_0,NEXUS_5,1.0,en].png | 4 +- ...hange-Night-6_8_null_1,NEXUS_5,1.0,en].png | 4 +- ...hange-Night-6_8_null_2,NEXUS_5,1.0,en].png | 4 +- ...hange-Night-6_8_null_3,NEXUS_5,1.0,en].png | 4 +- ...hange-Night-6_8_null_4,NEXUS_5,1.0,en].png | 4 +- ...tupView-Day-5_6_null_0,NEXUS_5,1.0,en].png | 4 +- ...tupView-Day-5_6_null_1,NEXUS_5,1.0,en].png | 4 +- ...tupView-Day-5_6_null_2,NEXUS_5,1.0,en].png | 4 +- ...tupView-Day-5_6_null_3,NEXUS_5,1.0,en].png | 4 +- ...tupView-Day-5_6_null_4,NEXUS_5,1.0,en].png | 4 +- ...pView-Night-5_7_null_0,NEXUS_5,1.0,en].png | 4 +- ...pView-Night-5_7_null_1,NEXUS_5,1.0,en].png | 4 +- ...pView-Night-5_7_null_2,NEXUS_5,1.0,en].png | 4 +- ...pView-Night-5_7_null_3,NEXUS_5,1.0,en].png | 4 +- ...pView-Night-5_7_null_4,NEXUS_5,1.0,en].png | 4 +- ...lowStepPage-Day_0_null,NEXUS_5,1.0,en].png | 4 +- ...wStepPage-Night_1_null,NEXUS_5,1.0,en].png | 4 +- ...onsView-Day-0_1_null_0,NEXUS_5,1.0,en].png | 4 +- ...onsView-Day-0_1_null_1,NEXUS_5,1.0,en].png | 4 +- ...onsView-Day-0_1_null_2,NEXUS_5,1.0,en].png | 4 +- ...onsView-Day-0_1_null_3,NEXUS_5,1.0,en].png | 4 +- ...sView-Night-0_2_null_0,NEXUS_5,1.0,en].png | 4 +- ...sView-Night-0_2_null_1,NEXUS_5,1.0,en].png | 4 +- ...sView-Night-0_2_null_2,NEXUS_5,1.0,en].png | 4 +- ...sView-Night-0_2_null_3,NEXUS_5,1.0,en].png | 4 +- ...thoutText-Day-7_8_null,NEXUS_5,1.0,en].png | 4 +- ...outText-Night-7_9_null,NEXUS_5,1.0,en].png | 4 +- ...reateLink-Day-6_7_null,NEXUS_5,1.0,en].png | 4 +- ...ateLink-Night-6_8_null,NEXUS_5,1.0,en].png | 4 +- ...gEditLink-Day-8_9_null,NEXUS_5,1.0,en].png | 4 +- ...itLink-Night-8_10_null,NEXUS_5,1.0,en].png | 4 +- ...ErrorView-Day-0_1_null,NEXUS_5,1.0,en].png | 4 +- ...rorView-Night-0_2_null,NEXUS_5,1.0,en].png | 2 +- tools/localazy/config.json | 3 +- 253 files changed, 4421 insertions(+), 326 deletions(-) rename {features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl => appconfig/src/main/kotlin/io/element/android/appconfig}/OnBoardingConfig.kt (78%) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginBindings.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginComponent.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/QrCodeLoginScope.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManager.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginManager.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationNode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStep.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStepPreviewProvider.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationView.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorNode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroEvents.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroNode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroState.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroStateProvider.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroView.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanEvents.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanNode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanState.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/di/FakeQrCodeLoginComponent.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/DefaultQrCodeLoginManagerTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/FakeQrCodeLoginManager.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenterTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt create mode 100644 features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/NumberedListMolecule.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/organisms/NumberedListOrganism.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CornerBorder.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/AnnotatedString.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientation.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/ForceOrientationInMobileDevices.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginDataFactory.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrCodeDecodeException.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrCodeLoginStep.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrLoginException.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrLoginProgressExtensions.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/RustQrCodeLoginDataFactory.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginData.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/qrlogin/FakeMatrixQrCodeLoginDataFactory.kt create mode 100644 libraries/qrcode/build.gradle.kts create mode 100644 libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt create mode 100644 libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 rename 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 => ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Day-11_12_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Day-11_12_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Night-11_13_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.searchaccountprovider_SearchAccountProviderView_null_SearchAccountProviderView-Night-11_13_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Day-12_13_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.login.impl.screens.waitlistscreen_WaitListView_null_WaitListView-Night-12_14_null_4,NEXUS_5,1.0,en].png} (100%) diff --git a/.maestro/tests/account/login.yaml b/.maestro/tests/account/login.yaml index 69833a985ed..18054297d62 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 969ac382a4e..e2a78fc5227 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 fae6d586aa2..72d2402eef7 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 4a48b517da4..7a2e02514fe 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 5b7bddb15f9..97fb3bec04d 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 07a546192d3..545dbb9f45e 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 47ddd893fa3..25bf1dc1b3e 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 633fc43b5c1..914b60756e7 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 6b7f9de6f48..0af102a4038 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 00000000000..d16a69f75f6 --- /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 00000000000..f92e192e400 --- /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 00000000000..12d4973c2db --- /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 fb86ae18a57..c07078cdff7 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 00000000000..9ab6494d959 --- /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 00000000000..4d5f0e6a95a --- /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 00000000000..cd0010dc01d --- /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 00000000000..038d9b19989 --- /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 00000000000..b41433d5c8f --- /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 00000000000..cd6125fee83 --- /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 00000000000..825dbe3ad16 --- /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 00000000000..66ddd8bfbed --- /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 00000000000..04aac518e1e --- /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 00000000000..982d7ac1af9 --- /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 00000000000..b207462bd16 --- /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 00000000000..fbe951ebb12 --- /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 00000000000..9d9609acdf8 --- /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 00000000000..7f7c62cd03a --- /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 00000000000..12e8de01f06 --- /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 00000000000..d4b24d68d6f --- /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 00000000000..d2c7a418b0f --- /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 00000000000..aeedc32542b --- /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 00000000000..45657c02260 --- /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 00000000000..764c46643a4 --- /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 00000000000..97925c11ad9 --- /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 228deb957c9..f393c527aeb 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 f4e52f7ac6e..cfc51c3f8c4 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 0616f621b1a..ae5981e3c07 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 27fd2265179..6873dd8fde9 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 31df1a3d047..c7537c9d7e7 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 01f11edaf6c..eb94c8b0f04 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 f81e84a86f9..9d5d99d46ec 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 17d61faae21..4f32383f208 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 dcfc5c7b089..59753ed9268 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 8087fc7c440..af3740fe6c9 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 ef643cd8a10..f1d6b4f2144 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 fcc55b038e4..156891980e8 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 800af345fd8..0362c6c0d01 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 df952171fce..1e01a8eba03 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 0127d21f8f9..d345095c4b0 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 f268b464f12..cade81c4f20 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 00000000000..f3a8dbd6940 --- /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 00000000000..a3ad568cf57 --- /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 00000000000..cc15a80e46e --- /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 00000000000..f53e9c74f74 --- /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 00000000000..78eae62377d --- /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 00000000000..f68e686f1a7 --- /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 00000000000..336e8d2114c --- /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 00000000000..6e258a46023 --- /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 00000000000..df79a51a41d --- /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 00000000000..aece85f2de1 --- /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 9ccd7de985b..35499568446 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 a70f14dc430..ddb3643dc1d 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 9994eacf81a..a8c7b0db00f 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 f7d829d5feb..faf3e3be8df 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 bfe6c06e006..ed7200310f6 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 1eaf60a8f9b..651d8c5cf44 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 00000000000..9a83573807c --- /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 420c6f5675b..202766e4ac5 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 845de55e0c6..c1ce30724e6 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 b2b1e2a3969..190f423bfe3 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 f18d013014b..cbb46849a9e 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 2cefff29b45..6ade84be204 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 2617aa077e1..133edcf1266 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 6cb1f4f7d25..13d5e18e1a2 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 00000000000..596995a2af9 --- /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 00000000000..ccdd875939b --- /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 789e4a3a184..92180616381 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 20f2396b7ad..13e514c63ad 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 00000000000..b8d60f9acfd --- /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 fa35864c45a..7f27927030b 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 00000000000..562b0033b4b --- /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 00000000000..e3a349d4793 --- /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 00000000000..0cab1f406a8 --- /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 1a0602fd154..5c3bd8efd4d 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 64fcd1eece9..76b6836eb3c 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 ac5e0d56cfe..0a6b97e13d1 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 d8b485871a1..139877500bb 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 501b40508e6..c7d3e79144a 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 00000000000..541675b0a88 --- /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 00000000000..0258d551065 --- /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 00000000000..a0719fa8f0b --- /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 00000000000..4ecc7a6cd69 --- /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 00000000000..0a31a46236f --- /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 80302933d53..cb0fbbefa43 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 50d82c1e3ab..d992c7fc1c1 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 00000000000..e55b662b724 --- /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 00000000000..1dc60297004 --- /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 00000000000..9cf55e018a4 --- /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 00000000000..da24fbf4bb5 --- /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 9cec3cbecfc..920ad130df4 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 00000000000..6e542dd7399 --- /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 93800c55be7..790084f193f 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 00000000000..65aa597bf16 --- /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 00000000000..be83581642c --- /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 00000000000..428ff0e3d4f --- /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 dd48d5d4af6..b154f4585eb 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 e1103596c2a..17904a637bd 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 c12b0c44e88..18f05bf6cef 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 00ac5ac1586..831f415574e 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 c6ddfe89fc3..687b00511e6 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 3c8fe9a75f6..6fd25ee44cf 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 14d8ea60c5f..6c2d4625efc 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 b7f6aac7171..a0ce47097a6 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 66635f04827..27f6e91dc25 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 69e4040c24a..55a038e5714 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 804737dd4f2..a176f8bbb53 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 00000000000..9fb63483f2d --- /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 00000000000..03544f53dca --- /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 00000000000..29947461991 --- /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 00000000000..217e36b2464 --- /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 00000000000..4bf8f86621a --- /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 00000000000..14b7d0575c2 --- /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 00000000000..44563689e1f --- /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 00000000000..22d425c7591 --- /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 00000000000..a3db4551e83 --- /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 00000000000..521857e7066 --- /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 00000000000..29f66103efc --- /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 00000000000..93556f727c9 --- /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 00000000000..353f84555c5 --- /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 00000000000..5fc511d67b1 --- /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 00000000000..f227e6a15e7 --- /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 00000000000..2218673d895 --- /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 00000000000..67bbda3f549 --- /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 00000000000..e7117a9b2ff --- /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 00000000000..b6871bed022 --- /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 00000000000..eaeed57bcb5 --- /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 00000000000..e509e677a6e --- /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 00000000000..a366c222037 --- /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 00000000000..2c643f5c7bb --- /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 00000000000..3daf90cabba --- /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 00000000000..99b1262c483 --- /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 00000000000..e5308fe0d7a --- /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 00000000000..8e6de754d7e --- /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 00000000000..56383562e5c --- /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 00000000000..54de03b1666 --- /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 00000000000..d3d7c038569 --- /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 00000000000..49e60cbc161 --- /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 00000000000..666b8bdd1ab --- /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 f92b8d86734..3e8df831cc5 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 ffdb8376504..4c0a94eb93a 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 6ecda3527cc..27382bbcf5b 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 ffdb8376504..4c0a94eb93a 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 405a4695e2c..d7b6cd01180 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 f0bb97209d1..a139e97e94d 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 cfaebd5f228..94890ff13f3 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 edef3eb4053..480ab7657dd 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 aa3f9abd52a..78c9bfbdb0d 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 0c8b719319c..b8c4f1add48 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 f407d449302..1faa021c781 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 b7242acf110..6985a68adea 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 5353902d592..39583240cdd 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 b7242acf110..6985a68adea 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 0acd91562be..99c4509f30b 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 62b78ef2686..e286d8ef972 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 8629678a19e..1c7d58d1c3b 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 6f9a01acc64..8770331a3eb 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 8274ac92d72..f51db01f62d 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 e6ba55728ec..f984aa86213 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 5ae2b8073f8..75b5e7fe468 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 fe2926ec489..58e5c0861ef 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 f2df014dda3..24eca80a51a 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 b0f81d6edf4..a04c52e653a 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 0618e2e36e3..d945c5a6e08 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 6aa63a44334..f1544ef1d9c 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 1e8a0d28134..f1efceae0e0 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 41b61c015cf..56b4f0f0326 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 ecac9d32ec1..fc133096052 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 6be4090def7..ebd91c50fa8 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 f81f879d4c3..abfaefd6a75 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 9b0bab38021..ef4185de49c 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 13bdef5f01d..b79b386b262 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 d4374e318a7..98e8b74c6e9 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 c5610061ab5..ef54fc8ce93 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 fc92b3b603f..f3a78d6ddb0 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 95652f882dd..69acbaae9f1 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 1f9bc16a8c3..ae4fd5f24ab 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 edc379c1d86..2801b314170 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 e870073ed04..9067a84cfe5 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 417e7f1f470..1b030e472a7 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 d6112d12e09..d178a1240a3 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 eddca870941..8996c25dbc6 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 8a0ff142f9d..0d7cc6779ff 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 d8682a6a155..d8b8316e1b2 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 aa07ae2e60d..a0f7e7d6f2e 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 5eba9096cd1..74802f62b8a 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 d9f174c0e9f..e314ac25cb7 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 9dd8bd3bfb8..b7ee516a912 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 a5ed227845f..831429ef291 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 ef11d24386a..26f366861d0 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 f9e00a548ec..7bc915f532a 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 6980beec6bc..96cffb14969 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 e1cf5691dac..2842bdafeb9 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 57c3222d341..c08d423ecae 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 57c3222d341..c08d423ecae 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 7ffddf4401c..ee16bf0bfd7 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 1eb1581dd41..b510cf5f41f 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 4abfdd267d5..d1b6e47b929 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 cc4d7d7dbff..8c1c1af9fca 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 cc4d7d7dbff..8c1c1af9fca 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 9dd2af19fae..c2fd1627982 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 dc74ff7aa70..1a88bcbb74f 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 3601a11e5a8..c85f292761e 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 57c3222d341..c08d423ecae 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 57c3222d341..c08d423ecae 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 7ffddf4401c..ee16bf0bfd7 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 7cbb1cc72ba..978f67cd0a0 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 4a41ae3ae36..cf21ef2df67 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 cc4d7d7dbff..8c1c1af9fca 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 cc4d7d7dbff..8c1c1af9fca 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 9dd2af19fae..c2fd1627982 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 794b0b8d258..68456e62f88 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 c62fade2e11..94c7a8d0f27 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 a578d69128d..3f5d84ad6fd 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 cffb3cf5d86..86af8532967 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 19e14046b15..2912f887933 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 956d853953b..1795cf1d23c 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 3a963029b6e..b8f53d17056 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 7ba9368ad24..915ba6d08cc 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 700e8ddcae6..7e845670268 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 a810217e457..b4a27083a29 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 c89936dda52..e9152092f3c 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 5ff6a58533a..4c6538a9755 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 df00761695a..1a247f193af 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 1eee8a30f6d..196d5ac08a8 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 32afaaec233..5deb60eb780 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 c2a890e33cb..1613f1718b1 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 5acca03c1c5..730902ab3fb 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 28c7fb7aac2..cf3b168ecdc 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 fa5c456e4b5..380649fc3c1 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_.*" ] }, {