// Copyright (c) 2025 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 FocusScope { id: root enum RootStack { Login, TOTP, FIDO, TOTPOrFIDO, MailboxPassword, HV } property alias currentIndex: stackLayout.currentIndex property alias username: usernameTextField.text property var wizard property string hvLinkUrl: "" property bool hvLinkClicked: false signal loginAbort(string username, bool wasSignedOut) function abort() { root.reset(); loginAbort(usernameTextField.text, false); Backend.loginAbort(usernameTextField.text); } function reset(clearUsername = false) { stackLayout.currentIndex = Login.RootStack.Login; loginLayout.reset(clearUsername); totpLayout.reset(); mailboxPasswordLayout.reset(); if (username.length === 0) { usernameTextField.forceActiveFocus(); } else { passwordTextField.forceActiveFocus(); } passwordTextField.hidePassword(); secondPasswordTextField.hidePassword(); hvLinkClicked = false; fidoLayout.reset(); } function resetViaHv() { usernameTextField.enabled = false; passwordTextField.enabled = false; signInButton.loading = true; secondPasswordButton.loading = false; secondPasswordTextField.enabled = true; hvLinkClicked = false; totpLayout.reset(); } 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 === Login.RootStack.TOTP, "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 === Login.RootStack.TOTP, "Unexpected login2FAErrorAbort"); root.reset(); errorLabel.text = qsTr("Incorrect login credentials. Please try again."); } function onLogin2FARequested(username) { console.assert(stackLayout.currentIndex === Login.RootStack.Login, "Unexpected login2FARequested"); twoFactorUsernameLabel.text = username; stackLayout.currentIndex = Login.RootStack.TOTP; twoFactorPasswordTextField.focus = true; switchToTotpButton.visible = false; switchToFidoButton.visible = false; } function onLoginFidoRequested(username) { fidoUsernameLabel.text = username; stackLayout.currentIndex = Login.RootStack.FIDO; switchToTotpButton.visible = false; switchToFidoButton.visible = false; } function onLogin2FAOrFidoRequested(username) { fidoUsernameLabel.text = username; twoFactorUsernameLabel.text = username; stackLayout.currentIndex = Login.RootStack.FIDO; switchToTotpButton.visible = true; switchToFidoButton.visible = true; } function onLoginFidoPinBlocked(_) { console.assert(stackLayout.currentIndex === Login.RootStack.FIDO, "Unexpected onLoginFidoPinBlocked"); root.reset(); } function onLoginFidoError(_) { console.assert(stackLayout.currentIndex === Login.RootStack.FIDO || stackLayout.currentIndex === Login.RootStack.Login, "Unexpected loginFidoError"); root.reset(); } function onLogin2PasswordError(_) { console.assert(stackLayout.currentIndex === Login.RootStack.MailboxPassword, "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 === Login.RootStack.MailboxPassword, "Unexpected login2PasswordErrorAbort"); root.reset(); errorLabel.text = qsTr("Incorrect login credentials. Please try again."); } function onLogin2PasswordRequested(username) { console.assert(stackLayout.currentIndex === Login.RootStack.Login || stackLayout.currentIndex === Login.RootStack.TOTP, "Unexpected login2PasswordRequested"); stackLayout.currentIndex = Login.RootStack.MailboxPassword; mailboxPasswordUsernameLabel.text = username; secondPasswordTextField.focus = true; } function onLoginAlreadyLoggedIn(_) { stackLayout.currentIndex = Login.RootStack.Login; root.reset(); } function onLoginConnectionError(_) { if (stackLayout.currentIndex === Login.RootStack.Login) { stackLayout.loginFailed(); } } function onLoginFinished(_) { stackLayout.currentIndex = Login.RootStack.Login; root.reset(); } function onLoginFreeUserError() { console.assert(stackLayout.currentIndex === Login.RootStack.Login, "Unexpected loginFreeUserError"); stackLayout.loginFailed(); } function onLoginUsernamePasswordError(errorMsg) { console.assert(stackLayout.currentIndex === Login.RootStack.Login, "Unexpected loginUsernamePasswordError"); stackLayout.loginFailed(); if (errorMsg !== "") errorLabel.text = errorMsg; else errorLabel.text = qsTr("Incorrect login credentials"); } function onLoginHvRequested(hvUrl) { console.assert(stackLayout.currentIndex === Login.RootStack.Login || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected loginHvRequested"); stackLayout.currentIndex = Login.RootStack.HV; hvUsernameLabel.text = usernameTextField.text; hvLinkUrl = hvUrl; } function onLoginHvError(_) { console.assert(stackLayout.currentIndex === Login.RootStack.Login || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected onLoginHvInvalidTokenError"); stackLayout.currentIndex = Login.RootStack.Login; root.resetViaHv(); root.reset() } target: Backend } Item { ColumnLayout { id: loginLayout function clearErrors() { usernameTextField.error = false; usernameTextField.errorString = ""; passwordTextField.error = false; passwordTextField.errorString = ""; errorLabel.text = ""; } function reset(clearUsername = false) { signInButton.loading = false; errorLabel.text = ""; usernameTextField.enabled = true; usernameTextField.focus = true; if (clearUsername) { usernameTextField.text = ""; } passwordTextField.enabled = true; passwordTextField.text = ""; clearErrors(); } anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: ProtonStyle.wizard_spacing_medium ColumnLayout { Layout.fillWidth: true spacing: ProtonStyle.wizard_spacing_small Label { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: qsTr("Sign in") type: Label.LabelType.Title } Label { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true color: wizard.colorScheme.text_weak colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: qsTr("Enter your Proton Account details.") type: Label.LabelType.Body } } RowLayout { Layout.fillWidth: true spacing: 0 ColorImage { color: wizard.colorScheme.signal_danger height: errorLabel.lineHeight source: "/qml/icons/ic-exclamation-circle-filled.svg" sourceSize.height: errorLabel.lineHeight visible: errorLabel.text.length > 0 } Label { id: errorLabel Layout.fillWidth: true Layout.leftMargin: 4 color: wizard.colorScheme.signal_danger colorScheme: wizard.colorScheme type: Label.LabelType.Caption_semibold wrapMode: Text.WordWrap } } TextField { id: usernameTextField Layout.fillWidth: true colorScheme: wizard.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: { loginLayout.clearErrors(); } } TextField { id: passwordTextField Layout.fillWidth: true colorScheme: wizard.colorScheme echoMode: TextInput.Password label: qsTr("Password") validateOnEditingFinished: false validator: function (str) { if (str.length === 0) { return qsTr("Enter password"); } } onAccepted: signInButton.checkAndSignIn() onTextChanged: { loginLayout.clearErrors(); } } Button { id: signInButton function checkAndSignIn() { usernameTextField.validate(); passwordTextField.validate(); if (usernameTextField.error || passwordTextField.error) { return; } usernameTextField.enabled = false; passwordTextField.enabled = false; loading = true; let usernameTextFiltered = usernameTextField.text.replace(/[\n\r]+$/, ""); let passwordTextFiltered = passwordTextField.text.replace(/[\n\r]+$/, ""); Backend.login(usernameTextFiltered, Qt.btoa(passwordTextFiltered)); } Layout.fillWidth: true colorScheme: wizard.colorScheme enabled: !loading text: loading ? qsTr("Signing in") : qsTr("Sign in") onClicked: { checkAndSignIn(); } } Button { Layout.fillWidth: true colorScheme: wizard.colorScheme enabled: !signInButton.loading secondary: true secondaryIsOpaque: true text: qsTr("Cancel") onClicked: { root.abort(); } } LinkLabel { Layout.alignment: Qt.AlignHCenter colorScheme: wizard.colorScheme external: true link: "https://proton.me/mail/pricing" text: qsTr("Create or upgrade your account") } } } Item { ColumnLayout { id: totpLayout function reset() { twoFAButton.loading = false; twoFactorPasswordTextField.enabled = true; twoFactorPasswordTextField.error = false; twoFactorPasswordTextField.errorString = ""; twoFactorPasswordTextField.text = ""; } anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: ProtonStyle.wizard_spacing_medium ColumnLayout { Layout.fillWidth: true spacing: ProtonStyle.wizard_spacing_small Label { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: qsTr("Two-factor authentication") type: Label.LabelType.Title } Label { id: twoFactorUsernameLabel Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true color: wizard.colorScheme.text_weak colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: "" type: Label.LabelType.Body } } Label { id: descriptionLabel Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: qsTr("You have enabled two-factor authentication. Enter the 6-digit code provided by your authenticator application.") type: Label.LabelType.Body wrapMode: Text.WordWrap } TextField { id: twoFactorPasswordTextField Layout.fillWidth: true colorScheme: wizard.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 colorScheme: wizard.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)); } } Button { Layout.fillWidth: true colorScheme: wizard.colorScheme enabled: !twoFAButton.loading secondary: true secondaryIsOpaque: true text: qsTr("Cancel") onClicked: { root.abort(); } } Label { id: switchToFidoButton Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: "" + qsTr("Use security key instead") + "" MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor enabled: !twoFAButton.loading onClicked: { stackLayout.currentIndex = Login.RootStack.FIDO; fidoLayout.reset(); totpLayout.reset(); } } } } } Item { ColumnLayout { id: fidoLayout anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: ProtonStyle.wizard_spacing_medium function reset() { fidoButton.loading = false; } ColumnLayout { Layout.fillWidth: true spacing: ProtonStyle.wizard_spacing_small Label { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: qsTr("Security key authentication") type: Label.LabelType.Title } Label { id: fidoUsernameLabel Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true color: wizard.colorScheme.text_weak colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: "" type: Label.LabelType.Body } } Label { id: fidoDescriptionLabel Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: qsTr("Security key authentication is enabled. Please connect your security key.") type: Label.LabelType.Body wrapMode: Text.WordWrap } Button { id: fidoButton Layout.fillWidth: true colorScheme: wizard.colorScheme enabled: !loading text: loading ? qsTr("Authenticating") : qsTr("Authenticate") onClicked: { if (Backend.goos === "windows") { fidoButton.loading = true; } Backend.loginFido(usernameTextField.text, ""); } } Button { Layout.fillWidth: true colorScheme: wizard.colorScheme enabled: !fidoButton.loading secondary: true secondaryIsOpaque: true text: qsTr("Cancel") onClicked: { root.abort(); } } Label { id: switchToTotpButton Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: "" + qsTr("Use authenticator app instead") + "" MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor enabled: !fidoButton.loading onClicked: { stackLayout.currentIndex = Login.RootStack.TOTP; fidoLayout.reset(); totpLayout.reset(); } } } } } Item { ColumnLayout { id: mailboxPasswordLayout function reset() { secondPasswordButton.loading = false; secondPasswordTextField.enabled = true; secondPasswordTextField.error = false; secondPasswordTextField.errorString = ""; secondPasswordTextField.text = ""; } anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: ProtonStyle.wizard_spacing_medium ColumnLayout { Layout.fillWidth: true spacing: ProtonStyle.wizard_spacing_small Label { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: qsTr("Unlock your mailbox") type: Label.LabelType.Title } Label { id: mailboxPasswordUsernameLabel Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true color: wizard.colorScheme.text_weak colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: "" type: Label.LabelType.Body } } Label { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: qsTr("You have secured your account with a separate mailbox password.") type: Label.LabelType.Body wrapMode: Text.WordWrap } TextField { id: secondPasswordTextField Layout.fillWidth: true colorScheme: wizard.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 colorScheme: wizard.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)); } } Button { Layout.fillWidth: true colorScheme: wizard.colorScheme enabled: !secondPasswordButton.loading secondary: true secondaryIsOpaque: true text: qsTr("Cancel") onClicked: { stackLayout.currentIndex = Login.RootStack.TOTP; twoFactorPasswordTextField.focus = true; } } } } Item { id: hvLayout ColumnLayout { Layout.fillWidth: true anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: ProtonStyle.wizard_spacing_extra_large ColumnLayout { spacing: ProtonStyle.wizard_spacing_medium ColumnLayout { spacing: ProtonStyle.wizard_spacing_small Label { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: qsTr("Human verification") type: Label.LabelType.Title wrapMode: Text.WordWrap } Label { id: hvUsernameLabel Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true color: wizard.colorScheme.text_weak colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter type: Label.LabelType.Body } } Label { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignHCenter text: qsTr("Please open the following link in your favourite web browser to verify you are human.") type: Label.LabelType.Body wrapMode: Text.WordWrap } } Label { id: hvRequestedUrlText type: Label.LabelType.Lead Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true colorScheme: wizard.colorScheme horizontalAlignment: Text.AlignLeft text: "" + hvLinkUrl.replace("&", "&")+ "" MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { Qt.openUrlExternally(hvLinkUrl); hvLinkClicked = true; } } } ColumnLayout { spacing: ProtonStyle.wizard_spacing_medium Button { id: hVContinueButton Layout.fillWidth: true colorScheme: wizard.colorScheme text: qsTr("I’ve completed the verification") enabled: hvLinkClicked function checkAndSignInHv() { console.assert(stackLayout.currentIndex === Login.RootStack.HV || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected checkInAndSignInHv") stackLayout.currentIndex = Login.RootStack.Login usernameTextField.validate(); passwordTextField.validate(); if (usernameTextField.error || passwordTextField.error) { return; } root.resetViaHv(); Backend.loginHv(usernameTextField.text, Qt.btoa(passwordTextField.text)); } onClicked: { checkAndSignInHv() } } Button { Layout.fillWidth: true colorScheme: wizard.colorScheme secondary: true secondaryIsOpaque: true text: qsTr("Cancel") onClicked: { root.abort(); } } } } } } }