From a9fecc4ca76d6d5ecba15b84ab4dc86be65c9224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tinkl?= Date: Fri, 10 Jan 2025 20:27:11 +0100 Subject: [PATCH] WIP feat(Onboarding) Implement new Login screen Fixes #17057 --- storybook/pages/LoginFlowPage.qml | 175 ++++++++++++++++ storybook/pages/OnboardingLayoutPage.qml | 2 - .../StatusQ/Controls/StatusPasswordInput.qml | 8 +- ui/StatusQ/src/onboarding/enums.h | 6 + ui/app/AppLayouts/Login/LoginFlow.qml | 169 +++++++++++++++ .../Login/components/LoginKeycardBox.qml | 193 ++++++++++++++++++ .../Login/components/LoginPasswordBox.qml | 39 ++++ ui/app/AppLayouts/Login/components/qmldir | 2 + .../Login/controls/LoginPasswordInput.qml | 64 ++++++ .../Login/controls/LoginUserSelector.qml | 160 +++++++++++++++ .../controls/LoginUserSelectorDelegate.qml | 82 ++++++++ ui/app/AppLayouts/Login/controls/qmldir | 3 + ui/app/AppLayouts/Login/qmldir | 1 + .../Onboarding2/OnboardingLayout.qml | 4 +- 14 files changed, 901 insertions(+), 7 deletions(-) create mode 100644 storybook/pages/LoginFlowPage.qml create mode 100644 ui/app/AppLayouts/Login/LoginFlow.qml create mode 100644 ui/app/AppLayouts/Login/components/LoginKeycardBox.qml create mode 100644 ui/app/AppLayouts/Login/components/LoginPasswordBox.qml create mode 100644 ui/app/AppLayouts/Login/components/qmldir create mode 100644 ui/app/AppLayouts/Login/controls/LoginPasswordInput.qml create mode 100644 ui/app/AppLayouts/Login/controls/LoginUserSelector.qml create mode 100644 ui/app/AppLayouts/Login/controls/LoginUserSelectorDelegate.qml create mode 100644 ui/app/AppLayouts/Login/controls/qmldir create mode 100644 ui/app/AppLayouts/Login/qmldir diff --git a/storybook/pages/LoginFlowPage.qml b/storybook/pages/LoginFlowPage.qml new file mode 100644 index 00000000000..b8ed203a7c2 --- /dev/null +++ b/storybook/pages/LoginFlowPage.qml @@ -0,0 +1,175 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.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.stores 1.0 + +import AppLayouts.Login 1.0 + +import utils 1.0 + +SplitView { + id: root + orientation: Qt.Vertical + + Logs { id: logs } + + LoginFlow { + id: loginLayout + SplitView.fillWidth: true + SplitView.fillHeight: true + + loginAccountsModel: ListModel { + readonly property var data: [ + { + keycardCreatedAccount: false, + colorId: 1, + colorHash: [{colorId: 3, segmentLength: 2}, {colorId: 7, segmentLength: 1}, {colorId: 4, segmentLength: 2}], + username: "Bob", + thumbnailImage: Theme.png("collectibles/Doodles"), + keyUid: "uid_1" + }, + { + keycardCreatedAccount: false, + colorId: 2, + colorHash: [{colorId: 9, segmentLength: 1}, {colorId: 7, segmentLength: 3}, {colorId: 10, segmentLength: 2}], + username: "John", + thumbnailImage: Theme.png("collectibles/CryptoPunks"), + keyUid: "uid_2" + }, + { + keycardCreatedAccount: false, + colorId: 3, + colorHash: [], + username: "8️⃣6️⃣.eth", + thumbnailImage: "", + keyUid: "uid_4" + }, + { + keycardCreatedAccount: true, + colorId: 4, + colorHash: [{colorId: 2, segmentLength: 4}, {colorId: 6, segmentLength: 3}, {colorId: 11, segmentLength: 1}], + username: "Very long username that should eventually elide on the right side", + thumbnailImage: Theme.png("collectibles/SuperRare"), + keyUid: "uid_3" + } + ] + Component.onCompleted: append(data) + } + onboardingStore: OnboardingStore { + readonly property int keycardState: ctrlKeycardState.currentValue // enum Onboarding.KeycardState + property int keycardRemainingPinAttempts: 5 + + function setPin(pin: string) { // -> bool + logs.logEvent("OnboardingStore.setPin", ["pin"], arguments) + const valid = pin === ctrlPin.text + if (!valid) + keycardRemainingPinAttempts-- + if (keycardRemainingPinAttempts <= 0) { + ctrlKeycardState.currentIndex = ctrlKeycardState.indexOfValue(Onboarding.KeycardState.Locked) + keycardRemainingPinAttempts = 5 + } + return valid + } + + function setPassword(password: string) { // -> bool // FIXME this or startupModuleInst Connections signals? + return !!password && password === ctrlPassword.text + } + } + isBiometricsLogin: localAccountSettings.storeToKeychainValue === Constants.keychain.storedValue.store + + onLoginRequested: (keyUid, method, data) => logs.logEvent("onLoginRequested", ["keyUid", "method", "data"], arguments) + onOnboardingCreateProfileFlowRequested: logs.logEvent("onOnboardingCreateProfileFlowRequested") + onOnboardingLoginFlowRequested: logs.logEvent("onOnboardingLoginFlowRequested") + onUnlockWithSeedphraseRequested: logs.logEvent("onUnlockWithSeedphraseRequested") + onLostKeycard: logs.logEvent("onLostKeycard") + + QtObject { + id: localAccountSettings + readonly property string storeToKeychainValue: ctrlTouchIdUser.checked ? Constants.keychain.storedValue.store : "" + } + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 150 + SplitView.preferredHeight: 150 + + logsView.logText: logs.logText + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.fillWidth: true + Label { + text: "Password:\t" + } + TextField { + id: ctrlPassword + text: "0123456789" + placeholderText: "Example password" + } + Switch { + id: ctrlTouchIdUser + text: "Touch ID login" + checked: true + } + + Label { + text: "Selected user ID: %1".arg(loginLayout.selectedUserKeyId || "N/A") + } + } + + RowLayout { + Layout.fillWidth: true + Label { + text: "Keycard PIN:\t" + } + TextField { + id: ctrlPin + text: "111111" + inputMask: "999999" + } + Label { + text: "State:" + } + ComboBox { + Layout.preferredWidth: 300 + id: ctrlKeycardState + focusPolicy: Qt.NoFocus + textRole: "text" + valueRole: "value" + model: [ + { value: Onboarding.KeycardState.NoPCSCService, text: "NoPCSCService" }, + { value: Onboarding.KeycardState.PluginReader, text: "PluginReader" }, + { value: Onboarding.KeycardState.InsertKeycard, text: "InsertKeycard" }, + { value: Onboarding.KeycardState.ReadingKeycard, text: "ReadingKeycard" }, + { value: Onboarding.KeycardState.WrongKeycard, text: "WrongKeycard" }, + { value: Onboarding.KeycardState.NotKeycard, text: "NotKeycard" }, + { value: Onboarding.KeycardState.MaxPairingSlotsReached, text: "MaxPairingSlotsReached" }, + { value: Onboarding.KeycardState.Locked, text: "Locked" }, + { value: Onboarding.KeycardState.NotEmpty, text: "NotEmpty" }, + { value: Onboarding.KeycardState.Empty, text: "Empty" } + ] + } + } + } + } +} + +// category: Onboarding +// status: good +// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=801-42615&m=dev diff --git a/storybook/pages/OnboardingLayoutPage.qml b/storybook/pages/OnboardingLayoutPage.qml index 328f7ca7cb2..c1d73c76018 100644 --- a/storybook/pages/OnboardingLayoutPage.qml +++ b/storybook/pages/OnboardingLayoutPage.qml @@ -46,8 +46,6 @@ SplitView { SplitView.fillWidth: true SplitView.fillHeight: true - networkChecksEnabled: true - onboardingStore: OnboardingStore { readonly property int keycardState: ctrlKeycardState.currentValue // enum Onboarding.KeycardState property int keycardRemainingPinAttempts: 5 diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusPasswordInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusPasswordInput.qml index edba8d57383..396d0b09f74 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusPasswordInput.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusPasswordInput.qml @@ -35,6 +35,8 @@ StatusTextField { */ property string signingPhrase: "" + property bool hasError + QtObject { id: d @@ -44,7 +46,6 @@ StatusTextField { readonly property int signingPhraseWordPadding: 8 readonly property int signingPhraseWordsSpacing: 8 readonly property int signingPhraseWordsHeight: 30 - } leftPadding: d.inputTextPadding @@ -57,13 +58,12 @@ StatusTextField { selectByMouse: true echoMode: TextInput.Password - color: Theme.palette.directColor1 background: Rectangle { color: Theme.palette.baseColor2 radius: d.radius - border.width: root.focus ? 1 : 0 - border.color: Theme.palette.primaryColor1 + border.width: root.focus || root.hasError ? 1 : 0 + border.color: root.hasError ? Theme.palette.dangerColor1 : Theme.palette.primaryColor1 } RowLayout { diff --git a/ui/StatusQ/src/onboarding/enums.h b/ui/StatusQ/src/onboarding/enums.h index ff7c1b97a07..029d612a7c2 100644 --- a/ui/StatusQ/src/onboarding/enums.h +++ b/ui/StatusQ/src/onboarding/enums.h @@ -25,6 +25,11 @@ class OnboardingEnums LoginWithKeycard }; + enum class LoginMethod { + Password, + Keycard, + }; + enum class KeycardState { NoPCSCService, PluginReader, @@ -55,6 +60,7 @@ class OnboardingEnums private: Q_ENUM(PrimaryFlow) Q_ENUM(SecondaryFlow) + Q_ENUM(LoginMethod) Q_ENUM(KeycardState) Q_ENUM(AddKeyPairState) Q_ENUM(SyncState) diff --git a/ui/app/AppLayouts/Login/LoginFlow.qml b/ui/app/AppLayouts/Login/LoginFlow.qml new file mode 100644 index 00000000000..30652e1075c --- /dev/null +++ b/ui/app/AppLayouts/Login/LoginFlow.qml @@ -0,0 +1,169 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 +import Qt.labs.settings 1.1 + +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding.enums 1.0 +import AppLayouts.Onboarding2.stores 1.0 + +import AppLayouts.Login.controls 1.0 +import AppLayouts.Login.components 1.0 + +import utils 1.0 + +Page { + id: root + + required property OnboardingStore onboardingStore + required property var loginAccountsModel + + property bool biometricsAvailable: Qt.platform.os === Constants.mac + required property bool isBiometricsLogin + + readonly property string selectedUserKeyId: loginUserSelector.selectedUserKeyId + + // methods: Onboarding.Enums.LoginMethod enum + signal loginRequested(string keyUid, int method, var data) + signal onboardingCreateProfileFlowRequested() + signal onboardingLoginFlowRequested() + signal unlockWithSeedphraseRequested() + signal lostKeycard() + + QtObject { + id: d + + readonly property bool currentUserIsKeycard: loginUserSelector.keycardCreatedAccount + + readonly property Settings settings: Settings { + category: "Login" + property string lastKeyUid + } + + function doPasswordLogin(password: string) { + if (password.length === 0) + return + + root.loginRequested(d.settings.lastKeyUid, Onboarding.LoginMethod.Password, {"password": password}) + passwordBox.clear() + } + + function doKeycardLogin(pin: string) { + if (pin.length === 0) + return + + root.loginRequested(d.settings.lastKeyUid, Onboarding.LoginMethod.Keycard, {"pin": pin}) + } + } + + Component.onCompleted: { + loginUserSelector.setSelection(d.settings.lastKeyUid) + passwordBox.forceActiveFocus() // FIXME handle password or keycard case + } + + implicitWidth: 1200 + implicitHeight: 700 + + padding: 40 + + background: Rectangle { + color: Theme.palette.background + } + + contentItem: Item { + ColumnLayout { + width: Math.min(400, parent.width) + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 200 + anchors.bottom: parent.bottom + spacing: Theme.padding + + StatusImage { + Layout.preferredWidth: 90 + Layout.preferredHeight: 90 + Layout.alignment: Qt.AlignHCenter + source: Theme.png("status") + mipmap: true + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 4 + radius: 12 + samples: 25 + spread: 0.2 + color: Theme.palette.dropShadow + } + } + + StatusBaseText { + id: headerText + Layout.fillWidth: true + text: qsTr("Welcome back") + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + LoginUserSelector { + id: loginUserSelector + Layout.topMargin: 20 + Layout.fillWidth: true + Layout.preferredHeight: 64 + loginAccountsModel: root.loginAccountsModel + currentKeycardLocked: root.onboardingStore.keycardState === Onboarding.KeycardState.Locked + onLoginUserRequested: (keyUid) => { + d.settings.lastKeyUid = keyUid + if (d.currentUserIsKeycard) { + keycardBox.clear() + } else { + passwordBox.clear() + passwordBox.forceActiveFocus() + } + } + onOnboardingCreateProfileFlowRequested: root.onboardingCreateProfileFlowRequested() + onOnboardingLoginFlowRequested: root.onboardingLoginFlowRequested() + } + + LoginPasswordBox { + Layout.fillWidth: true + id: passwordBox + visible: !d.currentUserIsKeycard + enabled: !!loginUserSelector.selectedUserKeyId + isBiometricsLogin: root.isBiometricsLogin + onLoginRequested: (password) => d.doPasswordLogin(password) + } + + LoginKeycardBox { + Layout.fillWidth: true + id: keycardBox + visible: d.currentUserIsKeycard + isBiometricsLogin: root.isBiometricsLogin + keycardState: root.onboardingStore.keycardState + tryToSetPinFunction: root.onboardingStore.setPin + keycardRemainingPinAttempts: root.onboardingStore.keycardRemainingPinAttempts + onUnlockWithSeedphraseRequested: root.unlockWithSeedphraseRequested() + onLoginRequested: (pin) => d.doKeycardLogin(pin) + } + + Item { Layout.fillHeight: true } + + StatusButton { + Layout.alignment: Qt.AlignHCenter + size: StatusBaseButton.Size.Small + visible: d.currentUserIsKeycard + normalColor: "transparent" + borderWidth: 1 + borderColor: Theme.palette.baseColor2 + text: qsTr("Lost this Keycard?") + onClicked: root.lostKeycard() + } + } + } +} diff --git a/ui/app/AppLayouts/Login/components/LoginKeycardBox.qml b/ui/app/AppLayouts/Login/components/LoginKeycardBox.qml new file mode 100644 index 00000000000..d908322a654 --- /dev/null +++ b/ui/app/AppLayouts/Login/components/LoginKeycardBox.qml @@ -0,0 +1,193 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding.enums 1.0 + +Control { + id: root + + required property bool isBiometricsLogin + required property int keycardState + property var tryToSetPinFunction: (pin) => { console.error("LoginKeycardBox::tryToSetPinFunction: IMPLEMENT ME"); return false } + required property int keycardRemainingPinAttempts + + signal loginRequested(string pin) + signal unlockWithSeedphraseRequested() + + function clear() { + d.wrongPin = false + pinInputField.statesInitialization() + pinInputField.forceFocus() + } + + verticalPadding: 20 + horizontalPadding: Theme.padding + + QtObject { + id: d + property bool wrongPin + } + + background: Rectangle { + color: "transparent" + border.width: 1 + border.color: Theme.palette.baseColor2 + radius: Theme.radius + } + + contentItem: ColumnLayout { + spacing: 12 + StatusIcon { + Layout.alignment: Qt.AlignHCenter + id: touchIdIcon + icon: "touch-id" + color: Theme.palette.baseColor1 + visible: false + } + StatusBaseText { + Layout.fillWidth: true + id: infoText + horizontalAlignment: Qt.AlignHCenter + elide: Text.ElideRight + color: Theme.palette.baseColor1 + } + StatusButton { + Layout.fillWidth: true + id: btnUnlockWithSeedphrase + visible: false + text: qsTr("Unlock with recovery phrase") + onClicked: root.unlockWithSeedphraseRequested() + } + StatusPinInput { + Layout.alignment: Qt.AlignHCenter + id: pinInputField + validator: StatusIntValidator { bottom: 0; top: 999999 } + visible: false + + onPinInputChanged: { + if (pinInput.length === 6) { + if (root.tryToSetPinFunction(pinInput)) { + root.loginRequested(pinInput) + d.wrongPin = false + } else { + d.wrongPin = true + pinInputField.statesInitialization() + pinInputField.forceFocus() + } + } + } + } + } + + states: [ + // normal/intro states + State { + name: "plugin" + when: root.keycardState === Onboarding.KeycardState.PluginReader || + root.keycardState === -1 + PropertyChanges { + target: infoText + text: qsTr("Plug in Keycard reader...") + } + }, + State { + name: "insert" + when: root.keycardState === Onboarding.KeycardState.InsertKeycard + PropertyChanges { + target: infoText + text: qsTr("Insert your Keycard...") + } + }, + State { + name: "reading" + when: root.keycardState === Onboarding.KeycardState.ReadingKeycard + PropertyChanges { + target: infoText + text: qsTr("Reading Keycard...") + } + }, + // error states + State { + name: "notKeycard" + when: root.keycardState === Onboarding.KeycardState.NotKeycard + PropertyChanges { + target: infoText + text: "".arg(Theme.palette.dangerColor1) + qsTr("Oops this isn’t a Keycard.
Remove card and insert a Keycard.") + "
" + } + }, + State { + name: "wrongKeycard" + when: root.keycardState === Onboarding.KeycardState.WrongKeycard || + root.keycardState === Onboarding.KeycardState.MaxPairingSlotsReached + PropertyChanges { + target: infoText + text: "".arg(Theme.palette.dangerColor1) + qsTr("Wrong Keycard for this profile inserted.
Remove card and insert the correct one.") + "
" + } + }, + State { + name: "noService" + when: root.keycardState === Onboarding.KeycardState.NoPCSCService + PropertyChanges { + target: infoText + text: "".arg(Theme.palette.dangerColor1) + qsTr("Smartcard reader service unavailable") + "" + } + }, + State { + name: "locked" + when: root.keycardState === Onboarding.KeycardState.Locked + PropertyChanges { + target: infoText + text: "".arg(Theme.palette.dangerColor1) + qsTr("Keycard locked") + "" + } + PropertyChanges { + target: btnUnlockWithSeedphrase + visible: true + } + }, + State { + name: "empty" + when: root.keycardState === Onboarding.KeycardState.Empty + PropertyChanges { + target: infoText + text: "".arg(Theme.palette.dangerColor1) + qsTr("The inserted Keycard is empty.
Remove card and insert the correct one.") + "
" + } + }, + State { + name: "wrongPin" + extend: "notEmpty" + when: root.keycardState === Onboarding.KeycardState.NotEmpty && d.wrongPin + PropertyChanges { + target: infoText + text: "".arg(Theme.palette.dangerColor1) + qsTr("PIN incorrect. %n attempt(s) remaining.", "", root.keycardRemainingPinAttempts) + "" + } + }, + // exit states + State { + name: "notEmpty" + when: root.keycardState === Onboarding.KeycardState.NotEmpty && !d.wrongPin + PropertyChanges { + target: infoText + text: qsTr("Enter Keycard PIN") + } + PropertyChanges { + target: pinInputField + visible: true + } + StateChangeScript { + script: { + pinInputField.forceFocus() + } + } + PropertyChanges { + target: touchIdIcon + visible: root.isBiometricsLogin + } + } + ] +} diff --git a/ui/app/AppLayouts/Login/components/LoginPasswordBox.qml b/ui/app/AppLayouts/Login/components/LoginPasswordBox.qml new file mode 100644 index 00000000000..56843f49add --- /dev/null +++ b/ui/app/AppLayouts/Login/components/LoginPasswordBox.qml @@ -0,0 +1,39 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Controls 0.1 + +import AppLayouts.Login.controls 1.0 + +FocusScope { + id: root + + required property bool isBiometricsLogin + + signal loginRequested(string password) + + function clear() { + txtPassword.clear() + } + + ColumnLayout { + spacing: 20 + anchors.fill: parent + LoginPasswordInput { + Layout.fillWidth: true + id: txtPassword + objectName: "loginPasswordInput" + focus: true + isBiometricsLogin: root.isBiometricsLogin + onAccepted: root.loginRequested(text) + } + + StatusButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Log In") + enabled: txtPassword.text !== "" + onClicked: root.loginRequested(txtPassword.text) + } + } +} diff --git a/ui/app/AppLayouts/Login/components/qmldir b/ui/app/AppLayouts/Login/components/qmldir new file mode 100644 index 00000000000..23bab5aa20c --- /dev/null +++ b/ui/app/AppLayouts/Login/components/qmldir @@ -0,0 +1,2 @@ +LoginKeycardBox 1.0 LoginKeycardBox.qml +LoginPasswordBox 1.0 LoginPasswordBox.qml diff --git a/ui/app/AppLayouts/Login/controls/LoginPasswordInput.qml b/ui/app/AppLayouts/Login/controls/LoginPasswordInput.qml new file mode 100644 index 00000000000..67730281bae --- /dev/null +++ b/ui/app/AppLayouts/Login/controls/LoginPasswordInput.qml @@ -0,0 +1,64 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +StatusPasswordInput { + id: root + + required property bool isBiometricsLogin + + rightPadding: iconsLayout.width + iconsLayout.anchors.rightMargin + placeholderText: qsTr("Password") + echoMode: d.showPassword ? TextInput.Normal : TextInput.Password + + QtObject { + id: d + property bool showPassword + } + + RowLayout { + id: iconsLayout + anchors.right: parent.right + anchors.rightMargin: Theme.halfPadding + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.halfPadding + + StatusIcon { + id: showPasswordButton + Layout.preferredHeight: 24 + Layout.preferredWidth: height + visible: root.text !== "" + icon: d.showPassword ? "hide" : "show" + color: hhandler.hovered ? Theme.palette.primaryColor1 : Theme.palette.baseColor1 + HoverHandler { + id: hhandler + cursorShape: hovered ? Qt.PointingHandCursor : undefined + } + TapHandler { + onSingleTapped: d.showPassword = !d.showPassword + } + StatusToolTip { + text: d.showPassword ? qsTr("Hide password") : qsTr("Reveal password") + visible: hhandler.hovered + } + } + Rectangle { + Layout.preferredWidth: 1 + Layout.preferredHeight: 28 + color: Theme.palette.directColor7 + visible: showPasswordButton.visible && touchIdIndicator.visible + } + StatusIcon { + id: touchIdIndicator + Layout.preferredHeight: 24 + Layout.preferredWidth: height + visible: root.isBiometricsLogin + icon: "touch-id" + color: Theme.palette.baseColor1 + } + } +} diff --git a/ui/app/AppLayouts/Login/controls/LoginUserSelector.qml b/ui/app/AppLayouts/Login/controls/LoginUserSelector.qml new file mode 100644 index 00000000000..dc858d0b087 --- /dev/null +++ b/ui/app/AppLayouts/Login/controls/LoginUserSelector.qml @@ -0,0 +1,160 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Popups 0.1 + +import AppLayouts.Login.controls 1.0 + +import SortFilterProxyModel 0.2 + +Control { + id: root + + required property var loginAccountsModel + required property bool currentKeycardLocked + + readonly property string selectedUserKeyId: userSelectorButton.keyUid + readonly property bool keycardCreatedAccount: userSelectorButton.keycardCreatedAccount + + signal onboardingCreateProfileFlowRequested() + signal onboardingLoginFlowRequested() + + signal loginUserRequested(string keyUid) + + function setSelection(keyUid: string) { + // reset user selector in case of empty call + if (!keyUid) { + userSelectorButton.keyUid = "" + userSelectorButton.label = "" + userSelectorButton.image = "" + userSelectorButton.colorHash = [] + userSelectorButton.colorId = 0 + userSelectorButton.keycardCreatedAccount = false + } else { + const item = ModelUtils.getByKey(root.loginAccountsModel, "keyUid", keyUid) + if (!!item) { + userSelectorButton.keyUid = keyUid + userSelectorButton.label = item.username + userSelectorButton.image = item.thumbnailImage + userSelectorButton.colorHash = item.colorHash + userSelectorButton.colorId = item.colorId + userSelectorButton.keycardCreatedAccount = item.keycardCreatedAccount + } + } + } + + function close() { + dropdown.close() + } + + QtObject { + id: d + + readonly property int maxPopupHeight: 300 + readonly property int delegateHeight: 64 + } + + contentItem: LoginUserSelectorDelegate { + id: userSelectorButton + background: Rectangle { + color: userSelectorButton.hovered ? Theme.palette.baseColor2 : "transparent" + border.width: 1 + border.color: Theme.palette.baseColor2 + radius: Theme.radius + } + rightPadding: spacing + Theme.padding + chevronIcon.width + StatusIcon { + id: chevronIcon + anchors.right: parent.right + anchors.rightMargin: Theme.padding + anchors.verticalCenter: parent.verticalCenter + icon: "chevron-down" + color: Theme.palette.baseColor1 + } + keycardLocked: root.currentKeycardLocked + onClicked: dropdown.opened ? dropdown.close() : dropdown.open() + } + + StatusDropdown { + id: dropdown + + y: parent.height + 4 + width: root.width + + verticalPadding: Theme.halfPadding + horizontalPadding: 0 + + contentItem: ColumnLayout { + spacing: 0 + StatusListView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.maximumHeight: d.maxPopupHeight + id: userSelectorPanel + model: SortFilterProxyModel { + id: proxyModel + sourceModel: root.loginAccountsModel + sorters: StringSorter { + roleName: "order" + sortOrder: Qt.AscendingOrder + } + filters: [ + ValueFilter { // don't show the currently selected item + roleName: "keyUid" + value: userSelectorButton.keyUid + inverted: true + } + ] + } + implicitHeight: contentHeight + spacing: 0 + delegate: LoginUserSelectorDelegate { + width: ListView.view.width + height: d.delegateHeight + keyUid: model.keyUid + label: model.username + image: model.thumbnailImage + colorId: model.colorId + colorHash: model.colorHash + // keycardLocked // FIXME needed here too? should be coming from the model imo + keycardCreatedAccount: model.keycardCreatedAccount + onClicked: { + root.close() + root.setSelection(keyUid) + root.loginUserRequested(keyUid) + } + } + } + StatusMenuSeparator { + Layout.fillWidth: true + } + LoginUserSelectorDelegate { + Layout.fillWidth: true + Layout.preferredHeight: d.delegateHeight + label: qsTr("Create profile") + image: "add" + isAction: true + onClicked: { + root.close() + root.onboardingCreateProfileFlowRequested() + } + } + LoginUserSelectorDelegate { + Layout.fillWidth: true + Layout.preferredHeight: d.delegateHeight + label: qsTr("Log in") + image: "profile" + isAction: true + onClicked: { + root.close() + root.onboardingLoginFlowRequested() + } + } + } + } +} diff --git a/ui/app/AppLayouts/Login/controls/LoginUserSelectorDelegate.qml b/ui/app/AppLayouts/Login/controls/LoginUserSelectorDelegate.qml new file mode 100644 index 00000000000..d8e05325bc7 --- /dev/null +++ b/ui/app/AppLayouts/Login/controls/LoginUserSelectorDelegate.qml @@ -0,0 +1,82 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 +import StatusQ.Popups 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils + +import shared.controls.chat 1.0 +import utils 1.0 + +ItemDelegate { + id: root + + property string keyUid + property string label + property int colorId + property var colorHash + property string image + property bool keycardCreatedAccount + property bool keycardLocked + property bool isAction + + verticalPadding: 12 + leftPadding: Theme.padding + rightPadding: Theme.bigPadding + spacing: Theme.padding + + background: Rectangle { + color: root.hovered || root.highlighted ? Theme.palette.statusSelect.menuItemHoverBackgroundColor + : "transparent" + } + + contentItem: RowLayout { + spacing: root.spacing + Loader { + id: userImageOrIcon + sourceComponent: root.isAction ? actionIcon : userImage + } + + Component { + id: actionIcon + StatusRoundIcon { + asset.name: root.image + } + } + + Component { + id: userImage + UserImage { + name: root.label + image: root.image + colorId: root.colorId + colorHash: root.colorHash + imageHeight: Constants.onboarding.userImageHeight + imageWidth: Constants.onboarding.userImageWidth + } + } + + StatusBaseText { + Layout.fillWidth: true + text: StatusQUtils.Emoji.parse(root.label) + color: root.isAction ? Theme.palette.primaryColor1 : Theme.palette.directColor1 + elide: Text.ElideRight + } + + Loader { + id: keycardIcon + active: root.keycardCreatedAccount + sourceComponent: StatusIcon { + icon: "keycard" + color: root.keycardLocked ? Theme.palette.dangerColor1 : Theme.palette.baseColor1 + } + } + } + + HoverHandler { + cursorShape: root.enabled ? Qt.PointingHandCursor : undefined + } +} diff --git a/ui/app/AppLayouts/Login/controls/qmldir b/ui/app/AppLayouts/Login/controls/qmldir new file mode 100644 index 00000000000..836fcae4cec --- /dev/null +++ b/ui/app/AppLayouts/Login/controls/qmldir @@ -0,0 +1,3 @@ +LoginUserSelector 1.0 LoginUserSelector.qml +LoginUserSelectorDelegate 1.0 LoginUserSelectorDelegate.qml +LoginPasswordInput 1.0 LoginPasswordInput.qml diff --git a/ui/app/AppLayouts/Login/qmldir b/ui/app/AppLayouts/Login/qmldir new file mode 100644 index 00000000000..c6291ec9526 --- /dev/null +++ b/ui/app/AppLayouts/Login/qmldir @@ -0,0 +1 @@ +LoginFlow 1.0 LoginFlow.qml diff --git a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml index 37525feff95..20b2f3a974c 100644 --- a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml +++ b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml @@ -23,7 +23,7 @@ Page { property int splashScreenDurationMs: 30000 property bool biometricsAvailable: Qt.platform.os === Constants.mac - property bool networkChecksEnabled + property bool networkChecksEnabled: true readonly property alias stack: stack @@ -148,6 +148,8 @@ Page { root.onboardingStore.setPin(pin) } + onReloadKeycardRequested: root.keycardReloaded() + onShareUsageDataRequested: (enabled) => { root.metricsStore.toggleCentralizedMetrics(enabled) Global.addCentralizedMetricIfEnabled(