diff --git a/internal/frontend/bridge-gui/bridge-gui/Resources.qrc b/internal/frontend/bridge-gui/bridge-gui/Resources.qrc index bc18612c..fbb413d6 100644 --- a/internal/frontend/bridge-gui/bridge-gui/Resources.qrc +++ b/internal/frontend/bridge-gui/bridge-gui/Resources.qrc @@ -110,6 +110,7 @@ qml/SetupGuide.qml qml/SetupWizard/SetupWizard.qml qml/SetupWizard/LoginLeftPane.qml + qml/SetupWizard/LoginRightPane.qml qml/SetupWizard/OnboardingLeftPane.qml qml/SetupWizard/OnboardingRightPane.qml qml/SetupWizard/StepDescriptionBox.qml diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/LoginRightPane.qml b/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/LoginRightPane.qml new file mode 100644 index 00000000..a6764d9f --- /dev/null +++ b/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/LoginRightPane.qml @@ -0,0 +1,420 @@ +// Copyright (c) 2023 Proton AG +// This file is part of Proton Mail Bridge. +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . +import QtQml +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Controls.impl +import Proton + +FocusScope { + id: root + + property ColorScheme colorScheme + property alias currentIndex: stackLayout.currentIndex + property alias username: usernameTextField.text + + signal loginAbort(string username, bool wasSignedOut) + + function abort() { + root.reset(); + loginAbort(usernameTextField.text, false); + Backend.loginAbort(usernameTextField.text); + } + function reset(clearUsername = false) { + stackLayout.currentIndex = 0; + loginNormalLayout.reset(clearUsername); + login2FALayout.reset(); + login2PasswordLayout.reset(); + } + + implicitHeight: children[0].implicitHeight + implicitWidth: children[0].implicitWidth + state: "Page 1" + + states: [ + State { + name: "Page 1" + + PropertyChanges { + currentIndex: 0 + target: stackLayout + } + }, + State { + name: "Page 2" + + PropertyChanges { + currentIndex: 1 + target: stackLayout + } + }, + State { + name: "Page 3" + + PropertyChanges { + currentIndex: 2 + target: stackLayout + } + } + ] + + StackLayout { + id: stackLayout + function loginFailed() { + signInButton.loading = false; + usernameTextField.enabled = true; + usernameTextField.error = true; + passwordTextField.enabled = true; + passwordTextField.error = true; + } + + anchors.fill: parent + + Connections { + function onLogin2FAError(_) { + console.assert(stackLayout.currentIndex === 1, "Unexpected login2FAError"); + twoFAButton.loading = false; + twoFactorPasswordTextField.enabled = true; + twoFactorPasswordTextField.error = true; + twoFactorPasswordTextField.errorString = qsTr("Your code is incorrect"); + twoFactorPasswordTextField.focus = true; + } + function onLogin2FAErrorAbort(_) { + console.assert(stackLayout.currentIndex === 1, "Unexpected login2FAErrorAbort"); + root.reset(); + errorLabel.text = qsTr("Incorrect login credentials. Please try again."); + } + function onLogin2FARequested(username) { + console.assert(stackLayout.currentIndex === 0, "Unexpected login2FARequested"); + twoFactorUsernameLabel.text = username; + stackLayout.currentIndex = 1; + twoFactorPasswordTextField.focus = true; + } + function onLogin2PasswordError(_) { + console.assert(stackLayout.currentIndex === 2, "Unexpected login2PasswordError"); + secondPasswordButton.loading = false; + secondPasswordTextField.enabled = true; + secondPasswordTextField.error = true; + secondPasswordTextField.errorString = qsTr("Your mailbox password is incorrect"); + secondPasswordTextField.focus = true; + } + function onLogin2PasswordErrorAbort(_) { + console.assert(stackLayout.currentIndex === 2, "Unexpected login2PasswordErrorAbort"); + root.reset(); + errorLabel.text = qsTr("Incorrect login credentials. Please try again."); + } + function onLogin2PasswordRequested() { + console.assert(stackLayout.currentIndex === 0 || stackLayout.currentIndex === 1, "Unexpected login2PasswordRequested"); + stackLayout.currentIndex = 2; + secondPasswordTextField.focus = true; + } + function onLoginAlreadyLoggedIn(_) { + stackLayout.currentIndex = 0; + root.reset(); + } + function onLoginConnectionError(_) { + if (stackLayout.currentIndex === 0) { + stackLayout.loginFailed(); + } + } + function onLoginFinished(_) { + stackLayout.currentIndex = 0; + root.reset(); + } + function onLoginFreeUserError() { + console.assert(stackLayout.currentIndex === 0, "Unexpected loginFreeUserError"); + stackLayout.loginFailed(); + } + function onLoginUsernamePasswordError(errorMsg) { + console.assert(stackLayout.currentIndex === 0, "Unexpected loginUsernamePasswordError"); + stackLayout.loginFailed(); + if (errorMsg !== "") + errorLabel.text = errorMsg; + else + errorLabel.text = qsTr("Incorrect login credentials"); + } + + target: Backend + } + ColumnLayout { + id: loginNormalLayout + function reset(clearUsername = false) { + signInButton.loading = false; + errorLabel.text = ""; + usernameTextField.enabled = true; + usernameTextField.error = false; + usernameTextField.errorString = ""; + usernameTextField.focus = true; + if (clearUsername) { + usernameTextField.text = ""; + } + passwordTextField.enabled = true; + passwordTextField.error = false; + passwordTextField.errorString = ""; + passwordTextField.text = ""; + } + + spacing: 0 + + Label { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 16 + colorScheme: root.colorScheme + text: qsTr("Sign in") + type: Label.LabelType.Title + } + Label { + id: subTitle + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 8 + color: root.colorScheme.text_weak + colorScheme: root.colorScheme + text: qsTr("Enter your Proton Account details.") + type: Label.LabelType.Body + } + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 36 + spacing: 0 + visible: errorLabel.text.length > 0 + + ColorImage { + color: root.colorScheme.signal_danger + height: errorLabel.lineHeight + source: "/qml/icons/ic-exclamation-circle-filled.svg" + sourceSize.height: errorLabel.lineHeight + } + Label { + id: errorLabel + Layout.fillWidth: true + Layout.leftMargin: 4 + color: root.colorScheme.signal_danger + colorScheme: root.colorScheme + type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption + wrapMode: Text.WordWrap + } + } + TextField { + id: usernameTextField + Layout.fillWidth: true + Layout.topMargin: 24 + colorScheme: root.colorScheme + focus: true + label: qsTr("Email or username") + validateOnEditingFinished: false + validator: function (str) { + if (str.length === 0) { + return qsTr("Enter email or username"); + } + } + + onAccepted: passwordTextField.forceActiveFocus() + onTextChanged: { + // remove "invalid username / password error" + if (error || errorLabel.text.length > 0) { + errorLabel.text = ""; + usernameTextField.error = false; + passwordTextField.error = false; + } + } + } + TextField { + id: passwordTextField + Layout.fillWidth: true + Layout.topMargin: 8 + colorScheme: root.colorScheme + echoMode: TextInput.Password + label: qsTr("Password") + validateOnEditingFinished: false + validator: function (str) { + if (str.length === 0) { + return qsTr("Enter password"); + } + } + + onAccepted: signInButton.checkAndSignIn() + onTextChanged: { + // remove "invalid username / password error" + if (error || errorLabel.text.length > 0) { + errorLabel.text = ""; + usernameTextField.error = false; + passwordTextField.error = false; + } + } + } + Button { + id: signInButton + function checkAndSignIn() { + usernameTextField.validate(); + passwordTextField.validate(); + if (usernameTextField.error || passwordTextField.error) { + return; + } + usernameTextField.enabled = false; + passwordTextField.enabled = false; + loading = true; + Backend.login(usernameTextField.text, Qt.btoa(passwordTextField.text)); + } + + Layout.fillWidth: true + Layout.topMargin: 24 + colorScheme: root.colorScheme + enabled: !loading + text: loading ? qsTr("Signing in") : qsTr("Sign in") + + onClicked: { + checkAndSignIn(); + } + } + Button { + id: cancelButton + Layout.fillWidth: true + Layout.topMargin: 24 + colorScheme: root.colorScheme + enabled: !loading + secondary: true + text: qsTr("Cancel") + + onClicked: { + root.abort(); + } + } + } + ColumnLayout { + id: login2FALayout + function reset() { + twoFAButton.loading = false; + twoFactorPasswordTextField.enabled = true; + twoFactorPasswordTextField.error = false; + twoFactorPasswordTextField.errorString = ""; + twoFactorPasswordTextField.text = ""; + } + + spacing: 0 + + Label { + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 16 + colorScheme: root.colorScheme + text: qsTr("Two-factor authentication") + type: Label.LabelType.Heading + } + Label { + id: twoFactorUsernameLabel + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 8 + color: root.colorScheme.text_weak + colorScheme: root.colorScheme + type: Label.LabelType.Lead + } + TextField { + id: twoFactorPasswordTextField + Layout.fillWidth: true + Layout.topMargin: 32 + assistiveText: qsTr("Enter the 6-digit code") + colorScheme: root.colorScheme + label: qsTr("Two-factor code") + validateOnEditingFinished: false + validator: function (str) { + if (str.length === 0) { + return qsTr("Enter the 6-digit code"); + } + } + + onAccepted: { + twoFAButton.onClicked(); + } + onTextChanged: { + if (text.length >= 6) { + twoFAButton.onClicked(); + } + } + } + Button { + id: twoFAButton + Layout.fillWidth: true + Layout.topMargin: 24 + colorScheme: root.colorScheme + enabled: !loading + text: loading ? qsTr("Authenticating") : qsTr("Authenticate") + + onClicked: { + twoFactorPasswordTextField.validate(); + if (twoFactorPasswordTextField.error) { + return; + } + twoFactorPasswordTextField.enabled = false; + loading = true; + Backend.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text)); + } + } + } + ColumnLayout { + id: login2PasswordLayout + function reset() { + secondPasswordButton.loading = false; + secondPasswordTextField.enabled = true; + secondPasswordTextField.error = false; + secondPasswordTextField.errorString = ""; + secondPasswordTextField.text = ""; + } + + spacing: 0 + + Label { + Layout.alignment: Qt.AlignCenter + Layout.topMargin: 16 + colorScheme: root.colorScheme + text: qsTr("Unlock your mailbox") + type: Label.LabelType.Heading + } + TextField { + id: secondPasswordTextField + Layout.fillWidth: true + Layout.topMargin: 8 + implicitHeight + 24 + subTitle.implicitHeight + colorScheme: root.colorScheme + echoMode: TextInput.Password + label: qsTr("Mailbox password") + validateOnEditingFinished: false + validator: function (str) { + if (str.length === 0) { + return qsTr("Enter password"); + } + } + + onAccepted: { + secondPasswordButton.onClicked(); + } + } + Button { + id: secondPasswordButton + Layout.fillWidth: true + Layout.topMargin: 24 + colorScheme: root.colorScheme + enabled: !loading + text: loading ? qsTr("Unlocking") : qsTr("Unlock") + + onClicked: { + secondPasswordTextField.validate(); + if (secondPasswordTextField.error) { + return; + } + secondPasswordTextField.enabled = false; + loading = true; + Backend.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text)); + } + } + } + } +} diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/SetupWizard.qml b/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/SetupWizard.qml index 2327355d..17b3d02e 100644 --- a/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/SetupWizard.qml +++ b/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/SetupWizard.qml @@ -22,6 +22,9 @@ Item { property ColorScheme colorScheme + function closeWizard() { + root.visible = false; + } function start() { root.visible = true; leftContent.currentIndex = 0; @@ -31,8 +34,16 @@ Item { root.visible = true; leftContent.currentIndex = 1; rightContent.currentIndex = 1; + loginRightPane.reset(true); } + Connections { + function onLoginFinished() { + root.closeWizard(); + } + + target: Backend + } RowLayout { anchors.fill: parent spacing: 0 @@ -74,8 +85,8 @@ Item { anchors.horizontalCenter: parent.horizontalCenter fillMode: Image.PreserveAspectFit height: 24 - source: root.colorScheme.mail_logo_with_wordmark mipmap: true + source: root.colorScheme.mail_logo_with_wordmark } } Rectangle { @@ -104,10 +115,15 @@ Item { } // stack index 1 - Rectangle { + LoginRightPane { + id: loginRightPane Layout.fillHeight: true Layout.fillWidth: true - color: "#f00" + colorScheme: root.colorScheme + + onLoginAbort: { + root.closeWizard(); + } } } Label {