mirror of
https://github.com/status-im/status-desktop.git
synced 2025-01-10 14:26:34 +00:00
feat(Desktop/Password): New password view design
Created new PasswordView.qml component. Added logic in new component: - Type of characters introduced. - Length of strength indicator bar depending on characters length. - Show/hide icons. - Error messages. - Added algorithm for password categorisation. Added Onboarding screens and modified flow. Replaced ChangePasswordModal content. Closes #4785
This commit is contained in:
parent
4c677d55e8
commit
6a023443ac
@ -64,6 +64,28 @@ QtObject {
|
||||
}
|
||||
}
|
||||
|
||||
DSM.State {
|
||||
id: createPasswordState
|
||||
onEntered: loader.sourceComponent = createPassword
|
||||
|
||||
DSM.SignalTransition {
|
||||
targetState: appState
|
||||
signal: startupModule.appStateChanged
|
||||
guard: state === Constants.appState.main
|
||||
}
|
||||
}
|
||||
|
||||
DSM.State {
|
||||
id: confirmPasswordState
|
||||
onEntered: loader.sourceComponent = confirmPassword
|
||||
|
||||
DSM.SignalTransition {
|
||||
targetState: appState
|
||||
signal: startupModule.appStateChanged
|
||||
guard: state === Constants.appState.main
|
||||
}
|
||||
}
|
||||
|
||||
DSM.State {
|
||||
id: stateLogin
|
||||
onEntered: { onBoardingStepChanged(login, ""); }
|
||||
@ -105,6 +127,18 @@ QtObject {
|
||||
guard: path === "KeycardFlowSelection"
|
||||
}
|
||||
|
||||
DSM.SignalTransition {
|
||||
targetState: createPasswordState
|
||||
signal: applicationWindow.navigateTo
|
||||
guard: path === "CreatePassword"
|
||||
}
|
||||
|
||||
DSM.SignalTransition {
|
||||
targetState: confirmPasswordState
|
||||
signal: applicationWindow.navigateTo
|
||||
guard: path === "ConfirmPassword"
|
||||
}
|
||||
|
||||
DSM.FinalState {
|
||||
id: onboardingDoneState
|
||||
}
|
||||
@ -154,6 +188,7 @@ QtObject {
|
||||
property var existingKeyComponent: Component {
|
||||
id: existingKey
|
||||
ExistingKeyView {
|
||||
onShowCreatePasswordView: { Global.applicationWindow.navigateTo("CreatePassword") }
|
||||
onClosed: function () {
|
||||
if (root.hasAccounts) {
|
||||
Global.applicationWindow.navigateTo("InitialState")
|
||||
@ -167,6 +202,7 @@ QtObject {
|
||||
property var genKeyComponent: Component {
|
||||
id: genKey
|
||||
GenKeyView {
|
||||
onShowCreatePasswordView: { Global.applicationWindow.navigateTo("CreatePassword") }
|
||||
onClosed: function () {
|
||||
if (root.hasAccounts) {
|
||||
Global.applicationWindow.navigateTo("InitialState")
|
||||
@ -201,4 +237,38 @@ QtObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var d: QtObject {
|
||||
property string newPassword
|
||||
property string confirmationPassword
|
||||
}
|
||||
|
||||
property var createPasswordComponent: Component {
|
||||
id: createPassword
|
||||
CreatePasswordView {
|
||||
newPassword: d.newPassword
|
||||
confirmationPassword: d.confirmationPassword
|
||||
|
||||
onPasswordCreated: {
|
||||
d.newPassword = newPassword
|
||||
d.confirmationPassword = confirmationPassword
|
||||
applicationWindow.navigateTo("ConfirmPassword")
|
||||
}
|
||||
onBackClicked: {
|
||||
d.newPassword = ""
|
||||
d.confirmationPassword = ""
|
||||
applicationWindow.navigateTo("InitialState");
|
||||
console.warn("TODO: Integration with onboarding flow!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var confirmPasswordComponent: Component {
|
||||
id: confirmPassword
|
||||
ConfirmPasswordView {
|
||||
password: d.newPassword
|
||||
|
||||
onBackClicked: { applicationWindow.navigateTo("CreatePassword") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
140
ui/app/AppLayouts/Onboarding/views/ConfirmPasswordView.qml
Normal file
140
ui/app/AppLayouts/Onboarding/views/ConfirmPasswordView.qml
Normal file
@ -0,0 +1,140 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import shared.controls 1.0
|
||||
import shared 1.0
|
||||
import shared.panels 1.0
|
||||
import utils 1.0
|
||||
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
import "../stores"
|
||||
|
||||
Page {
|
||||
id: root
|
||||
|
||||
property string password
|
||||
|
||||
signal backClicked()
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Component.onCompleted: confPswInput.forceActiveFocus(Qt.MouseFocusReason)
|
||||
|
||||
Column {
|
||||
id: view
|
||||
spacing: 4 * Style.current.padding
|
||||
width: 416
|
||||
anchors.centerIn: parent
|
||||
|
||||
StatusBaseText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Have you written down your password?")
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
color: Theme.palette.directColor1
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Style.current.padding
|
||||
|
||||
StatusBaseText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("You will never be able to recover your password if you lose it.")
|
||||
font.pixelSize: 12
|
||||
color: Theme.palette.dangerColor1
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("If you need to, write it using pen and paper and keep in a safe place.")
|
||||
font.pixelSize: 12
|
||||
color: Theme.palette.baseColor1
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("If you lose your password you will lose access to your Status profile.")
|
||||
font.pixelSize: 12
|
||||
color: Theme.palette.baseColor1
|
||||
}
|
||||
}
|
||||
|
||||
// TODO replace with StatusInput as soon as it supports password
|
||||
Input {
|
||||
id: confPswInput
|
||||
|
||||
property bool showPassword: false
|
||||
|
||||
width: parent.width
|
||||
enabled: !submitBtn.loading
|
||||
placeholderText: submitBtn.loading ?
|
||||
qsTr("Connecting...") :
|
||||
qsTr("Confirm you password (again)")
|
||||
textField.echoMode: showPassword ? TextInput.Normal : TextInput.Password
|
||||
textField.validator: RegExpValidator { regExp: /^[!-~]+$/ } // That incudes NOT extended ASCII printable characters less space
|
||||
keepHeight: true
|
||||
textField.rightPadding: showHideCurrentIcon.width + showHideCurrentIcon.anchors.rightMargin + Style.current.padding / 2
|
||||
|
||||
StatusFlatRoundButton {
|
||||
id: showHideCurrentIcon
|
||||
visible: confPswInput.text !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 16
|
||||
width: 24
|
||||
height: 24
|
||||
icon.name: confPswInput.showPassword ? "hide" : "show"
|
||||
icon.color: Theme.palette.baseColor1
|
||||
|
||||
onClicked: confPswInput.showPassword = !confPswInput.showPassword
|
||||
}
|
||||
}
|
||||
|
||||
// Just a column filler to fit the design
|
||||
Item {
|
||||
height: Style.current.padding
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StatusButton {
|
||||
id: submitBtn
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Finalize Status Password Creation")
|
||||
enabled: !submitBtn.loading && confPswInput.text === root.password
|
||||
|
||||
property Timer sim: Timer {
|
||||
id: pause
|
||||
interval: 20
|
||||
onTriggered: {
|
||||
// Create new password call action to the backend
|
||||
OnboardingStore.onBoardingModul.storeSelectedAccountAndLogin(root.password)
|
||||
Global.applicationWindow.prepareForStoring(root.password, false)
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
confPswInput.text = ""
|
||||
submitBtn.loading = true
|
||||
// Create password operation blocks the UI so loading = true; will never have any affect until changePassword/createPassword is done.
|
||||
// Getting around it with a small pause (timer) in order to get the desired behavior
|
||||
pause.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Back button:
|
||||
StatusRoundButton {
|
||||
enabled: !submitBtn.loading
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.current.padding
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Style.current.padding
|
||||
icon.name: "arrow-left"
|
||||
onClicked: { root.backClicked() }
|
||||
}
|
||||
}
|
69
ui/app/AppLayouts/Onboarding/views/CreatePasswordView.qml
Normal file
69
ui/app/AppLayouts/Onboarding/views/CreatePasswordView.qml
Normal file
@ -0,0 +1,69 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
import utils 1.0
|
||||
|
||||
import "../../Profile/views"
|
||||
|
||||
Page {
|
||||
id: root
|
||||
|
||||
property string newPassword
|
||||
property string confirmationPassword
|
||||
|
||||
signal passwordCreated(string newPassword, string confirmationPassword)
|
||||
signal backClicked()
|
||||
|
||||
Component.onCompleted: { view.forceNewPswInputFocus() }
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
readonly property int zBehind: 1
|
||||
readonly property int zFront: 100
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
spacing: 4 * Style.current.padding
|
||||
anchors.centerIn: parent
|
||||
z: view.zFront
|
||||
|
||||
PasswordView {
|
||||
id: view
|
||||
newPswText: root.newPassword
|
||||
confirmationPswText: root.confirmationPassword
|
||||
}
|
||||
|
||||
StatusButton {
|
||||
id: submitBtn
|
||||
z: d.zFront
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: qsTr("Create password")
|
||||
enabled: view.ready
|
||||
onClicked: { passwordCreated(view.newPswText, view.confirmationPswText) }
|
||||
}
|
||||
}
|
||||
|
||||
// Back button:
|
||||
StatusRoundButton {
|
||||
z: d.zFront // Focusable / clickable component
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.current.padding
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Style.current.padding
|
||||
icon.name: "arrow-left"
|
||||
onClicked: { root.backClicked() }
|
||||
}
|
||||
|
||||
// By clicking anywhere outside password entries fields or focusable element in the view, it is needed to check if passwords entered matches
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: d.zBehind // Behind focusable components
|
||||
onClicked: { view.checkPasswordMatches() }
|
||||
}
|
||||
}
|
@ -9,6 +9,9 @@ import "../shared"
|
||||
|
||||
Item {
|
||||
property var onClosed: function () {}
|
||||
|
||||
signal showCreatePasswordView()
|
||||
|
||||
id: existingKeyView
|
||||
anchors.fill: parent
|
||||
|
||||
@ -38,7 +41,7 @@ Item {
|
||||
onButtonClicked: {
|
||||
recoverySuccessModal.wentNext = true
|
||||
recoverySuccessModal.close()
|
||||
createPasswordModal.open()
|
||||
showCreatePasswordView()
|
||||
}
|
||||
onClosed: function () {
|
||||
if (!recoverySuccessModal.wentNext) {
|
||||
@ -46,11 +49,4 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CreatePasswordModal {
|
||||
id: createPasswordModal
|
||||
onClosed: function () {
|
||||
existingKeyView.onClosed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,9 @@ import "../shared"
|
||||
|
||||
Item {
|
||||
property var onClosed: function () {}
|
||||
|
||||
signal showCreatePasswordView()
|
||||
|
||||
id: genKeyView
|
||||
anchors.fill: parent
|
||||
|
||||
@ -19,7 +22,7 @@ Item {
|
||||
onNextClick: function (selectedIndex, displayName) {
|
||||
wentNext = true
|
||||
OnboardingStore.setCurrentAccountAndDisplayName(selectedIndex, displayName)
|
||||
createPasswordModal.open()
|
||||
showCreatePasswordView()
|
||||
}
|
||||
onClosed: function () {
|
||||
if (!wentNext) {
|
||||
@ -27,11 +30,4 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CreatePasswordModal {
|
||||
id: createPasswordModal
|
||||
onClosed: function () {
|
||||
genKeyView.onClosed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ import shared.controls 1.0
|
||||
|
||||
import StatusQ.Popups 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
import "../views"
|
||||
|
||||
StatusModal {
|
||||
id: root
|
||||
@ -17,33 +20,15 @@ StatusModal {
|
||||
property var privacyStore
|
||||
signal passwordChanged()
|
||||
|
||||
width: 480
|
||||
height: 510
|
||||
closePolicy: Popup.NoAutoClose
|
||||
header.title: qsTr("Change password")
|
||||
|
||||
onOpened: root.reset()
|
||||
|
||||
function lengthValidator(text) {
|
||||
return text.length >= 6 ? "" : qsTr("At least 6 characters")
|
||||
}
|
||||
|
||||
function reset() {
|
||||
currentPasswordInput.state = "init"
|
||||
passwordInput.state = "init"
|
||||
confirmPasswordInput.state = "init"
|
||||
currentPasswordInput.forceActiveFocus(Qt.MouseFocusReason)
|
||||
}
|
||||
|
||||
function onChangePasswordResponse(success) {
|
||||
if (success) {
|
||||
passwordChanged()
|
||||
submitBtn.enabled = false;
|
||||
submitBtn.enabled = false
|
||||
} else {
|
||||
currentPasswordInput.state = "incorrect"
|
||||
currentPasswordInput.forceActiveFocus(Qt.MouseFocusReason)
|
||||
view.reset()
|
||||
console.warn("TODO: Display error message when change password action failure! ")
|
||||
}
|
||||
submitBtn.loading = false;
|
||||
submitBtn.loading = false
|
||||
}
|
||||
|
||||
Connections {
|
||||
@ -51,152 +36,50 @@ StatusModal {
|
||||
onPasswordChanged: onChangePasswordResponse(success)
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: contentItem
|
||||
anchors.fill: parent
|
||||
anchors {
|
||||
topMargin: Style.current.xlPadding + root.topPadding
|
||||
leftMargin: Style.current.xlPadding
|
||||
rightMargin: Style.current.xlPadding
|
||||
bottomMargin: Style.current.xlPadding + root.bottomPadding
|
||||
}
|
||||
spacing: Style.current.padding
|
||||
width: 480
|
||||
height: 546
|
||||
closePolicy: Popup.NoAutoClose
|
||||
header.title: qsTr("Change password")
|
||||
|
||||
// TODO replace with StatusInput as soon as it supports password
|
||||
Input {
|
||||
id: currentPasswordInput
|
||||
onOpened: view.reset()
|
||||
|
||||
readonly property bool ready: state == "typing" && validationError == ""
|
||||
|
||||
anchors.left: undefined
|
||||
anchors.right: undefined
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Current password")
|
||||
|
||||
textField.echoMode: TextInput.Password
|
||||
keepHeight: true
|
||||
placeholderText: ""
|
||||
state: "init"
|
||||
|
||||
onTextChanged: if (text != "" && state != "typing") state = "typing"
|
||||
onStateChanged: if (state == "init") resetInternal()
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "init"
|
||||
},
|
||||
State {
|
||||
name: "typing"
|
||||
PropertyChanges { target: currentPasswordInput; validationError: root.lengthValidator(text) }
|
||||
},
|
||||
State {
|
||||
name: "incorrect"
|
||||
PropertyChanges { target: currentPasswordInput; validationError: qsTr("Incorrect password") }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// TODO replace with StatusInput as soon as it supports password
|
||||
Input {
|
||||
id: passwordInput
|
||||
|
||||
readonly property bool ready: state == "typing" && validationError == ""
|
||||
|
||||
anchors.left: undefined
|
||||
anchors.right: undefined
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("New password")
|
||||
|
||||
textField.echoMode: TextInput.Password
|
||||
keepHeight: true
|
||||
placeholderText: ""
|
||||
state: "init"
|
||||
|
||||
onTextChanged: if (text != "" && state != "typing") state = "typing"
|
||||
onStateChanged: if (state == "init") resetInternal()
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "init"
|
||||
},
|
||||
State {
|
||||
name: "typing"
|
||||
PropertyChanges { target: passwordInput; validationError: root.lengthValidator(text) }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// TODO replace with StatusInput as soon as it supports password
|
||||
Input {
|
||||
id: confirmPasswordInput
|
||||
|
||||
readonly property bool ready: state == "typing" && validationError == ""
|
||||
|
||||
anchors.left: undefined
|
||||
anchors.right: undefined
|
||||
Layout.fillWidth: true
|
||||
label: qsTr("Confirm new password")
|
||||
|
||||
textField.echoMode: TextInput.Password
|
||||
keepHeight: true
|
||||
placeholderText: ""
|
||||
state: "init"
|
||||
|
||||
onTextChanged: if (text != "" && state != "typing") state = "typing"
|
||||
onStateChanged: if (state == "init") resetInternal()
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "init"
|
||||
},
|
||||
State {
|
||||
name: "typing"
|
||||
PropertyChanges {
|
||||
target: confirmPasswordInput;
|
||||
validationError: confirmPasswordInput.text != passwordInput.text ? qsTr("Password does not match") : ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Your password protects your keys. You need it to unlock Status and transact.")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.preferredWidth: 340
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: Style.current.secondaryText
|
||||
font.pixelSize: Style.current.tertiaryTextFontSize
|
||||
}
|
||||
PasswordView {
|
||||
id: view
|
||||
anchors.topMargin: Style.current.padding
|
||||
anchors.centerIn: parent
|
||||
titleVisible: false
|
||||
introText: qsTr("Change password used to unlock Status on this device & sign transactions.")
|
||||
createNewPsw: false
|
||||
}
|
||||
|
||||
rightButtons: [
|
||||
StatusButton {
|
||||
id: submitBtn
|
||||
|
||||
text: qsTr("Change password")
|
||||
enabled: !submitBtn.loading && currentPasswordInput.ready &&
|
||||
passwordInput.ready && confirmPasswordInput.ready
|
||||
text: qsTr("Change Password")
|
||||
enabled: !submitBtn.loading && view.ready
|
||||
|
||||
property Timer sim: Timer {
|
||||
id: pause
|
||||
interval: 20
|
||||
onTriggered: {
|
||||
root.privacyStore.changePassword(currentPasswordInput.text, passwordInput.text)
|
||||
// Change current password call action to the backend
|
||||
root.privacyStore.changePassword(view.currentPswText, view.newPswText)
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
submitBtn.loading = true;
|
||||
//changePassword operation blocks the UI so loading = true; will never
|
||||
//have any affect until changePassword is done. Getting around it with a
|
||||
//small pause (timer) in order to get the desired behavior
|
||||
// ChangePassword operation blocks the UI so loading = true; will never have any affect until changePassword/createPassword is done.
|
||||
// Getting around it with a small pause (timer) in order to get the desired behavior
|
||||
pause.start();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// By clicking anywhere outside password entries fields or focusable element in the view, it is needed to check if passwords entered matches
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: view.zBehind // Behind focusable components in the view
|
||||
onClicked: { view.checkPasswordMatches() }
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ ModalPopup {
|
||||
checked: localAccountSettings.storeToKeychainValue === Constants.storeToKeychainValueStore
|
||||
onCheckedChanged: {
|
||||
if (checked && localAccountSettings.storeToKeychainValue !== Constants.storeToKeychainValueStore) {
|
||||
// TODO: REFACTOR TO NEW PASWORD VIEW
|
||||
var storePassPopup = Global.openPopup(storePasswordModal)
|
||||
if(storePassPopup)
|
||||
{
|
||||
|
365
ui/app/AppLayouts/Profile/views/PasswordView.qml
Normal file
365
ui/app/AppLayouts/Profile/views/PasswordView.qml
Normal file
@ -0,0 +1,365 @@
|
||||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import shared.panels 1.0
|
||||
import shared.controls 1.0
|
||||
import utils 1.0
|
||||
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Components 0.1
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property bool ready: newPswInput.text.length >= root.minPswLen && newPswInput.text === confirmPswInput.text && errorTxt.text === ""
|
||||
property int minPswLen: 6
|
||||
property bool createNewPsw: true
|
||||
property string title: qsTr("Create a password")
|
||||
property bool titleVisible: true
|
||||
property string introText: qsTr("Create a password to unlock Status on this device & sign transactions.")
|
||||
property string recoverText: qsTr("You will not be able to recover this password if it is lost.")
|
||||
property string strengthenText: qsTr("Minimum 6 characers. To strengthen your password consider including:")
|
||||
readonly property int zBehind: 1
|
||||
readonly property int zFront: 100
|
||||
|
||||
property alias currentPswText: currentPswInput.text
|
||||
property alias newPswText: newPswInput.text
|
||||
property alias confirmationPswText: confirmPswInput.text
|
||||
property alias errorMsgText: errorTxt.text
|
||||
|
||||
function forceNewPswInputFocus() { newPswInput.forceActiveFocus(Qt.MouseFocusReason) }
|
||||
|
||||
function reset() {
|
||||
newPswInput.text = ""
|
||||
currentPswInput.text = ""
|
||||
confirmPswInput.text = ""
|
||||
errorTxt.text = ""
|
||||
strengthInditactor.strength = StatusPasswordStrengthIndicator.Strength.None
|
||||
|
||||
// Update focus:
|
||||
if(root.createNewPsw)
|
||||
newPswInput.forceActiveFocus(Qt.MouseFocusReason)
|
||||
else
|
||||
currentPswInput.forceActiveFocus(Qt.MouseFocusReason)
|
||||
}
|
||||
|
||||
function checkPasswordMatches() {
|
||||
if(newPswInput.text.length >= root.minPswLen) {
|
||||
errorTxt.text = ""
|
||||
if(confirmPswInput.text !== newPswInput.text) {
|
||||
errorTxt.text = qsTr("Passwords don't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
property bool containsLower: false
|
||||
property bool containsUpper: false
|
||||
property bool containsNumbers: false
|
||||
property bool containsSymbols: false
|
||||
|
||||
readonly property var validator: RegExpValidator { regExp: /^[!-~]+$/ } // That incudes NOT extended ASCII printable characters less space
|
||||
|
||||
// Password strength categorization / validation
|
||||
function lowerCaseValidator(text) { return (/[a-z]/.test(text)) }
|
||||
function upperCaseValidator(text) { return (/[A-Z]/.test(text)) }
|
||||
function numbersValidator(text) { return (/\d/.test(text)) }
|
||||
// That incudes NOT extended ASCII printable symbols less space:
|
||||
function symbolsValidator(text) { return (/[!-\/:-@[-`{-~]/.test(text)) }
|
||||
function findUniqueChars(text) {
|
||||
// The variable that contains the unique values
|
||||
let uniq = "";
|
||||
|
||||
for(let i = 0; i < text.length; i++) {
|
||||
// Checking if the uniq contains the character
|
||||
if(uniq.includes(text[i]) === false) {
|
||||
// If the character not present in uniq
|
||||
// Concatenate the character with uniq
|
||||
uniq += text[i]
|
||||
}
|
||||
}
|
||||
return uniq
|
||||
}
|
||||
|
||||
// Algorithm defined in functional requirements / Password categorization
|
||||
function getPswStrength() {
|
||||
let rules = 0
|
||||
let points = 0
|
||||
let strengthType = StatusPasswordStrengthIndicator.Strength.None
|
||||
|
||||
if(newPswInput.text.length >= root.minPswLen) { points += 10; rules++ }
|
||||
if(d.containsLower) { points += 5; rules++ }
|
||||
if(d.containsUpper) { points += 5; rules++ }
|
||||
if(d.containsNumbers) { points += 5; rules++ }
|
||||
if(d.containsSymbols) { points += 10; rules++ }
|
||||
|
||||
let uniq = d.findUniqueChars(newPswInput.text)
|
||||
if(uniq.length >= 5) { points += 5; rules++ }
|
||||
|
||||
// Update points according to rules used:
|
||||
points += rules * 10/*factor*/
|
||||
|
||||
// Strength decision taken:
|
||||
if(points > 0 && points < 40) strengthType = StatusPasswordStrengthIndicator.Strength.VeryWeak
|
||||
else if(points >= 40 && points < 60) strengthType = StatusPasswordStrengthIndicator.Strength.Weak
|
||||
else if(points >= 60 && points < 80) strengthType = StatusPasswordStrengthIndicator.Strength.SoSo
|
||||
else if(points >= 80 && points < 100) strengthType = StatusPasswordStrengthIndicator.Strength.Good
|
||||
else if(points >= 100) strengthType = StatusPasswordStrengthIndicator.Strength.Great
|
||||
return strengthType
|
||||
}
|
||||
|
||||
// Password validation / error message selection:
|
||||
function passwordValidation() {
|
||||
errorTxt.text = ""
|
||||
|
||||
// 3 rules to validate:
|
||||
// * Password is in pwnd passwords database
|
||||
if(isInPwndDatabase())
|
||||
errorTxt.text = qsTr("This password has been pwned and shouldn't be used")
|
||||
|
||||
// * Common password
|
||||
else if(isCommonPassword())
|
||||
errorTxt.text = qsTr("This password is a common word and shouldn't be used")
|
||||
|
||||
// * Password too short
|
||||
else if(isTooShort())
|
||||
errorTxt.text = qsTr("Password must be at least 6 characters long")
|
||||
}
|
||||
|
||||
function isInPwndDatabase() {
|
||||
// "TODO - Nice To Have: Pwnd password validation NOT implemented yet! "
|
||||
return false
|
||||
}
|
||||
|
||||
function isCommonPassword() {
|
||||
// "TODO - Nice To Have: Common password validation NOT implemented yet! "
|
||||
return false
|
||||
}
|
||||
|
||||
function isTooShort() { return newPswInput.text.length < root.minPswLen }
|
||||
}
|
||||
|
||||
spacing: 3 * Style.current.padding / 2
|
||||
z: root.zFront
|
||||
width: 416
|
||||
|
||||
// View visual content:
|
||||
StatusBaseText {
|
||||
id: title
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.titleVisible
|
||||
text: root.title
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
color: Theme.palette.directColor1
|
||||
}
|
||||
|
||||
Column {
|
||||
StatusBaseText {
|
||||
id: introTxt
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: root.introText
|
||||
font.pixelSize: 12
|
||||
color: Theme.palette.baseColor1
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
id: recoverTxt
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: root.recoverText
|
||||
font.pixelSize: 12
|
||||
color: Theme.palette.dangerColor1
|
||||
}
|
||||
}
|
||||
|
||||
// TODO replace with StatusInput as soon as it supports password
|
||||
Input {
|
||||
id: currentPswInput
|
||||
|
||||
property bool showPassword
|
||||
|
||||
z: root.zFront
|
||||
visible: !root.createNewPsw
|
||||
width: parent.width
|
||||
placeholderText: qsTr("Current password")
|
||||
textField.echoMode: showPassword ? TextInput.Normal : TextInput.Password
|
||||
textField.validator: d.validator
|
||||
keepHeight: true
|
||||
textField.rightPadding: showHideCurrentIcon.width + showHideCurrentIcon.anchors.rightMargin + Style.current.padding / 2
|
||||
|
||||
StatusFlatRoundButton {
|
||||
id: showHideCurrentIcon
|
||||
visible: currentPswInput.text !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 16
|
||||
width: 24
|
||||
height: 24
|
||||
icon.name: currentPswInput.showPassword ? "hide" : "show"
|
||||
icon.color: Theme.palette.baseColor1
|
||||
|
||||
onClicked: currentPswInput.showPassword = !currentPswInput.showPassword
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Style.current.padding / 2
|
||||
z: root.zFront
|
||||
width: parent.width
|
||||
|
||||
// TODO replace with StatusInput as soon as it supports password
|
||||
Input {
|
||||
id: newPswInput
|
||||
|
||||
property bool showPassword
|
||||
|
||||
width: parent.width
|
||||
placeholderText: qsTr("New password")
|
||||
textField.echoMode: showPassword ? TextInput.Normal : TextInput.Password
|
||||
textField.validator: d.validator
|
||||
keepHeight: true
|
||||
textField.rightPadding: showHideNewIcon.width + showHideNewIcon.anchors.rightMargin + Style.current.padding / 2
|
||||
|
||||
onTextChanged: {
|
||||
// Update password checkers
|
||||
d.containsLower = d.lowerCaseValidator(text)
|
||||
d.containsUpper = d.upperCaseValidator(text)
|
||||
d.containsNumbers = d.numbersValidator(text)
|
||||
d.containsSymbols = d.symbolsValidator(text)
|
||||
|
||||
// Update strength indicator:
|
||||
strengthInditactor.strength = d.getPswStrength()
|
||||
}
|
||||
|
||||
StatusFlatRoundButton {
|
||||
id: showHideNewIcon
|
||||
visible: newPswInput.text !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 16
|
||||
width: 24
|
||||
height: 24
|
||||
icon.name: newPswInput.showPassword ? "hide" : "show"
|
||||
icon.color: Theme.palette.baseColor1
|
||||
|
||||
onClicked: newPswInput.showPassword = !newPswInput.showPassword
|
||||
}
|
||||
}
|
||||
|
||||
StatusPasswordStrengthIndicator {
|
||||
id: strengthInditactor
|
||||
width: parent.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
value: newPswInput.text.length > root.minPswLen ? root.minPswLen : newPswInput.text.length
|
||||
from: 0
|
||||
to: root.minPswLen
|
||||
labelVeryWeak: qsTr("Very weak")
|
||||
labelWeak: qsTr("Weak")
|
||||
labelSoso: qsTr("So-so")
|
||||
labelGood: qsTr("Good")
|
||||
labelGreat: qsTr("Great")
|
||||
}
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
id: strengthenTxt
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: root.strengthenText
|
||||
font.pixelSize: 12
|
||||
color: Theme.palette.baseColor1
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Style.current.padding
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StatusBaseText {
|
||||
id: lowerCaseTxt
|
||||
text: "• " + qsTr("Lower case")
|
||||
font.pixelSize: 12
|
||||
color: d.containsLower ? Theme.palette.successColor1 : Theme.palette.baseColor1
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
id: upperCaseTxt
|
||||
text: "• " + qsTr("Upper case")
|
||||
font.pixelSize: 12
|
||||
color: d.containsUpper ? Theme.palette.successColor1 : Theme.palette.baseColor1
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
id: numbersTxt
|
||||
text: "• " + qsTr("Numbers")
|
||||
font.pixelSize: 12
|
||||
color: d.containsNumbers ? Theme.palette.successColor1 : Theme.palette.baseColor1
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
id: symbolsTxt
|
||||
text: "• " + qsTr("Symbols")
|
||||
font.pixelSize: 12
|
||||
color: d.containsSymbols ? Theme.palette.successColor1 : Theme.palette.baseColor1
|
||||
}
|
||||
}
|
||||
|
||||
// TODO replace with StatusInput as soon as it supports password
|
||||
Input {
|
||||
id: confirmPswInput
|
||||
|
||||
property bool showPassword
|
||||
|
||||
z: root.zFront
|
||||
width: parent.width
|
||||
placeholderText: qsTr("Confirm password")
|
||||
textField.echoMode: showPassword ? TextInput.Normal : TextInput.Password
|
||||
textField.validator: d.validator
|
||||
keepHeight: true
|
||||
textField.rightPadding: showHideConfirmIcon.width + showHideConfirmIcon.anchors.rightMargin + Style.current.padding / 2
|
||||
|
||||
onTextChanged: { if(textField.text.length === newPswInput.text.length) root.checkPasswordMatches() }
|
||||
textField.onFocusChanged: {
|
||||
// When clicking into the confirmation input, validate if new password:
|
||||
if(textField.focus) d.passwordValidation()
|
||||
|
||||
// When leaving the confirmation input because of the button or other input component is focused, check if password matches
|
||||
else root.checkPasswordMatches(true)
|
||||
}
|
||||
|
||||
StatusFlatRoundButton {
|
||||
id: showHideConfirmIcon
|
||||
visible: confirmPswInput.text !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 16
|
||||
width: 24
|
||||
height: 24
|
||||
icon.name: confirmPswInput.showPassword ? "hide" : "show"
|
||||
icon.color: Theme.palette.baseColor1
|
||||
|
||||
onClicked: confirmPswInput.showPassword = !confirmPswInput.showPassword
|
||||
}
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
id: errorTxt
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.pixelSize: 12
|
||||
color: Theme.palette.dangerColor1
|
||||
onTextChanged: {
|
||||
if(text === "") filler.visible = true
|
||||
else filler.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
// Just a column filler to keep the component height althought errorTxt.text is ""
|
||||
Item {
|
||||
id: filler
|
||||
width: root.width
|
||||
visible: true
|
||||
height: errorTxt.height
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user