From 6914cc7c94d4a68dc7c6f38778575ab8da0eb6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cie=C5=9Blak?= Date: Wed, 29 Jan 2025 18:20:26 +0100 Subject: [PATCH] Onboarding: LoginScreen location adjusted - LoginScreen SB page simplified - OnboardingLayoutPage - settings introduced for handier testing - LoginScreen moved from OnboardingLayout to OnboardingFlow, making first flow's page bound to model content (instead of relying on check during initialization) - UnblockWithPukFlow removed from OnboardingLayout (now it's used only in OnboardingFlow) - Login error/success processing extracted from LoginScreen to OnboardingLayout - small bug fixed in Utils::objectTypeName Closes: #17160 --- storybook/pages/LoginScreenPage.qml | 36 +++--- storybook/pages/OnboardingLayoutPage.qml | 20 ++-- .../qmlTests/tests/tst_OnboardingLayout.qml | 5 + .../AppLayouts/Onboarding2/OnboardingFlow.qml | 72 +++++++++++- .../Onboarding2/OnboardingLayout.qml | 105 ++++++++++-------- .../Onboarding2/pages/LoginScreen.qml | 98 +++++++--------- ui/imports/utils/Utils.qml | 2 +- 7 files changed, 198 insertions(+), 140 deletions(-) diff --git a/storybook/pages/LoginScreenPage.qml b/storybook/pages/LoginScreenPage.qml index 909599e7869..ee29f1a8f16 100644 --- a/storybook/pages/LoginScreenPage.qml +++ b/storybook/pages/LoginScreenPage.qml @@ -2,20 +2,12 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import QtQuick.Window 2.15 -import QtQml.Models 2.15 - -import StatusQ 0.1 -import StatusQ.Core 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Components 0.1 -import StatusQ.Core.Theme 0.1 import Models 1.0 import Storybook 1.0 import AppLayouts.Onboarding.enums 1.0 import AppLayouts.Onboarding2.pages 1.0 -import AppLayouts.Onboarding2.stores 1.0 import utils 1.0 @@ -25,8 +17,8 @@ SplitView { Logs { id: logs } - OnboardingStore { - id: store + QtObject { + id: driver // keycard property int keycardState: Onboarding.KeycardState.NoPCSCService @@ -34,7 +26,7 @@ SplitView { property int keycardRemainingPukAttempts: 3 function setPin(pin: string) { // -> bool - logs.logEvent("OnboardingStore.setPin", ["pin"], arguments) + logs.logEvent("setPin", ["pin"], arguments) const valid = pin === ctrlPin.text if (!valid) keycardRemainingPinAttempts-- // SIMULATION: decrease the remaining PIN attempts @@ -46,7 +38,7 @@ SplitView { } function setPuk(puk) { // -> bool - logs.logEvent("OnboardingStore.setPuk", ["puk"], arguments) + logs.logEvent("setPuk", ["puk"], arguments) const valid = puk === ctrlPuk.text if (!valid) keycardRemainingPukAttempts-- @@ -71,7 +63,13 @@ SplitView { SplitView.fillHeight: true loginAccountsModel: LoginAccountsModel {} - onboardingStore: store + + keycardState: driver.keycardState + + tryToSetPinFunction: (pin) => driver.setPin(pin) + keycardRemainingPinAttempts: driver.keycardRemainingPinAttempts + keycardRemainingPukAttempts: driver.keycardRemainingPukAttempts + biometricsAvailable: ctrlBiometrics.checked isBiometricsLogin: localAccountSettings.storeToKeychainValue === Constants.keychain.storedValue.store onBiometricsRequested: biometricsPopup.open() @@ -80,7 +78,7 @@ SplitView { // SIMULATION: emit an error in case of wrong password if (method === Onboarding.LoginMethod.Password && data.password !== ctrlPassword.text) { - onboardingStore.accountLoginError("The impossible has happened", Math.random() < 0.5) + driver.accountLoginError("The impossible has happened", Math.random() < 0.5) } } onOnboardingCreateProfileFlowRequested: logs.logEvent("onOnboardingCreateProfileFlowRequested") @@ -105,9 +103,9 @@ SplitView { password: ctrlPassword.text pin: ctrlPin.text selectedProfileIsKeycard: loginScreen.selectedProfileIsKeycard - onAccountLoginError: (error, wrongPassword) => store.accountLoginError(error, wrongPassword) - onObtainingPasswordSuccess: (password) => store.obtainingPasswordSuccess(password) - onObtainingPasswordError: (errorDescription, errorType, wrongFingerprint) => store.obtainingPasswordError(errorDescription, errorType, wrongFingerprint) + onAccountLoginError: (error, wrongPassword) => driver.accountLoginError(error, wrongPassword) + onObtainingPasswordSuccess: (password) => driver.obtainingPasswordSuccess(password) + onObtainingPasswordError: (errorDescription, errorType, wrongFingerprint) => driver.obtainingPasswordError(errorDescription, errorType, wrongFingerprint) } LogsAndControlsPanel { @@ -179,8 +177,8 @@ SplitView { textRole: "name" valueRole: "value" model: Onboarding.getModelFromEnum("KeycardState") - onActivated: store.keycardState = currentValue - Component.onCompleted: currentIndex = Qt.binding(() => indexOfValue(store.keycardState)) + onActivated: driver.keycardState = currentValue + Component.onCompleted: currentIndex = Qt.binding(() => indexOfValue(driver.keycardState)) } } } diff --git a/storybook/pages/OnboardingLayoutPage.qml b/storybook/pages/OnboardingLayoutPage.qml index f4273fd39fe..5a0336d33ee 100644 --- a/storybook/pages/OnboardingLayoutPage.qml +++ b/storybook/pages/OnboardingLayoutPage.qml @@ -3,11 +3,7 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import QtQuick.Window 2.15 -import StatusQ 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Core 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Components 0.1 +import Qt.labs.settings 1.0 import AppLayouts.Onboarding.enums 1.0 import AppLayouts.Onboarding2 1.0 @@ -379,9 +375,11 @@ SplitView { const stack = onboarding.stack let content = `Stack (${stack.depth}):` - for (let i = 0; i < stack.depth; i++) - content += " " + InspectionUtils.baseName( - stack.get(i, StackView.ForceLoad)) + for (let i = 0; i < stack.depth; i++) { + const stackEntry = stack.get(i, StackView.ForceLoad) + content += " " + InspectionUtils.baseName(stackEntry instanceof Loader + ? stackEntry.item : stackEntry) + } return content } @@ -526,6 +524,12 @@ SplitView { } } } + + Settings { + property alias useBiometrics: ctrlBiometrics.checked + property alias showLoginScreen: ctrlLoginScreen.checked + property alias useTouchId: ctrlTouchIdUser.checked + } } // category: Onboarding diff --git a/storybook/qmlTests/tests/tst_OnboardingLayout.qml b/storybook/qmlTests/tests/tst_OnboardingLayout.qml index aa6b1531800..2d241565e16 100644 --- a/storybook/qmlTests/tests/tst_OnboardingLayout.qml +++ b/storybook/qmlTests/tests/tst_OnboardingLayout.qml @@ -172,6 +172,11 @@ Item { verify(!!stack) tryCompare(stack, "busy", false) // wait for page transitions to stop + if (stack.currentItem instanceof Loader) { + verify(stack.currentItem.item instanceof pageClass) + return stack.currentItem.item + } + verify(stack.currentItem instanceof pageClass) return stack.currentItem } diff --git a/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml b/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml index 3e725c786ef..952631fdfbf 100644 --- a/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml +++ b/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml @@ -1,6 +1,8 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQml 2.15 +import StatusQ 0.1 import StatusQ.Popups 0.1 import StatusQ.Core.Utils 0.1 as SQUtils @@ -12,6 +14,8 @@ SQUtils.QObject { required property StackView stackView + required property var loginAccountsModel + required property int keycardState required property int addKeyPairState required property int syncState @@ -19,6 +23,8 @@ SQUtils.QObject { required property int remainingPinAttempts required property int remainingPukAttempts + required property bool isBiometricsLogin // FIXME should come from the loginAccountsModel for each profile separately? + required property bool biometricsAvailable required property bool displayKeycardPromoBanner required property bool networkChecksEnabled @@ -32,6 +38,8 @@ SQUtils.QObject { required property var tryToSetPinFunction required property var tryToSetPukFunction + signal biometricsRequested + signal loginRequested(string keyUid, int method, var data) signal keycardPinCreated(string pin) signal keycardPinEntered(string pin) signal enableBiometricsRequested(bool enable) @@ -51,7 +59,7 @@ SQUtils.QObject { signal finished(int flow) function init() { - root.stackView.push(welcomePage) + root.stackView.push(entryPage) } function startCreateProfileFlow() { @@ -66,10 +74,13 @@ SQUtils.QObject { root.stackView.push(keycardLostPage) } + readonly property LoginScreen loginScreen: d.loginScreen + QtObject { id: d property int flow + property LoginScreen loginScreen: null function pushOrSkipBiometricsPage() { if (root.biometricsAvailable) { @@ -88,6 +99,15 @@ SQUtils.QObject { } } + Component { + id: entryPage + + Loader { + sourceComponent: loginAccountsModel.ModelCount.empty ? welcomePage + : loginScreenComponent + } + } + Component { id: welcomePage @@ -109,6 +129,39 @@ SQUtils.QObject { } } + Component { + id: loginScreenComponent + + LoginScreen { + id: loginScreen + + keycardState: root.keycardState + tryToSetPinFunction: root.tryToSetPinFunction + + keycardRemainingPinAttempts: root.remainingPinAttempts + keycardRemainingPukAttempts: root.remainingPukAttempts + + loginAccountsModel: root.loginAccountsModel + biometricsAvailable: root.biometricsAvailable + isBiometricsLogin: root.isBiometricsLogin + onBiometricsRequested: root.biometricsRequested() + onLoginRequested: (keyUid, method, data) => root.loginRequested(keyUid, method, data) + + onOnboardingCreateProfileFlowRequested: root.startCreateProfileFlow() + onOnboardingLoginFlowRequested: root.startLoginFlow() + onLostKeycard: root.startLostKeycardFlow() + onUnblockWithSeedphraseRequested: console.warn("!!! FIXME OnboardingLayout::onUnblockWithSeedphraseRequested") + onUnblockWithPukRequested: unblockWithPukFlow.init() + onKeycardFactoryResetRequested: console.warn("!!! FIXME OnboardingLayout::onKeycardFactoryResetRequested") + + Binding { + target: d + restoreMode: Binding.RestoreValue + property: "loginScreen" + value: loginScreen + } + } + } Component { id: helpUsImproveStatusPage @@ -279,6 +332,8 @@ SQUtils.QObject { UnblockWithPukFlow { id: unblockWithPukFlow + property string pin + stackView: root.stackView keycardState: root.keycardState tryToSetPukFunction: root.tryToSetPukFunction @@ -287,12 +342,21 @@ SQUtils.QObject { keycardPinInfoPageDelay: root.keycardPinInfoPageDelay onReloadKeycardRequested: root.reloadKeycardRequested() - onKeycardPinCreated: (pin) => root.keycardPinCreated(pin) + onKeycardPinCreated: (pin) => { + unblockWithPukFlow.pin = pin + root.keycardPinCreated(pin) + } onKeycardFactoryResetRequested: root.keycardFactoryResetRequested() onFinished: { - d.flow = Onboarding.SecondaryFlow.LoginWithKeycard - d.pushOrSkipBiometricsPage() + if (root.loginScreen) { + root.loginRequested(root.loginScreen.selectedProfileKeyId, + Onboarding.LoginMethod.Keycard, { pin }) + d.selectedProfileKeyId = "" + } else { + d.flow = Onboarding.SecondaryFlow.LoginWithKeycard + d.pushOrSkipBiometricsPage() + } } } diff --git a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml index e4dc3c0918d..ee88de65ae8 100644 --- a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml +++ b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml @@ -44,11 +44,7 @@ Page { function restartFlow() { unload() - - if (!loginAccountsModel || loginAccountsModel.ModelCount.empty) - onboardingFlow.init() - else - stack.push(loginScreenComponent) + onboardingFlow.init() } function unload() { @@ -143,6 +139,8 @@ Page { stackView: stack + loginAccountsModel: root.loginAccountsModel + keycardState: root.onboardingStore.keycardState syncState: root.onboardingStore.syncState addKeyPairState: root.onboardingStore.addKeyPairState @@ -150,6 +148,7 @@ Page { seedWords: root.onboardingStore.getMnemonic().split(" ") displayKeycardPromoBanner: !d.settings.keycardPromoShown + isBiometricsLogin: root.isBiometricsLogin biometricsAvailable: root.biometricsAvailable networkChecksEnabled: root.networkChecksEnabled @@ -161,6 +160,9 @@ Page { remainingPinAttempts: root.onboardingStore.keycardRemainingPinAttempts remainingPukAttempts: root.onboardingStore.keycardRemainingPukAttempts + onBiometricsRequested: root.biometricsRequested() + onLoginRequested: (keyUid, method, data) => root.loginRequested(keyUid, method, data) + onKeycardPinCreated: (pin) => { d.keycardPin = pin root.onboardingStore.setPin(pin) @@ -187,61 +189,66 @@ Page { onKeycardFactoryResetRequested: console.warn("!!! FIXME OnboardingLayout::onKeycardFactoryResetRequested") } - Component { - id: loginScreenComponent - LoginScreen { - id: loginScreen - onboardingStore: root.onboardingStore - loginAccountsModel: root.loginAccountsModel - biometricsAvailable: root.biometricsAvailable - isBiometricsLogin: root.isBiometricsLogin - onBiometricsRequested: root.biometricsRequested() - onLoginRequested: (keyUid, method, data) => root.loginRequested(keyUid, method, data) - - onOnboardingCreateProfileFlowRequested: onboardingFlow.startCreateProfileFlow() - onOnboardingLoginFlowRequested: onboardingFlow.startLoginFlow() - onLostKeycard: onboardingFlow.startLostKeycardFlow() - onUnblockWithSeedphraseRequested: console.warn("!!! FIXME OnboardingLayout::onUnblockWithSeedphraseRequested") - onUnblockWithPukRequested: { - d.selectedProfileKeyId = selectedProfileKeyId - unblockWithPukFlow.init() - } - onKeycardFactoryResetRequested: console.warn("!!! FIXME OnboardingLayout::onKeycardFactoryResetRequested") + Connections { + target: stack.currentItem + ignoreUnknownSignals: true + + function onOpenLink(link: string) { + Qt.openUrlExternally(link) + } + function onOpenLinkWithConfirmation(link: string, domain: string) { + Qt.openUrlExternally(link) } } - UnblockWithPukFlow { - id: unblockWithPukFlow + Connections { + target: root.onboardingStore - stackView: stack - keycardState: root.onboardingStore.keycardState - tryToSetPukFunction: root.onboardingStore.setPuk - remainingAttempts: root.onboardingStore.keycardRemainingPukAttempts + // (password) login + function onAccountLoginError(error: string, wrongPassword: bool) { + const loginScreen = onboardingFlow.loginScreen - keycardPinInfoPageDelay: root.keycardPinInfoPageDelay + if (!error || !loginScreen || loginScreen.currentProfileIsKeycard) + return - onReloadKeycardRequested: root.reloadKeycardRequested() - onKeycardPinCreated: (pin) => { - d.keycardPin = pin - root.onboardingStore.setPin(pin) - } - onKeycardFactoryResetRequested: console.warn("!!! FIXME OnboardingLayout::onKeycardFactoryResetRequested") + let validationError + let detailedError - onFinished: { - root.loginRequested(d.selectedProfileKeyId, Onboarding.LoginMethod.Keycard, {"pin": d.keycardPin}) - d.selectedProfileKeyId = "" + // SQLITE_NOTADB: "file is not a database" + if (error.includes("file is not a database") || wrongPassword) { + validationError = qsTr("Password incorrect. %1").arg("" + qsTr("Forgot password?") + "") + detailedError = "" + } else { + validationError = qsTr("Login failed. %1").arg("" + qsTr("Show details.") + "") + detailedError = error + } + + loginScreen.setError(validationError, detailedError) } - } - Connections { - target: stack.currentItem - ignoreUnknownSignals: true + // biometrics + function onObtainingPasswordError(errorDescription: string, errorType: string, wrongFingerprint: bool) { + const loginScreen = onboardingFlow.loginScreen - function onOpenLink(link: string) { - Qt.openUrlExternally(link) + if (!loginScreen || errorType === Constants.keychain.errorType.authentication) { + // We are notifying user only about keychain errors. + return + } + + const error = wrongFingerprint + ? qsTr("Fingerprint not recognised. Try entering password instead.") + : errorDescription + + loginScreen.setObtainingPasswordError(error, wrongFingerprint) } - function onOpenLinkWithConfirmation(link: string, domain: string) { - Qt.openUrlExternally(link) + + function onObtainingPasswordSuccess(password: string) { + const loginScreen = onboardingFlow.loginScreen + + if (!loginScreen || !root.isBiometricsLogin) + return + + loginScreen.setObtainingPasswordSuccess(password) } } diff --git a/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml b/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml index 881a71f10c4..b65e35a5a41 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml @@ -18,7 +18,11 @@ import utils 1.0 OnboardingPage { id: root - required property OnboardingStore onboardingStore + required property int keycardState + + required property var tryToSetPinFunction + required property int keycardRemainingPinAttempts + required property int keycardRemainingPukAttempts // [{keyUid:string, username:string, thumbnailImage:string, colorId:int, colorHash:var, order:int, keycardCreatedAccount:bool}] required property var loginAccountsModel @@ -66,14 +70,14 @@ OnboardingPage { if (password.length === 0) return - root.loginRequested(d.settings.lastKeyUid, Onboarding.LoginMethod.Password, {"password": password}) + root.loginRequested(d.settings.lastKeyUid, Onboarding.LoginMethod.Password, { password }) } function doKeycardLogin(pin: string) { if (pin.length === 0) return - root.loginRequested(d.settings.lastKeyUid, Onboarding.LoginMethod.Keycard, {"pin": pin}) + root.loginRequested(d.settings.lastKeyUid, Onboarding.LoginMethod.Keycard, { pin }) } } @@ -83,64 +87,40 @@ OnboardingPage { passwordBox.forceActiveFocus() } - Connections { - target: root.onboardingStore - - // (password) login - function onAccountLoginError(error: string, wrongPassword: bool) { - if (error) { - if (!d.currentProfileIsKeycard) { // PIN validation done separately - // SQLITE_NOTADB: "file is not a database" - if (error.includes("file is not a database") || wrongPassword) { - passwordBox.validationError = qsTr("Password incorrect. %1").arg("" + qsTr("Forgot password?") + "") - passwordBox.detailedError = "" - } else { - passwordBox.validationError = qsTr("Login failed. %1").arg("" + qsTr("Show details.") + "") - passwordBox.detailedError = error - } + function setObtainingPasswordError(error: string, wrongFingerprint: bool) { + d.biometricsSuccessful = false + d.biometricsFailed = wrongFingerprint - passwordBox.clear() - passwordBox.forceActiveFocus() - } - } + if (d.currentProfileIsKeycard) { + keycardBox.clear() + } else { + passwordBox.validationError = error + passwordBox.clear() + passwordBox.forceActiveFocus() } + } - // biometrics - function onObtainingPasswordError(errorDescription: string, errorType: string, wrongFingerprint: bool) { - if (errorType === Constants.keychain.errorType.authentication) { - // We are notifying user only about keychain errors. - return - } - - d.biometricsSuccessful = false - d.biometricsFailed = wrongFingerprint - - if (d.currentProfileIsKeycard) { - keycardBox.clear() - } else { - passwordBox.validationError = wrongFingerprint ? qsTr("Fingerprint not recognised. Try entering password instead.") - : errorDescription - passwordBox.clear() - passwordBox.forceActiveFocus() - } - } - function onObtainingPasswordSuccess(password: string) { - if (!root.isBiometricsLogin) - return + function setObtainingPasswordSuccess(password: string) { + if (!root.isBiometricsLogin) + return - d.biometricsSuccessful = true - d.biometricsFailed = false + d.biometricsSuccessful = true + d.biometricsFailed = false - if (d.currentProfileIsKeycard) { - keycardBox.setPin(password) // automatic login, emits loginRequested() already - } else { - passwordBox.validationError = "" - passwordBox.password = password - d.doPasswordLogin(password) - } + if (d.currentProfileIsKeycard) { + keycardBox.setPin(password) // automatic login, emits loginRequested() already + } else { + passwordBox.validationError = "" + passwordBox.password = password + d.doPasswordLogin(password) } } + function setError(error: string, detailedError: string) { + passwordBox.validationError = error + passwordBox.detailedError = detailedError + } + padding: 40 contentItem: Item { @@ -177,8 +157,8 @@ OnboardingPage { Layout.fillWidth: true Layout.preferredHeight: 64 model: root.loginAccountsModel - currentKeycardLocked: root.onboardingStore.keycardState === Onboarding.KeycardState.BlockedPIN || - root.onboardingStore.keycardState === Onboarding.KeycardState.BlockedPUK + currentKeycardLocked: root.keycardState === Onboarding.KeycardState.BlockedPIN || + root.keycardState === Onboarding.KeycardState.BlockedPUK onSelectedProfileKeyIdChanged: { d.resetBiometricsResult() d.settings.lastKeyUid = selectedProfileKeyId @@ -221,10 +201,10 @@ OnboardingPage { isBiometricsLogin: root.biometricsAvailable && root.isBiometricsLogin biometricsSuccessful: d.biometricsSuccessful biometricsFailed: d.biometricsFailed - keycardState: root.onboardingStore.keycardState - tryToSetPinFunction: root.onboardingStore.setPin - keycardRemainingPinAttempts: root.onboardingStore.keycardRemainingPinAttempts - keycardRemainingPukAttempts: root.onboardingStore.keycardRemainingPukAttempts + keycardState: root.keycardState + tryToSetPinFunction: root.tryToSetPinFunction + keycardRemainingPinAttempts: root.keycardRemainingPinAttempts + keycardRemainingPukAttempts: root.keycardRemainingPukAttempts onUnblockWithSeedphraseRequested: root.unblockWithSeedphraseRequested() onUnblockWithPukRequested: root.unblockWithPukRequested() onKeycardFactoryResetRequested: root.keycardFactoryResetRequested() diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index f1a053bfeae..f255152e223 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -972,7 +972,7 @@ QtObject { let typeName = item.toString() if (typeName.startsWith("QQuick")) - typeName = name.substring(6) + typeName = typeName.substring(6) const underscoreIndex = typeName.indexOf("_")