[Settings]: Added change password view (#13284)

* [Settings]: Added change password view

Closes #10037

Adding configuration options to PasswordView

* feat(ChangePassword): Integrate ConfirmChangePasswordModal

1. Integrate with backend
2. Clean unused components

* feat: Add support to restart application

1. Adding restart app support in DOtherSide
2. Integrating nimqml
3. Expose in qml in Utils

* chore: Move changeDatabasePassword call to threadpool

* chore(squish): Fix failing tests due to settings index changes

---------

Co-authored-by: Alex Jbanca <alexjb@status.im>
This commit is contained in:
Alexandra Betouni 2024-02-09 13:31:37 +02:00 committed by GitHub
parent d29e5406de
commit 480985ca4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 573 additions and 295 deletions

View File

@ -110,6 +110,9 @@ QtObject:
proc downloadImageByUrl*(self: Utils, url: string, path: string) {.slot.} = proc downloadImageByUrl*(self: Utils, url: string, path: string) {.slot.} =
downloadImageByUrl(url, path) downloadImageByUrl(url, path)
proc restartApplication*(self: Utils) {.slot.} =
restartApplication()
proc generateQRCodeSVG*(self: Utils, text: string, border: int = 0): string = proc generateQRCodeSVG*(self: Utils, text: string, border: int = 0): string =
var qr0: array[0..qrcodegen_BUFFER_LEN_MAX, uint8] var qr0: array[0..qrcodegen_BUFFER_LEN_MAX, uint8]
var tempBuffer: array[0..qrcodegen_BUFFER_LEN_MAX, uint8] var tempBuffer: array[0..qrcodegen_BUFFER_LEN_MAX, uint8]

View File

@ -67,6 +67,9 @@ method mnemonicBackedUp*(self: Module) =
self.view.emitMnemonicBackedUpSignal() self.view.emitMnemonicBackedUpSignal()
method onPasswordChanged*(self: Module, success: bool, errorMsg: string) = method onPasswordChanged*(self: Module, success: bool, errorMsg: string) =
if singletonInstance.localAccountSettings.getStoreToKeychainValue() != LS_VALUE_NEVER:
singletonInstance.localAccountSettings.setStoreToKeychainValue(LS_VALUE_NOT_NOW)
self.view.emitPasswordChangedSignal(success, errorMsg) self.view.emitPasswordChangedSignal(success, errorMsg)
method getMnemonic*(self: Module): string = method getMnemonic*(self: Module): string =

View File

@ -0,0 +1,22 @@
import ../../../backend/privacy as status_privacy
type
ChangeDatabasePasswordTaskArg = ref object of QObjectTaskArg
accountId: string
currentPassword: string
newPassword: string
const changeDatabasePasswordTask*: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[ChangeDatabasePasswordTaskArg](argEncoded)
let output = %* {
"error": "",
"result": ""
}
try:
let result = status_privacy.changeDatabasePassword(arg.accountId, arg.currentPassword, arg.newPassword)
output["result"] = %result.result
except Exception as e:
output["error"] = %e.msg
arg.finish(output)

View File

@ -5,12 +5,15 @@ import ../settings/service as settings_service
import ../accounts/service as accounts_service import ../accounts/service as accounts_service
import ../../../app/core/eventemitter import ../../../app/core/eventemitter
import ../../../app/core/tasks/[qt, threadpool]
import ../../../backend/eth as status_eth import ../../../backend/eth as status_eth
import ../../../backend/privacy as status_privacy import ../../../backend/privacy as status_privacy
import ../../common/utils as common_utils import ../../common/utils as common_utils
include ./async_tasks
logScope: logScope:
topics = "privacy-service" topics = "privacy-service"
@ -27,6 +30,7 @@ QtObject:
events: EventEmitter events: EventEmitter
settingsService: settings_service.Service settingsService: settings_service.Service
accountsService: accounts_service.Service accountsService: accounts_service.Service
threadpool: threadpool.ThreadPool
proc delete*(self: Service) = proc delete*(self: Service) =
self.QObject.delete self.QObject.delete
@ -58,6 +62,36 @@ QtObject:
except Exception as e: except Exception as e:
error "error: ", procName="getDefaultAccount", errName = e.name, errDesription = e.msg error "error: ", procName="getDefaultAccount", errName = e.name, errDesription = e.msg
proc onChangeDatabasePasswordResponse(self: Service, responseStr: string) {.slot.} =
var data = OperationSuccessArgs(success: false, errorMsg: "")
try:
let response = responseStr.parseJson
# nim runtime error
let error = response["error"].getStr
if error != "":
data.errorMsg = error
self.events.emit(SIGNAL_PASSWORD_CHANGED, data)
return;
let result = response["result"]
if(result.contains("error")):
let errMsg = result["error"].getStr
if(errMsg.len == 0):
data.success = true
else:
# backend runtime error
data.errorMsg = errMsg
error "error: ", procName="changePassword", errDesription = errMsg
except Exception as e:
error "error: ", procName="changePassword", errName = e.name, errDesription = e.msg
data.errorMsg = e.msg
self.events.emit(SIGNAL_PASSWORD_CHANGED, data)
proc changePassword*(self: Service, password: string, newPassword: string) = proc changePassword*(self: Service, password: string, newPassword: string) =
try: try:
var data = OperationSuccessArgs(success: false, errorMsg: "") var data = OperationSuccessArgs(success: false, errorMsg: "")
@ -76,16 +110,15 @@ QtObject:
return return
let loggedInAccount = self.accountsService.getLoggedInAccount() let loggedInAccount = self.accountsService.getLoggedInAccount()
let response = status_privacy.changeDatabasePassword(loggedInAccount.keyUid, common_utils.hashPassword(password), common_utils.hashPassword(newPassword)) let arg = ChangeDatabasePasswordTaskArg(
tptr: cast[ByteAddress](changeDatabasePasswordTask),
if(response.result.contains("error")): vptr: cast[ByteAddress](self.vptr),
let errMsg = response.result["error"].getStr slot: "onChangeDatabasePasswordResponse",
if(errMsg.len == 0): accountId: loggedInAccount.keyUid,
data.success = true currentPassword: common_utils.hashPassword(password),
else: newPassword: common_utils.hashPassword(newPassword)
error "error: ", procName="changePassword", errDesription = errMsg )
self.threadpool.start(arg)
self.events.emit(SIGNAL_PASSWORD_CHANGED, data)
except Exception as e: except Exception as e:
error "error: ", procName="changePassword", errName = e.name, errDesription = e.msg error "error: ", procName="changePassword", errName = e.name, errDesription = e.msg

View File

@ -0,0 +1,88 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import AppLayouts.Profile.popups 1.0
import Storybook 1.0
import utils 1.0
SplitView {
id: root
PopupBackground {
id: popupBg
property var popupIntance: null
SplitView.fillWidth: true
SplitView.fillHeight: true
Button {
id: reopenButton
anchors.centerIn: parent
text: "Reopen"
enabled: globalUtilsMock.ready
onClicked: modal.open()
}
QtObject {
id: globalUtilsMock
property bool ready: false
property var globalUtils: QtObject {
function restartApplication() {
if (popupBg.popupIntance)
popupBg.popupIntance.close()
}
}
Component.onCompleted: {
Utils.globalUtilsInst = globalUtilsMock.globalUtils
globalUtilsMock.ready = true
}
}
ConfirmChangePasswordModal {
id: modal
visible: true
onChangePasswordRequested: {
passwordChangedTimer.start()
}
Component.onCompleted: {
popupBg.popupIntance = modal
}
Timer {
id: passwordChangedTimer
interval: 2000
repeat: false
onTriggered: {
if (successFlow.checked) {
modal.passwordSuccessfulyChanged()
} else {
modal.close()
}
}
}
}
}
LogsAndControlsPanel {
SplitView.minimumHeight: 100
SplitView.preferredHeight: 200
SplitView.preferredWidth: 300
ColumnLayout {
CheckBox {
id: successFlow
text: "%1 in 2 seconds".arg(successFlow.checked ? "Success" : "Error")
checked: true
}
}
}
}
// category: Popups
// https://www.figma.com/file/d0G7m8X6ELjQlFOEKQpn1g/Profile-WIP?type=design&node-id=11-111195&mode=design&t=j3guZtz78wkceVda-0

View File

@ -12,21 +12,22 @@ _EXTRA_MENU_ITEM_OBJ_NAME = "-ExtraMenuItem"
# These values are used to determine the dynamic `objectName` of the subsection item instead of using "design" properties like `text`. # These values are used to determine the dynamic `objectName` of the subsection item instead of using "design" properties like `text`.
class SettingsSubsection(Enum): class SettingsSubsection(Enum):
PROFILE: str = "0" + _MAIN_MENU_ITEM_OBJ_NAME PROFILE: str = "0" + _MAIN_MENU_ITEM_OBJ_NAME
CONTACTS: str = "1" + _MAIN_MENU_ITEM_OBJ_NAME PASSWORD: str = "1" + _MAIN_MENU_ITEM_OBJ_NAME
ENS_USERNAMES: str = "2" + _MAIN_MENU_ITEM_OBJ_NAME CONTACTS: str = "2" + _MAIN_MENU_ITEM_OBJ_NAME
MESSAGING: str = "3" + _APP_MENU_ITEM_OBJ_NAME ENS_USERNAMES: str = "3" + _MAIN_MENU_ITEM_OBJ_NAME
WALLET: str = "4" + _APP_MENU_ITEM_OBJ_NAME MESSAGING: str = "4" + _APP_MENU_ITEM_OBJ_NAME
APPEARANCE: str = "5" + _SETTINGS_MENU_ITEM_OBJ_NAME WALLET: str = "5" + _APP_MENU_ITEM_OBJ_NAME
LANGUAGE: str = "6" + _SETTINGS_MENU_ITEM_OBJ_NAME APPEARANCE: str = "6" + _SETTINGS_MENU_ITEM_OBJ_NAME
NOTIFICATIONS: str = "7" + _SETTINGS_MENU_ITEM_OBJ_NAME LANGUAGE: str = "7" + _SETTINGS_MENU_ITEM_OBJ_NAME
DEVICE_SETTINGS: str = "8" + _SETTINGS_MENU_ITEM_OBJ_NAME NOTIFICATIONS: str = "8" + _SETTINGS_MENU_ITEM_OBJ_NAME
BROWSER: str = "9" + _APP_MENU_ITEM_OBJ_NAME DEVICE_SETTINGS: str = "9" + _SETTINGS_MENU_ITEM_OBJ_NAME
ADVANCED: str = "10" + _SETTINGS_MENU_ITEM_OBJ_NAME BROWSER: str = "10" + _APP_MENU_ITEM_OBJ_NAME
ABOUT: str = "11" + _EXTRA_MENU_ITEM_OBJ_NAME ADVANCED: str = "11" + _SETTINGS_MENU_ITEM_OBJ_NAME
COMMUNITY: str = "12" + _APP_MENU_ITEM_OBJ_NAME ABOUT: str = "12" + _EXTRA_MENU_ITEM_OBJ_NAME
KEYCARD: str = "13" + _MAIN_MENU_ITEM_OBJ_NAME COMMUNITY: str = "13" + _APP_MENU_ITEM_OBJ_NAME
SIGNOUT: str = "16" + _EXTRA_MENU_ITEM_OBJ_NAME KEYCARD: str = "14" + _MAIN_MENU_ITEM_OBJ_NAME
BACKUP_SEED: str = "17" + _MAIN_MENU_ITEM_OBJ_NAME SIGNOUT: str = "17" + _EXTRA_MENU_ITEM_OBJ_NAME
BACKUP_SEED: str = "18" + _MAIN_MENU_ITEM_OBJ_NAME
# Main: # Main:
navBarListView_Settings_navbar_StatusNavBarTabButton = {"checkable": True, "container": mainWindow_navBarListView_ListView, "objectName": "Settings-navbar", "type": "StatusNavBarTabButton", "visible": True} navBarListView_Settings_navbar_StatusNavBarTabButton = {"checkable": True, "container": mainWindow_navBarListView_ListView, "objectName": "Settings-navbar", "type": "StatusNavBarTabButton", "visible": True}

View File

@ -93,6 +93,7 @@ Rectangle {
property alias subTitleBadgeComponent: subTitleBadgeLoader.sourceComponent property alias subTitleBadgeComponent: subTitleBadgeLoader.sourceComponent
property alias errorIcon: errorIcon property alias errorIcon: errorIcon
property alias statusListItemTagsRowLayout: statusListItemSubtitleTagsRow property alias statusListItemTagsRowLayout: statusListItemSubtitleTagsRow
property bool showLoadingIndicator: false
property int subTitleBadgeLoaderAlignment: Qt.AlignVCenter property int subTitleBadgeLoaderAlignment: Qt.AlignVCenter
@ -113,7 +114,9 @@ Rectangle {
} }
return Math.max(64, statusListItemTitleArea.height + 90) return Math.max(64, statusListItemTitleArea.height + 90)
} }
color: { color: bgColor
property color bgColor: {
if (sensor.containsMouse || root.highlighted) { if (sensor.containsMouse || root.highlighted) {
switch(type) { switch(type) {
case StatusListItem.Type.Primary: case StatusListItem.Type.Primary:
@ -153,6 +156,8 @@ Rectangle {
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
hoverEnabled: true hoverEnabled: true
StatusSmartIdenticon { StatusSmartIdenticon {
id: iconOrImage id: iconOrImage
anchors.left: parent.left anchors.left: parent.left
@ -160,9 +165,9 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
asset: root.asset asset: root.asset
name: root.title name: root.title
active: root.asset.isLetterIdenticon || active: ((root.asset.isLetterIdenticon ||
!!root.asset.name || !!root.asset.name ||
!!root.asset.emoji !!root.asset.emoji) && !root.showLoadingIndicator)
badge.border.color: root.color badge.border.color: root.color
ringSettings: root.ringSettings ringSettings: root.ringSettings
loading: root.loading loading: root.loading
@ -170,6 +175,19 @@ Rectangle {
onClicked: root.iconClicked(mouse) onClicked: root.iconClicked(mouse)
} }
Loader {
id: loadingIndicator
anchors.left: parent.left
anchors.leftMargin: root.leftPadding
anchors.top: statusListItemTitleArea.top
active: root.showLoadingIndicator
sourceComponent: StatusLoadingIndicator {
width: 24
height: 24
color: Theme.palette.baseColor1
}
}
Item { Item {
id: statusListItemTitleArea id: statusListItemTitleArea
@ -181,9 +199,9 @@ Rectangle {
return !root.titleAsideText && !isIconsRowVisible ? statusListItemTitleArea.right : undefined return !root.titleAsideText && !isIconsRowVisible ? statusListItemTitleArea.right : undefined
} }
anchors.left: iconOrImage.active ? iconOrImage.right : parent.left anchors.left: iconOrImage.active ? iconOrImage.right : loadingIndicator.active ? loadingIndicator.right : parent.left
anchors.right: statusListItemLabel.visible ? statusListItemLabel.left : statusListItemComponentsSlot.left anchors.right: statusListItemLabel.visible ? statusListItemLabel.left : statusListItemComponentsSlot.left
anchors.leftMargin: iconOrImage.active ? 16 : root.leftPadding anchors.leftMargin: iconOrImage.active ? 16 : loadingIndicator.active ? 6 : root.leftPadding
anchors.rightMargin: Math.max(root.rightPadding, titleIconsRow.requiredWidth) anchors.rightMargin: Math.max(root.rightPadding, titleIconsRow.requiredWidth)
anchors.verticalCenter: bottomModel.length === 0 ? parent.verticalCenter : undefined anchors.verticalCenter: bottomModel.length === 0 ? parent.verticalCenter : undefined
@ -291,7 +309,7 @@ Rectangle {
Loader { Loader {
id: subTitleBadgeLoader id: subTitleBadgeLoader
Layout.alignment: root.subTitleBadgeLoaderAlignment Layout.alignment: root.subTitleBadgeLoaderAlignment
visible: sourceComponent visible: sourceComponent && !root.showLoadingIndicator
} }
StatusTextWithLoadingState { StatusTextWithLoadingState {

View File

@ -59,6 +59,7 @@ Loader {
root.asset.bgColor root.asset.bgColor
image.fillMode: Image.PreserveAspectCrop image.fillMode: Image.PreserveAspectCrop
} }
Loader { Loader {
anchors.centerIn: parent anchors.centerIn: parent
active: root.asset.imgStatus === Image.Error || active: root.asset.imgStatus === Image.Error ||

View File

@ -7,7 +7,6 @@ import StatusQ.Core.Utils 0.1 as StatusQUtils
import utils 1.0 import utils 1.0
import shared.views 1.0 import shared.views 1.0
import "../../Profile/views"
import "../controls" import "../controls"
import "../stores" import "../stores"
@ -44,6 +43,7 @@ Item {
PasswordView { PasswordView {
id: view id: view
Layout.preferredWidth: root.width - 2 * Style.current.bigPadding Layout.preferredWidth: root.width - 2 * Style.current.bigPadding
Layout.maximumWidth: 460
Layout.fillHeight: true Layout.fillHeight: true
passwordStrengthScoreFunction: root.startupStore.getPasswordStrengthScore passwordStrengthScoreFunction: root.startupStore.getPasswordStrengthScore
highSizeIntro: true highSizeIntro: true

View File

@ -6,7 +6,7 @@ import QtQuick.Window 2.15
import utils 1.0 import utils 1.0
import shared 1.0 import shared 1.0
import shared.panels 1.0 import shared.panels 1.0
import shared.stores 1.0 import shared.stores 1.0 as SharedStores
import shared.popups.keycard 1.0 import shared.popups.keycard 1.0
import shared.stores.send 1.0 import shared.stores.send 1.0
@ -38,7 +38,7 @@ StatusSectionLayout {
required property TransactionStore transactionStore required property TransactionStore transactionStore
required property WalletAssetsStore walletAssetsStore required property WalletAssetsStore walletAssetsStore
required property CollectiblesStore collectiblesStore required property CollectiblesStore collectiblesStore
required property CurrenciesStore currencyStore required property SharedStores.CurrenciesStore currencyStore
backButtonName: root.store.backButtonName backButtonName: root.store.backButtonName
notificationCount: activityCenterStore.unreadNotificationsCount notificationCount: activityCenterStore.unreadNotificationsCount
@ -147,6 +147,19 @@ StatusSectionLayout {
} }
} }
Loader {
active: false
asynchronous: true
sourceComponent: ChangePasswordView {
implicitWidth: parent.width
implicitHeight: parent.height
privacyStore: root.store.privacyStore
passwordStrengthScoreFunction: SharedStores.RootStore.getPasswordStrengthScore
contentWidth: d.contentWidth
sectionTitle: root.store.getNameForSubsection(Constants.settingsSubsection.password)
}
}
Loader { Loader {
active: false active: false
asynchronous: true asynchronous: true

View File

@ -1,115 +0,0 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.12
import utils 1.0
import shared 1.0
import shared.views 1.0
import shared.panels 1.0
import shared.controls 1.0
import shared.stores 1.0
import StatusQ.Popups 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import "../views"
StatusModal {
id: root
property var privacyStore
signal passwordChanged()
function onChangePasswordResponse(success, errorMsg) {
if (success) {
if (Qt.platform.os === Constants.mac && localAccountSettings.storeToKeychainValue !== Constants.keychain.storedValue.never) {
localAccountSettings.storeToKeychainValue = Constants.keychain.storedValue.notNow;
}
passwordChanged()
}
else {
view.reset()
view.errorMsgText = errorMsg
console.warn("TODO: Display error message when change password action failure! ")
}
d.passwordProcessing = "";
submitBtn.loading = false;
}
QtObject {
id: d
// We temporarly store the password during "changePassword" call
// to store it to KeyChain after successfull change operation.
property string passwordProcessing: ""
function submit() {
submitBtn.loading = true
// 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()
}
}
Connections {
target: root.privacyStore.privacyModule
function onPasswordChanged(success: bool, errorMsg: string) {
onChangePasswordResponse(success, errorMsg)
}
}
width: 480
height: 546
closePolicy: submitBtn.loading? Popup.NoAutoClose : Popup.CloseOnEscape | Popup.CloseOnPressOutside
hasCloseButton: !submitBtn.loading
headerSettings.title: qsTr("Change password")
onOpened: view.reset()
PasswordView {
id: view
anchors {
fill: parent
topMargin: Style.current.padding
bottomMargin: Style.current.padding
leftMargin: Style.current.padding
rightMargin: Style.current.padding
}
passwordStrengthScoreFunction: RootStore.getPasswordStrengthScore
titleVisible: false
introText: qsTr("Change password used to unlock Status on this device & sign transactions.")
fixIntroTextWidth: true
createNewPsw: false
onReturnPressed: if(submitBtn.enabled) d.submit()
}
rightButtons: [
StatusButton {
id: submitBtn
objectName: "changePasswordModalSubmitButton"
text: qsTr("Change password and restart Status")
enabled: !submitBtn.loading && view.ready
property Timer sim: Timer {
id: pause
interval: 20
onTriggered: {
// Change current password call action to the backend
d.passwordProcessing = view.newPswText
root.privacyStore.changePassword(view.currentPswText, view.newPswText)
}
}
onClicked: { d.submit() }
}
]
// 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() }
}
}

View File

@ -1,56 +0,0 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.12
import StatusQ.Core 0.1
import StatusQ.Popups 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
StatusModal {
id: root
width: 400
height: 248
closePolicy: Popup.NoAutoClose
hasCloseButton: false
showHeader: false
contentItem: ColumnLayout {
anchors.fill: parent
anchors.margins: 45
spacing: Style.current.halfPadding
StatusIcon {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 26
Layout.preferredHeight: 26
icon: "checkmark"
color: Style.current.green
}
StatusBaseText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: 18
text: qsTr("<b>Password changed</b>")
color: Theme.palette.directColor1
}
StatusBaseText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: 13
color: Theme.palette.baseColor1
text: qsTr("You need to sign in again using the new password.")
}
StatusButton {
id: submitBtn
objectName:"changePasswordSuccessModalSignOutAndQuitButton"
Layout.alignment: Qt.AlignHCenter
text: qsTr("Sign out & Quit")
onClicked: {
//quits the app TODO: change this to logout instead when supported
Qt.quit();
}
}
}
}

View File

@ -38,7 +38,7 @@ ModalPopup {
type: StatusBaseButton.Type.Danger type: StatusBaseButton.Type.Danger
text: qsTr("Restart") text: qsTr("Restart")
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
onClicked: Qt.quit() onClicked: Utils.restartApplication();
} }
} }
} }

View File

@ -0,0 +1,136 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.12
import QtQml.Models 2.15
import utils 1.0
import shared 1.0
import shared.views 1.0
import shared.panels 1.0
import shared.controls 1.0
import shared.stores 1.0
import StatusQ.Core 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import "../views"
StatusDialog {
id: root
signal changePasswordRequested()
// Currently this modal handles only the happy path
// The error is handled in the caller
function passwordSuccessfulyChanged() {
d.dbEncryptionInProgress = false;
d.passwordChanged = true;
}
QtObject {
id: d
function reset() {
d.dbEncryptionInProgress = false;
d.passwordChanged = false;
}
property bool passwordChanged: false
property bool dbEncryptionInProgress: false
}
onClosed: {
d.reset();
}
width: 480
height: 546
closePolicy: d.passwordChanged || d.dbEncryptionInProgress ? Popup.NoAutoClose : Popup.CloseOnEscape | Popup.CloseOnPressOutside
Column {
anchors.fill: parent
anchors.margins: 16
spacing: 20
StatusBaseText {
width: parent.width
wrapMode: Text.WordWrap
text: qsTr("Your data must now be re-encrypted with your new password. This process may take some time, during which you wont be able to interact with the app. Do not quit the app or turn off your device. Doing so will lead to data corruption, loss of your Status profile and the inability to restart Status.")
}
Item {
width: parent.width
height: 76
Rectangle {
anchors.fill: parent
visible: d.passwordChanged
border.color: Theme.palette.successColor1
color: Theme.palette.successColor1
radius: 8
opacity: .1
}
StatusListItem {
id: listItem
anchors.fill: parent
sensor.enabled: false
visible: (d.dbEncryptionInProgress || d.passwordChanged)
title: !d.dbEncryptionInProgress ? qsTr("Re-encryption complete") :
qsTr("Re-encrypting your data with your new password...")
subTitle: !d.dbEncryptionInProgress ? qsTr("Restart Status and log in using your new password") :
qsTr("Do not quit the app of turn off your device")
statusListItemSubTitle.customColor: !d.passwordChanged ? Style.current.red : Theme.palette.successColor1
statusListItemIcon.active: d.passwordChanged
asset.name: "checkmark-circle"
asset.width: 24
asset.height: 24
asset.bgWidth: 0
asset.bgHeight: 0
asset.color: Theme.palette.successColor1
showLoadingIndicator: (d.dbEncryptionInProgress && !d.passwordChanged)
asset.isLetterIdenticon: false
border.width: !d.passwordChanged ? 1 : 0
border.color: Theme.palette.baseColor5
color: d.passwordChanged ? "transparent" : bgColor
}
}
}
header: StatusDialogHeader {
visible: true
headline.title: qsTr("Change password")
actions.closeButton.visible: !(d.passwordChanged || d.dbEncryptionInProgress)
actions.closeButton.onClicked: root.close()
}
footer: StatusDialogFooter {
id: footer
leftButtons: ObjectModel {
StatusFlatButton {
text: qsTr("Cancel")
visible: !d.dbEncryptionInProgress && !d.passwordChanged
textColor: Style.current.darkGrey
onClicked: { root.close(); }
}
}
rightButtons: ObjectModel {
StatusButton {
id: submitBtn
objectName: "changePasswordModalSubmitButton"
text: !d.dbEncryptionInProgress && !d.passwordChanged ? qsTr("Re-encrypt data using new password") : qsTr("Restart status")
enabled: !d.dbEncryptionInProgress
onClicked: {
if (d.passwordChanged) {
Utils.restartApplication();
} else {
d.dbEncryptionInProgress = true
root.changePasswordRequested()
}
}
}
}
}
}

View File

@ -8,3 +8,4 @@ RemoveKeypairPopup 1.0 RemoveKeypairPopup.qml
TokenListPopup 1.0 TokenListPopup.qml TokenListPopup 1.0 TokenListPopup.qml
WalletKeypairAccountMenu 1.0 WalletKeypairAccountMenu.qml WalletKeypairAccountMenu 1.0 WalletKeypairAccountMenu.qml
WalletAddressMenu 1.0 WalletAddressMenu.qml WalletAddressMenu 1.0 WalletAddressMenu.qml
ConfirmChangePasswordModal 1.0 ConfirmChangePasswordModal.qml

View File

@ -94,6 +94,9 @@ QtObject {
append({subsection: Constants.settingsSubsection.profile, append({subsection: Constants.settingsSubsection.profile,
text: qsTr("Profile"), text: qsTr("Profile"),
icon: "profile"}) icon: "profile"})
append({subsection: Constants.settingsSubsection.password,
text: qsTr("Password"),
icon: "profile"})
append({subsection: Constants.settingsSubsection.keycard, append({subsection: Constants.settingsSubsection.keycard,
text: qsTr("Keycard"), text: qsTr("Keycard"),
icon: "keycard"}) icon: "keycard"})

View File

@ -0,0 +1,102 @@
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 shared.stores 1.0
import shared.views 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
import AppLayouts.Profile.popups 1.0
SettingsContentBase {
id: root
property var privacyStore
property var passwordStrengthScoreFunction: function () {}
//TODO https://github.com/status-im/status-desktop/issues/13302
// titleRowComponentLoader.sourceComponent: Item {
// implicitWidth: 226
// implicitHeight: 38
// StatusSwitch {
// LayoutMirroring.enabled: true
// text: qsTr("Enable biometrics")
// onToggled: {
// //
// }
// }
// }
ColumnLayout {
PasswordView {
id: choosePasswordForm
width: 507
height: 660
createNewPsw: false
title: qsTr("Change your password.")
titleSize: 17
contentAlignment: Qt.AlignLeft
passwordStrengthScoreFunction: root.passwordStrengthScoreFunction
onReadyChanged: {
submitBtn.enabled = ready
}
onReturnPressed: {
if (ready) {
confirmPasswordChangePopup.open();
}
}
}
RowLayout {
Layout.fillWidth: true
StatusLinkText {
text: qsTr("Clear & cancel")
onClicked: {
choosePasswordForm.reset();
}
}
Item { Layout.fillWidth: true }
StatusButton {
id: submitBtn
Layout.alignment: Qt.AlignRight
objectName: "changePasswordModalSubmitButton"
text: qsTr("Change password")
enabled: choosePasswordForm.ready
onClicked: { confirmPasswordChangePopup.open(); }
}
}
ConfirmChangePasswordModal {
id: confirmPasswordChangePopup
onChangePasswordRequested: {
root.privacyStore.changePassword(choosePasswordForm.currentPswText, choosePasswordForm.newPswText);
}
Connections {
target: root.privacyStore.privacyModule
function onPasswordChanged(success: bool, errorMsg: string) {
if (success) {
confirmPasswordChangePopup.passwordSuccessfulyChanged()
return
}
choosePasswordForm.reset()
choosePasswordForm.errorMsgText = errorMsg
confirmPasswordChangePopup.close()
}
}
}
}
}

View File

@ -233,19 +233,6 @@ SettingsContentBase {
socialLinksModel: root.profileStore.temporarySocialLinksModel socialLinksModel: root.profileStore.temporarySocialLinksModel
} }
Component {
id: changePasswordModal
ChangePasswordModal {
privacyStore: root.privacyStore
onPasswordChanged: Global.openPopup(successPopup)
}
}
Component {
id: successPopup
ChangePasswordSuccessModal {}
}
Component { Component {
id: profilePreview id: profilePreview
ProfileDialog { ProfileDialog {

View File

@ -1,7 +1,8 @@
AboutView 1.0 AboutView.qml AboutView 1.0 AboutView.qml
LanguageView 1.0 LanguageView.qml
AppearanceView 1.0 AppearanceView.qml AppearanceView 1.0 AppearanceView.qml
NotificationsView 1.0 NotificationsView.qml
CommunitiesView 1.0 CommunitiesView.qml
BrowserView 1.0 BrowserView.qml BrowserView 1.0 BrowserView.qml
ChangePasswordView 1.0 ChangePasswordView.qml
CommunitiesView 1.0 CommunitiesView.qml
LanguageView 1.0 LanguageView.qml
NotificationsView 1.0 NotificationsView.qml
SyncingView 1.0 SyncingView.qml SyncingView 1.0 SyncingView.qml

View File

@ -22,11 +22,11 @@ Item {
spacing: Style.current.padding spacing: Style.current.padding
PasswordView { PasswordView {
Layout.minimumWidth: 460
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
passwordStrengthScoreFunction: RootStore.getPasswordStrengthScore passwordStrengthScoreFunction: RootStore.getPasswordStrengthScore
highSizeIntro: true highSizeIntro: true
fixIntroTextWidth: true
newPswText: root.sharedKeycardModule.getNewPassword() newPswText: root.sharedKeycardModule.getNewPassword()
confirmationPswText: root.sharedKeycardModule.getNewPassword() confirmationPswText: root.sharedKeycardModule.getNewPassword()

View File

@ -19,11 +19,13 @@ ColumnLayout {
property bool createNewPsw: true property bool createNewPsw: true
property string title: qsTr("Create a password") property string title: qsTr("Create a password")
property bool titleVisible: true property bool titleVisible: true
property real titleSize: 22
property string introText: qsTr("Create a password to unlock Status on this device & sign transactions.") 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 recoverText: qsTr("You will not be able to recover this password if it is lost.")
property string strengthenText: qsTr("Minimum %n character(s). To strengthen your password consider including:", "", Constants.minPasswordLength) property string strengthenText: qsTr("Minimum %n character(s). To strengthen your password consider including:", "", Constants.minPasswordLength)
property bool highSizeIntro: false property bool highSizeIntro: false
property bool fixIntroTextWidth: false
property int contentAlignment: Qt.AlignHCenter
property var passwordStrengthScoreFunction: function () {} property var passwordStrengthScoreFunction: function () {}
@ -80,8 +82,6 @@ ColumnLayout {
readonly property var validatorRegexp: /^[!-~]{0,64}$/ readonly property var validatorRegexp: /^[!-~]{0,64}$/
readonly property string validatorErrMessage: qsTr("Only letters, numbers, underscores and hyphens allowed") readonly property string validatorErrMessage: qsTr("Only letters, numbers, underscores and hyphens allowed")
readonly property int defaultInputWidth: 416
// Password strength categorization / validation // Password strength categorization / validation
function lowerCaseValidator(text) { return (/[a-z]/.test(text)) } function lowerCaseValidator(text) { return (/[a-z]/.test(text)) }
function upperCaseValidator(text) { return (/[A-Z]/.test(text)) } function upperCaseValidator(text) { return (/[A-Z]/.test(text)) }
@ -142,16 +142,17 @@ ColumnLayout {
function isTooShort() { return newPswInput.text.length < Constants.minPasswordLength } function isTooShort() { return newPswInput.text.length < Constants.minPasswordLength }
} }
implicitWidth: 460
spacing: Style.current.bigPadding spacing: Style.current.bigPadding
z: root.zFront z: root.zFront
// View visual content: // View visual content:
StatusBaseText { StatusBaseText {
id: title id: title
Layout.alignment: Qt.AlignHCenter Layout.alignment: root.contentAlignment
visible: root.titleVisible visible: root.titleVisible
text: root.title text: root.title
font.pixelSize: 22 font.pixelSize: root.titleSize
font.bold: true font.bold: true
color: Theme.palette.directColor1 color: Theme.palette.directColor1
} }
@ -159,16 +160,16 @@ ColumnLayout {
ColumnLayout { ColumnLayout {
id: introColumn id: introColumn
Layout.preferredWidth: root.fixIntroTextWidth ? d.defaultInputWidth : parent.width Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: root.contentAlignment
spacing: 4 spacing: 4
StatusBaseText { StatusBaseText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: root.contentAlignment
text: root.introText text: root.introText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: root.contentAlignment
font.pixelSize: root.highSizeIntro ? Style.current.primaryTextFontSize : Style.current.tertiaryTextFontSize font.pixelSize: root.highSizeIntro ? Style.current.primaryTextFontSize : Style.current.tertiaryTextFontSize
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
@ -176,10 +177,10 @@ ColumnLayout {
StatusBaseText { StatusBaseText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: root.contentAlignment
text: root.recoverText text: root.recoverText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: root.contentAlignment
font.pixelSize: root.highSizeIntro ? Style.current.primaryTextFontSize : Style.current.tertiaryTextFontSize font.pixelSize: root.highSizeIntro ? Style.current.primaryTextFontSize : Style.current.tertiaryTextFontSize
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
color: Theme.palette.dangerColor1 color: Theme.palette.dangerColor1
@ -194,8 +195,8 @@ ColumnLayout {
z: root.zFront z: root.zFront
visible: !root.createNewPsw visible: !root.createNewPsw
Layout.preferredWidth: d.defaultInputWidth Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: root.contentAlignment
placeholderText: qsTr("Current password") placeholderText: qsTr("Current password")
echoMode: showPassword ? TextInput.Normal : TextInput.Password echoMode: showPassword ? TextInput.Normal : TextInput.Password
rightPadding: showHideCurrentIcon.width + showHideCurrentIcon.anchors.rightMargin + Style.current.padding / 2 rightPadding: showHideCurrentIcon.width + showHideCurrentIcon.anchors.rightMargin + Style.current.padding / 2
@ -219,7 +220,8 @@ ColumnLayout {
ColumnLayout { ColumnLayout {
spacing: 4 spacing: 4
z: root.zFront z: root.zFront
Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true
Layout.alignment: root.contentAlignment
StatusPasswordInput { StatusPasswordInput {
id: newPswInput id: newPswInput
@ -227,8 +229,8 @@ ColumnLayout {
property bool showPassword property bool showPassword
Layout.preferredWidth: d.defaultInputWidth Layout.alignment: root.contentAlignment
Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true
placeholderText: qsTr("New password") placeholderText: qsTr("New password")
echoMode: showPassword ? TextInput.Normal : TextInput.Password echoMode: showPassword ? TextInput.Normal : TextInput.Password
rightPadding: showHideNewIcon.width + showHideNewIcon.anchors.rightMargin + Style.current.padding / 2 rightPadding: showHideNewIcon.width + showHideNewIcon.anchors.rightMargin + Style.current.padding / 2
@ -269,6 +271,7 @@ ColumnLayout {
StatusPasswordStrengthIndicator { StatusPasswordStrengthIndicator {
id: strengthInditactor id: strengthInditactor
Layout.fillWidth: true
value: Math.min(Constants.minPasswordLength, newPswInput.text.length) value: Math.min(Constants.minPasswordLength, newPswInput.text.length)
from: 0 from: 0
to: Constants.minPasswordLength to: Constants.minPasswordLength
@ -280,46 +283,66 @@ ColumnLayout {
} }
} }
StatusBaseText { Rectangle {
id: strengthenTxt Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true
wrapMode: Text.WordWrap Layout.minimumHeight: 80
text: root.strengthenText border.color: Theme.palette.baseColor2
font.pixelSize: 12 border.width: 1
color: Theme.palette.baseColor1 radius: Style.current.radius
clip: true implicitHeight: strengthColumn.implicitHeight
} implicitWidth: strengthColumn.implicitWidth
RowLayout { ColumnLayout {
spacing: Style.current.padding id: strengthColumn
Layout.alignment: Qt.AlignHCenter anchors.fill: parent
anchors.margins: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
spacing: Style.current.padding
StatusBaseText { StatusBaseText {
id: lowerCaseTxt id: strengthenTxt
text: "• " + qsTr("Lower case") Layout.fillHeight: true
font.pixelSize: 12 Layout.alignment: Qt.AlignHCenter
color: d.containsLower ? Theme.palette.successColor1 : Theme.palette.baseColor1 wrapMode: Text.WordWrap
} text: root.strengthenText
font.pixelSize: 12
color: Theme.palette.baseColor1
clip: true
}
StatusBaseText { RowLayout {
id: upperCaseTxt spacing: Style.current.padding
text: "• " + qsTr("Upper case") Layout.alignment: Qt.AlignHCenter
font.pixelSize: 12
color: d.containsUpper ? Theme.palette.successColor1 : Theme.palette.baseColor1
}
StatusBaseText { StatusBaseText {
id: numbersTxt id: lowerCaseTxt
text: "• " + qsTr("Numbers") text: "• " + qsTr("Lower case")
font.pixelSize: 12 font.pixelSize: 12
color: d.containsNumbers ? Theme.palette.successColor1 : Theme.palette.baseColor1 color: d.containsLower ? Theme.palette.successColor1 : Theme.palette.baseColor1
} }
StatusBaseText { StatusBaseText {
id: symbolsTxt id: upperCaseTxt
text: "• " + qsTr("Symbols") text: "• " + qsTr("Upper case")
font.pixelSize: 12 font.pixelSize: 12
color: d.containsSymbols ? Theme.palette.successColor1 : Theme.palette.baseColor1 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
}
}
} }
} }
@ -330,8 +353,8 @@ ColumnLayout {
property bool showPassword property bool showPassword
z: root.zFront z: root.zFront
Layout.preferredWidth: d.defaultInputWidth Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: root.contentAlignment
placeholderText: qsTr("Confirm password") placeholderText: qsTr("Confirm password")
echoMode: showPassword ? TextInput.Normal : TextInput.Password echoMode: showPassword ? TextInput.Normal : TextInput.Password
rightPadding: showHideConfirmIcon.width + showHideConfirmIcon.anchors.rightMargin + Style.current.padding / 2 rightPadding: showHideConfirmIcon.width + showHideConfirmIcon.anchors.rightMargin + Style.current.padding / 2
@ -376,7 +399,7 @@ ColumnLayout {
StatusBaseText { StatusBaseText {
id: errorTxt id: errorTxt
Layout.alignment: Qt.AlignHCenter Layout.alignment: root.contentAlignment
Layout.fillHeight: true Layout.fillHeight: true
font.pixelSize: 12 font.pixelSize: 12
color: Theme.palette.dangerColor1 color: Theme.palette.dangerColor1

View File

@ -331,25 +331,26 @@ QtObject {
readonly property QtObject settingsSubsection: QtObject { readonly property QtObject settingsSubsection: QtObject {
readonly property int profile: 0 readonly property int profile: 0
readonly property int contacts: 1 readonly property int password: 1
readonly property int ensUsernames: 2 readonly property int contacts: 2
readonly property int messaging: 3 readonly property int ensUsernames: 3
readonly property int wallet: 4 readonly property int messaging: 4
readonly property int appearance: 5 readonly property int wallet:5
readonly property int language: 6 readonly property int appearance: 6
readonly property int notifications: 7 readonly property int language: 7
readonly property int syncingSettings: 8 readonly property int notifications: 8
readonly property int browserSettings: 9 readonly property int syncingSettings: 9
readonly property int advanced: 10 readonly property int browserSettings: 10
readonly property int about: 11 readonly property int advanced: 11
readonly property int communitiesSettings: 12 readonly property int about: 12
readonly property int keycard: 13 readonly property int communitiesSettings: 13
readonly property int about_terms: 14 // a subpage under "About" readonly property int keycard: 14
readonly property int about_privacy: 15 // a subpage under "About" readonly property int about_terms: 15 // a subpage under "About"
readonly property int about_privacy: 16 // a subpage under "About"
// special treatment; these do not participate in the main settings' StackLayout // special treatment; these do not participate in the main settings' StackLayout
readonly property int signout: 16 readonly property int signout: 17
readonly property int backUpSeed: 17 readonly property int backUpSeed: 18
} }
readonly property QtObject walletSettingsSubsection: QtObject { readonly property QtObject walletSettingsSubsection: QtObject {

View File

@ -17,6 +17,10 @@ QtObject {
readonly property int maxImgSizeBytes: Constants.maxUploadFilesizeMB * 1048576 /* 1 MB in bytes */ readonly property int maxImgSizeBytes: Constants.maxUploadFilesizeMB * 1048576 /* 1 MB in bytes */
readonly property int communityIdLength: 68 readonly property int communityIdLength: 68
function restartApplication() {
globalUtilsInst.restartApplication()
}
function isDigit(value) { function isDigit(value) {
return /^\d$/.test(value); return /^\d$/.test(value);
} }

View File

@ -127,6 +127,9 @@ DOS_API void DOS_CALL dos_qguiapplication_icon(const char *filename);
/// \note A QGuiApplication should have been already created through dos_qguiapplication_create() /// \note A QGuiApplication should have been already created through dos_qguiapplication_create()
DOS_API void DOS_CALL dos_qguiapplication_quit(void); DOS_API void DOS_CALL dos_qguiapplication_quit(void);
/// @brief Calls the QGuiApplication::quit() function of the current QGuiApplication and QProcess::startDetached to spawn another process
DOS_API void DOS_CALL dos_qguiapplication_restart(void);
/// \brief Free the memory of the current QGuiApplication /// \brief Free the memory of the current QGuiApplication
/// \note A QGuiApplication should have been already created through dos_qguiapplication_create() /// \note A QGuiApplication should have been already created through dos_qguiapplication_create()
DOS_API void DOS_CALL dos_qguiapplication_delete(void); DOS_API void DOS_CALL dos_qguiapplication_delete(void);

View File

@ -371,6 +371,12 @@ void dos_qguiapplication_quit()
QMetaObject::invokeMethod(qGuiApp, "quit", Qt::QueuedConnection); QMetaObject::invokeMethod(qGuiApp, "quit", Qt::QueuedConnection);
} }
void dos_qguiapplication_restart()
{
QProcess::startDetached(QCoreApplication::applicationFilePath());
dos_qguiapplication_quit();
}
void dos_qguiapplication_icon(const char *filename) void dos_qguiapplication_icon(const char *filename)
{ {
qGuiApp->setWindowIcon(QIcon(filename)); qGuiApp->setWindowIcon(QIcon(filename));

2
vendor/nimqml vendored

@ -1 +1 @@
Subproject commit 2d733c5ec6977edac451c4d0017911b59d01a310 Subproject commit 13a8890db484d3ff40b410c2ff4b3e3bd2e0e880