From 480985ca4ed6d338699dc230c3a0fdb42c43ac5a Mon Sep 17 00:00:00 2001 From: Alexandra Betouni <31625338+alexandraB99@users.noreply.github.com> Date: Fri, 9 Feb 2024 13:31:37 +0200 Subject: [PATCH] [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 --- src/app/global/utils.nim | 3 + .../main/profile_section/privacy/module.nim | 3 + .../service/privacy/async_tasks.nim | 22 +++ src/app_service/service/privacy/service.nim | 53 +++++-- .../pages/ConfirmChangePasswordModalPage.qml | 88 ++++++++++++ .../global_shared/scripts/settings_names.py | 31 ++-- .../src/StatusQ/Components/StatusListItem.qml | 30 +++- .../Components/StatusSmartIdenticon.qml | 1 + .../Onboarding/views/CreatePasswordView.qml | 2 +- ui/app/AppLayouts/Profile/ProfileLayout.qml | 17 ++- .../Profile/popups/ChangePasswordModal.qml | 115 --------------- .../popups/ChangePasswordSuccessModal.qml | 56 -------- .../Profile/popups/ConfirmAppRestartModal.qml | 2 +- .../popups/ConfirmChangePasswordModal.qml | 136 ++++++++++++++++++ ui/app/AppLayouts/Profile/popups/qmldir | 1 + .../Profile/stores/ProfileSectionStore.qml | 3 + .../Profile/views/ChangePasswordView.qml | 102 +++++++++++++ .../Profile/views/MyProfileView.qml | 13 -- ui/app/AppLayouts/Profile/views/qmldir | 7 +- .../popups/keycard/states/CreatePassword.qml | 2 +- ui/imports/shared/views/PasswordView.qml | 131 ++++++++++------- ui/imports/utils/Constants.qml | 35 ++--- ui/imports/utils/Utils.qml | 4 + .../lib/include/DOtherSide/DOtherSide.h | 3 + vendor/DOtherSide/lib/src/DOtherSide.cpp | 6 + vendor/nimqml | 2 +- 26 files changed, 573 insertions(+), 295 deletions(-) create mode 100644 src/app_service/service/privacy/async_tasks.nim create mode 100644 storybook/pages/ConfirmChangePasswordModalPage.qml delete mode 100644 ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml delete mode 100644 ui/app/AppLayouts/Profile/popups/ChangePasswordSuccessModal.qml create mode 100644 ui/app/AppLayouts/Profile/popups/ConfirmChangePasswordModal.qml create mode 100644 ui/app/AppLayouts/Profile/views/ChangePasswordView.qml diff --git a/src/app/global/utils.nim b/src/app/global/utils.nim index ac00a23a90..eac34d4153 100644 --- a/src/app/global/utils.nim +++ b/src/app/global/utils.nim @@ -110,6 +110,9 @@ QtObject: proc downloadImageByUrl*(self: Utils, url: string, path: string) {.slot.} = downloadImageByUrl(url, path) + proc restartApplication*(self: Utils) {.slot.} = + restartApplication() + proc generateQRCodeSVG*(self: Utils, text: string, border: int = 0): string = var qr0: array[0..qrcodegen_BUFFER_LEN_MAX, uint8] var tempBuffer: array[0..qrcodegen_BUFFER_LEN_MAX, uint8] diff --git a/src/app/modules/main/profile_section/privacy/module.nim b/src/app/modules/main/profile_section/privacy/module.nim index 06b338e1f8..5214793e85 100644 --- a/src/app/modules/main/profile_section/privacy/module.nim +++ b/src/app/modules/main/profile_section/privacy/module.nim @@ -67,6 +67,9 @@ method mnemonicBackedUp*(self: Module) = self.view.emitMnemonicBackedUpSignal() 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) method getMnemonic*(self: Module): string = diff --git a/src/app_service/service/privacy/async_tasks.nim b/src/app_service/service/privacy/async_tasks.nim new file mode 100644 index 0000000000..0b5e94288c --- /dev/null +++ b/src/app_service/service/privacy/async_tasks.nim @@ -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) diff --git a/src/app_service/service/privacy/service.nim b/src/app_service/service/privacy/service.nim index 4325e5ed2c..fbd5ac88a3 100644 --- a/src/app_service/service/privacy/service.nim +++ b/src/app_service/service/privacy/service.nim @@ -5,12 +5,15 @@ import ../settings/service as settings_service import ../accounts/service as accounts_service import ../../../app/core/eventemitter +import ../../../app/core/tasks/[qt, threadpool] import ../../../backend/eth as status_eth import ../../../backend/privacy as status_privacy import ../../common/utils as common_utils +include ./async_tasks + logScope: topics = "privacy-service" @@ -27,6 +30,7 @@ QtObject: events: EventEmitter settingsService: settings_service.Service accountsService: accounts_service.Service + threadpool: threadpool.ThreadPool proc delete*(self: Service) = self.QObject.delete @@ -58,6 +62,36 @@ QtObject: except Exception as e: 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) = try: var data = OperationSuccessArgs(success: false, errorMsg: "") @@ -76,16 +110,15 @@ QtObject: return let loggedInAccount = self.accountsService.getLoggedInAccount() - let response = status_privacy.changeDatabasePassword(loggedInAccount.keyUid, common_utils.hashPassword(password), common_utils.hashPassword(newPassword)) - - if(response.result.contains("error")): - let errMsg = response.result["error"].getStr - if(errMsg.len == 0): - data.success = true - else: - error "error: ", procName="changePassword", errDesription = errMsg - - self.events.emit(SIGNAL_PASSWORD_CHANGED, data) + let arg = ChangeDatabasePasswordTaskArg( + tptr: cast[ByteAddress](changeDatabasePasswordTask), + vptr: cast[ByteAddress](self.vptr), + slot: "onChangeDatabasePasswordResponse", + accountId: loggedInAccount.keyUid, + currentPassword: common_utils.hashPassword(password), + newPassword: common_utils.hashPassword(newPassword) + ) + self.threadpool.start(arg) except Exception as e: error "error: ", procName="changePassword", errName = e.name, errDesription = e.msg diff --git a/storybook/pages/ConfirmChangePasswordModalPage.qml b/storybook/pages/ConfirmChangePasswordModalPage.qml new file mode 100644 index 0000000000..4c15922d91 --- /dev/null +++ b/storybook/pages/ConfirmChangePasswordModalPage.qml @@ -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 diff --git a/test/ui-test/testSuites/global_shared/scripts/settings_names.py b/test/ui-test/testSuites/global_shared/scripts/settings_names.py index ba4ce2404e..b10ce4e37d 100644 --- a/test/ui-test/testSuites/global_shared/scripts/settings_names.py +++ b/test/ui-test/testSuites/global_shared/scripts/settings_names.py @@ -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`. class SettingsSubsection(Enum): PROFILE: str = "0" + _MAIN_MENU_ITEM_OBJ_NAME - CONTACTS: str = "1" + _MAIN_MENU_ITEM_OBJ_NAME - ENS_USERNAMES: str = "2" + _MAIN_MENU_ITEM_OBJ_NAME - MESSAGING: str = "3" + _APP_MENU_ITEM_OBJ_NAME - WALLET: str = "4" + _APP_MENU_ITEM_OBJ_NAME - APPEARANCE: str = "5" + _SETTINGS_MENU_ITEM_OBJ_NAME - LANGUAGE: str = "6" + _SETTINGS_MENU_ITEM_OBJ_NAME - NOTIFICATIONS: str = "7" + _SETTINGS_MENU_ITEM_OBJ_NAME - DEVICE_SETTINGS: str = "8" + _SETTINGS_MENU_ITEM_OBJ_NAME - BROWSER: str = "9" + _APP_MENU_ITEM_OBJ_NAME - ADVANCED: str = "10" + _SETTINGS_MENU_ITEM_OBJ_NAME - ABOUT: str = "11" + _EXTRA_MENU_ITEM_OBJ_NAME - COMMUNITY: str = "12" + _APP_MENU_ITEM_OBJ_NAME - KEYCARD: str = "13" + _MAIN_MENU_ITEM_OBJ_NAME - SIGNOUT: str = "16" + _EXTRA_MENU_ITEM_OBJ_NAME - BACKUP_SEED: str = "17" + _MAIN_MENU_ITEM_OBJ_NAME + PASSWORD: str = "1" + _MAIN_MENU_ITEM_OBJ_NAME + CONTACTS: str = "2" + _MAIN_MENU_ITEM_OBJ_NAME + ENS_USERNAMES: str = "3" + _MAIN_MENU_ITEM_OBJ_NAME + MESSAGING: str = "4" + _APP_MENU_ITEM_OBJ_NAME + WALLET: str = "5" + _APP_MENU_ITEM_OBJ_NAME + APPEARANCE: str = "6" + _SETTINGS_MENU_ITEM_OBJ_NAME + LANGUAGE: str = "7" + _SETTINGS_MENU_ITEM_OBJ_NAME + NOTIFICATIONS: str = "8" + _SETTINGS_MENU_ITEM_OBJ_NAME + DEVICE_SETTINGS: str = "9" + _SETTINGS_MENU_ITEM_OBJ_NAME + BROWSER: str = "10" + _APP_MENU_ITEM_OBJ_NAME + ADVANCED: str = "11" + _SETTINGS_MENU_ITEM_OBJ_NAME + ABOUT: str = "12" + _EXTRA_MENU_ITEM_OBJ_NAME + COMMUNITY: str = "13" + _APP_MENU_ITEM_OBJ_NAME + KEYCARD: str = "14" + _MAIN_MENU_ITEM_OBJ_NAME + SIGNOUT: str = "17" + _EXTRA_MENU_ITEM_OBJ_NAME + BACKUP_SEED: str = "18" + _MAIN_MENU_ITEM_OBJ_NAME # Main: navBarListView_Settings_navbar_StatusNavBarTabButton = {"checkable": True, "container": mainWindow_navBarListView_ListView, "objectName": "Settings-navbar", "type": "StatusNavBarTabButton", "visible": True} diff --git a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml index 25ac3fb4f3..0252940533 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml @@ -93,6 +93,7 @@ Rectangle { property alias subTitleBadgeComponent: subTitleBadgeLoader.sourceComponent property alias errorIcon: errorIcon property alias statusListItemTagsRowLayout: statusListItemSubtitleTagsRow + property bool showLoadingIndicator: false property int subTitleBadgeLoaderAlignment: Qt.AlignVCenter @@ -113,7 +114,9 @@ Rectangle { } return Math.max(64, statusListItemTitleArea.height + 90) } - color: { + color: bgColor + + property color bgColor: { if (sensor.containsMouse || root.highlighted) { switch(type) { case StatusListItem.Type.Primary: @@ -153,6 +156,8 @@ Rectangle { acceptedButtons: Qt.NoButton hoverEnabled: true + + StatusSmartIdenticon { id: iconOrImage anchors.left: parent.left @@ -160,9 +165,9 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter asset: root.asset name: root.title - active: root.asset.isLetterIdenticon || + active: ((root.asset.isLetterIdenticon || !!root.asset.name || - !!root.asset.emoji + !!root.asset.emoji) && !root.showLoadingIndicator) badge.border.color: root.color ringSettings: root.ringSettings loading: root.loading @@ -170,6 +175,19 @@ Rectangle { 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 { id: statusListItemTitleArea @@ -181,9 +199,9 @@ Rectangle { 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.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.verticalCenter: bottomModel.length === 0 ? parent.verticalCenter : undefined @@ -291,7 +309,7 @@ Rectangle { Loader { id: subTitleBadgeLoader Layout.alignment: root.subTitleBadgeLoaderAlignment - visible: sourceComponent + visible: sourceComponent && !root.showLoadingIndicator } StatusTextWithLoadingState { diff --git a/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml b/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml index 8b24875e8c..e95924cabd 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml @@ -59,6 +59,7 @@ Loader { root.asset.bgColor image.fillMode: Image.PreserveAspectCrop } + Loader { anchors.centerIn: parent active: root.asset.imgStatus === Image.Error || diff --git a/ui/app/AppLayouts/Onboarding/views/CreatePasswordView.qml b/ui/app/AppLayouts/Onboarding/views/CreatePasswordView.qml index 3368494e1d..a3c1e85ffd 100644 --- a/ui/app/AppLayouts/Onboarding/views/CreatePasswordView.qml +++ b/ui/app/AppLayouts/Onboarding/views/CreatePasswordView.qml @@ -7,7 +7,6 @@ import StatusQ.Core.Utils 0.1 as StatusQUtils import utils 1.0 import shared.views 1.0 -import "../../Profile/views" import "../controls" import "../stores" @@ -44,6 +43,7 @@ Item { PasswordView { id: view Layout.preferredWidth: root.width - 2 * Style.current.bigPadding + Layout.maximumWidth: 460 Layout.fillHeight: true passwordStrengthScoreFunction: root.startupStore.getPasswordStrengthScore highSizeIntro: true diff --git a/ui/app/AppLayouts/Profile/ProfileLayout.qml b/ui/app/AppLayouts/Profile/ProfileLayout.qml index 9b9488ed23..7d89470595 100644 --- a/ui/app/AppLayouts/Profile/ProfileLayout.qml +++ b/ui/app/AppLayouts/Profile/ProfileLayout.qml @@ -6,7 +6,7 @@ import QtQuick.Window 2.15 import utils 1.0 import shared 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.stores.send 1.0 @@ -38,7 +38,7 @@ StatusSectionLayout { required property TransactionStore transactionStore required property WalletAssetsStore walletAssetsStore required property CollectiblesStore collectiblesStore - required property CurrenciesStore currencyStore + required property SharedStores.CurrenciesStore currencyStore backButtonName: root.store.backButtonName 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 { active: false asynchronous: true diff --git a/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml b/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml deleted file mode 100644 index 1657226625..0000000000 --- a/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml +++ /dev/null @@ -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() } - } -} diff --git a/ui/app/AppLayouts/Profile/popups/ChangePasswordSuccessModal.qml b/ui/app/AppLayouts/Profile/popups/ChangePasswordSuccessModal.qml deleted file mode 100644 index f65753c67c..0000000000 --- a/ui/app/AppLayouts/Profile/popups/ChangePasswordSuccessModal.qml +++ /dev/null @@ -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("Password changed") - 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(); - } - } - } -} diff --git a/ui/app/AppLayouts/Profile/popups/ConfirmAppRestartModal.qml b/ui/app/AppLayouts/Profile/popups/ConfirmAppRestartModal.qml index 972c6cfaaa..065bc67628 100644 --- a/ui/app/AppLayouts/Profile/popups/ConfirmAppRestartModal.qml +++ b/ui/app/AppLayouts/Profile/popups/ConfirmAppRestartModal.qml @@ -38,7 +38,7 @@ ModalPopup { type: StatusBaseButton.Type.Danger text: qsTr("Restart") anchors.bottom: parent.bottom - onClicked: Qt.quit() + onClicked: Utils.restartApplication(); } } } diff --git a/ui/app/AppLayouts/Profile/popups/ConfirmChangePasswordModal.qml b/ui/app/AppLayouts/Profile/popups/ConfirmChangePasswordModal.qml new file mode 100644 index 0000000000..51e31e1d3c --- /dev/null +++ b/ui/app/AppLayouts/Profile/popups/ConfirmChangePasswordModal.qml @@ -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 won’t 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() + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Profile/popups/qmldir b/ui/app/AppLayouts/Profile/popups/qmldir index 05e28c337e..20e7b261a4 100644 --- a/ui/app/AppLayouts/Profile/popups/qmldir +++ b/ui/app/AppLayouts/Profile/popups/qmldir @@ -8,3 +8,4 @@ RemoveKeypairPopup 1.0 RemoveKeypairPopup.qml TokenListPopup 1.0 TokenListPopup.qml WalletKeypairAccountMenu 1.0 WalletKeypairAccountMenu.qml WalletAddressMenu 1.0 WalletAddressMenu.qml +ConfirmChangePasswordModal 1.0 ConfirmChangePasswordModal.qml diff --git a/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml b/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml index 818078f400..a5da305574 100644 --- a/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml +++ b/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml @@ -94,6 +94,9 @@ QtObject { append({subsection: Constants.settingsSubsection.profile, text: qsTr("Profile"), icon: "profile"}) + append({subsection: Constants.settingsSubsection.password, + text: qsTr("Password"), + icon: "profile"}) append({subsection: Constants.settingsSubsection.keycard, text: qsTr("Keycard"), icon: "keycard"}) diff --git a/ui/app/AppLayouts/Profile/views/ChangePasswordView.qml b/ui/app/AppLayouts/Profile/views/ChangePasswordView.qml new file mode 100644 index 0000000000..03d6662e2a --- /dev/null +++ b/ui/app/AppLayouts/Profile/views/ChangePasswordView.qml @@ -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() + } + } + } + } +} diff --git a/ui/app/AppLayouts/Profile/views/MyProfileView.qml b/ui/app/AppLayouts/Profile/views/MyProfileView.qml index c937ef6aae..9b9930e237 100644 --- a/ui/app/AppLayouts/Profile/views/MyProfileView.qml +++ b/ui/app/AppLayouts/Profile/views/MyProfileView.qml @@ -233,19 +233,6 @@ SettingsContentBase { socialLinksModel: root.profileStore.temporarySocialLinksModel } - Component { - id: changePasswordModal - ChangePasswordModal { - privacyStore: root.privacyStore - onPasswordChanged: Global.openPopup(successPopup) - } - } - - Component { - id: successPopup - ChangePasswordSuccessModal {} - } - Component { id: profilePreview ProfileDialog { diff --git a/ui/app/AppLayouts/Profile/views/qmldir b/ui/app/AppLayouts/Profile/views/qmldir index e96a0918aa..6df10a22c7 100644 --- a/ui/app/AppLayouts/Profile/views/qmldir +++ b/ui/app/AppLayouts/Profile/views/qmldir @@ -1,7 +1,8 @@ AboutView 1.0 AboutView.qml -LanguageView 1.0 LanguageView.qml AppearanceView 1.0 AppearanceView.qml -NotificationsView 1.0 NotificationsView.qml -CommunitiesView 1.0 CommunitiesView.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 diff --git a/ui/imports/shared/popups/keycard/states/CreatePassword.qml b/ui/imports/shared/popups/keycard/states/CreatePassword.qml index 827ca4345b..d6ee3fa4e7 100644 --- a/ui/imports/shared/popups/keycard/states/CreatePassword.qml +++ b/ui/imports/shared/popups/keycard/states/CreatePassword.qml @@ -22,11 +22,11 @@ Item { spacing: Style.current.padding PasswordView { + Layout.minimumWidth: 460 Layout.fillWidth: true Layout.fillHeight: true passwordStrengthScoreFunction: RootStore.getPasswordStrengthScore highSizeIntro: true - fixIntroTextWidth: true newPswText: root.sharedKeycardModule.getNewPassword() confirmationPswText: root.sharedKeycardModule.getNewPassword() diff --git a/ui/imports/shared/views/PasswordView.qml b/ui/imports/shared/views/PasswordView.qml index f5a4b01882..92223db568 100644 --- a/ui/imports/shared/views/PasswordView.qml +++ b/ui/imports/shared/views/PasswordView.qml @@ -19,11 +19,13 @@ ColumnLayout { property bool createNewPsw: true property string title: qsTr("Create a password") 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 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 bool highSizeIntro: false - property bool fixIntroTextWidth: false + + property int contentAlignment: Qt.AlignHCenter property var passwordStrengthScoreFunction: function () {} @@ -80,8 +82,6 @@ ColumnLayout { readonly property var validatorRegexp: /^[!-~]{0,64}$/ readonly property string validatorErrMessage: qsTr("Only letters, numbers, underscores and hyphens allowed") - readonly property int defaultInputWidth: 416 - // Password strength categorization / validation function lowerCaseValidator(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 } } + implicitWidth: 460 spacing: Style.current.bigPadding z: root.zFront // View visual content: StatusBaseText { id: title - Layout.alignment: Qt.AlignHCenter + Layout.alignment: root.contentAlignment visible: root.titleVisible text: root.title - font.pixelSize: 22 + font.pixelSize: root.titleSize font.bold: true color: Theme.palette.directColor1 } @@ -159,16 +160,16 @@ ColumnLayout { ColumnLayout { id: introColumn - Layout.preferredWidth: root.fixIntroTextWidth ? d.defaultInputWidth : parent.width - Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.alignment: root.contentAlignment spacing: 4 StatusBaseText { Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter + Layout.alignment: root.contentAlignment text: root.introText - horizontalAlignment: Text.AlignHCenter + horizontalAlignment: root.contentAlignment font.pixelSize: root.highSizeIntro ? Style.current.primaryTextFontSize : Style.current.tertiaryTextFontSize wrapMode: Text.WordWrap color: Theme.palette.baseColor1 @@ -176,10 +177,10 @@ ColumnLayout { StatusBaseText { Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter + Layout.alignment: root.contentAlignment text: root.recoverText - horizontalAlignment: Text.AlignHCenter + horizontalAlignment: root.contentAlignment font.pixelSize: root.highSizeIntro ? Style.current.primaryTextFontSize : Style.current.tertiaryTextFontSize wrapMode: Text.WordWrap color: Theme.palette.dangerColor1 @@ -194,8 +195,8 @@ ColumnLayout { z: root.zFront visible: !root.createNewPsw - Layout.preferredWidth: d.defaultInputWidth - Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.alignment: root.contentAlignment placeholderText: qsTr("Current password") echoMode: showPassword ? TextInput.Normal : TextInput.Password rightPadding: showHideCurrentIcon.width + showHideCurrentIcon.anchors.rightMargin + Style.current.padding / 2 @@ -219,7 +220,8 @@ ColumnLayout { ColumnLayout { spacing: 4 z: root.zFront - Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.alignment: root.contentAlignment StatusPasswordInput { id: newPswInput @@ -227,8 +229,8 @@ ColumnLayout { property bool showPassword - Layout.preferredWidth: d.defaultInputWidth - Layout.alignment: Qt.AlignHCenter + Layout.alignment: root.contentAlignment + Layout.fillWidth: true placeholderText: qsTr("New password") echoMode: showPassword ? TextInput.Normal : TextInput.Password rightPadding: showHideNewIcon.width + showHideNewIcon.anchors.rightMargin + Style.current.padding / 2 @@ -269,6 +271,7 @@ ColumnLayout { StatusPasswordStrengthIndicator { id: strengthInditactor + Layout.fillWidth: true value: Math.min(Constants.minPasswordLength, newPswInput.text.length) from: 0 to: Constants.minPasswordLength @@ -280,46 +283,66 @@ ColumnLayout { } } - StatusBaseText { - id: strengthenTxt - Layout.alignment: Qt.AlignHCenter - wrapMode: Text.WordWrap - text: root.strengthenText - font.pixelSize: 12 - color: Theme.palette.baseColor1 - clip: true - } + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 80 + border.color: Theme.palette.baseColor2 + border.width: 1 + radius: Style.current.radius + implicitHeight: strengthColumn.implicitHeight + implicitWidth: strengthColumn.implicitWidth - RowLayout { - spacing: Style.current.padding - Layout.alignment: Qt.AlignHCenter + ColumnLayout { + id: strengthColumn + anchors.fill: parent + anchors.margins: Style.current.padding + anchors.verticalCenter: parent.verticalCenter + spacing: Style.current.padding - StatusBaseText { - id: lowerCaseTxt - text: "• " + qsTr("Lower case") - font.pixelSize: 12 - color: d.containsLower ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } + StatusBaseText { + id: strengthenTxt + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter + wrapMode: Text.WordWrap + text: root.strengthenText + font.pixelSize: 12 + color: Theme.palette.baseColor1 + clip: true + } - StatusBaseText { - id: upperCaseTxt - text: "• " + qsTr("Upper case") - font.pixelSize: 12 - color: d.containsUpper ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } + RowLayout { + spacing: Style.current.padding + Layout.alignment: Qt.AlignHCenter - StatusBaseText { - id: numbersTxt - text: "• " + qsTr("Numbers") - font.pixelSize: 12 - color: d.containsNumbers ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } + StatusBaseText { + id: lowerCaseTxt + text: "• " + qsTr("Lower case") + font.pixelSize: 12 + color: d.containsLower ? Theme.palette.successColor1 : Theme.palette.baseColor1 + } - StatusBaseText { - id: symbolsTxt - text: "• " + qsTr("Symbols") - font.pixelSize: 12 - color: d.containsSymbols ? 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 + } + } } } @@ -330,8 +353,8 @@ ColumnLayout { property bool showPassword z: root.zFront - Layout.preferredWidth: d.defaultInputWidth - Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.alignment: root.contentAlignment placeholderText: qsTr("Confirm password") echoMode: showPassword ? TextInput.Normal : TextInput.Password rightPadding: showHideConfirmIcon.width + showHideConfirmIcon.anchors.rightMargin + Style.current.padding / 2 @@ -376,7 +399,7 @@ ColumnLayout { StatusBaseText { id: errorTxt - Layout.alignment: Qt.AlignHCenter + Layout.alignment: root.contentAlignment Layout.fillHeight: true font.pixelSize: 12 color: Theme.palette.dangerColor1 diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index c0f1c077b8..92ed33bfd1 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -331,25 +331,26 @@ QtObject { readonly property QtObject settingsSubsection: QtObject { readonly property int profile: 0 - readonly property int contacts: 1 - readonly property int ensUsernames: 2 - readonly property int messaging: 3 - readonly property int wallet: 4 - readonly property int appearance: 5 - readonly property int language: 6 - readonly property int notifications: 7 - readonly property int syncingSettings: 8 - readonly property int browserSettings: 9 - readonly property int advanced: 10 - readonly property int about: 11 - readonly property int communitiesSettings: 12 - readonly property int keycard: 13 - readonly property int about_terms: 14 // a subpage under "About" - readonly property int about_privacy: 15 // a subpage under "About" + readonly property int password: 1 + readonly property int contacts: 2 + readonly property int ensUsernames: 3 + readonly property int messaging: 4 + readonly property int wallet:5 + readonly property int appearance: 6 + readonly property int language: 7 + readonly property int notifications: 8 + readonly property int syncingSettings: 9 + readonly property int browserSettings: 10 + readonly property int advanced: 11 + readonly property int about: 12 + readonly property int communitiesSettings: 13 + readonly property int keycard: 14 + 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 - readonly property int signout: 16 - readonly property int backUpSeed: 17 + readonly property int signout: 17 + readonly property int backUpSeed: 18 } readonly property QtObject walletSettingsSubsection: QtObject { diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index 198953dde2..2944e64790 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -17,6 +17,10 @@ QtObject { readonly property int maxImgSizeBytes: Constants.maxUploadFilesizeMB * 1048576 /* 1 MB in bytes */ readonly property int communityIdLength: 68 + function restartApplication() { + globalUtilsInst.restartApplication() + } + function isDigit(value) { return /^\d$/.test(value); } diff --git a/vendor/DOtherSide/lib/include/DOtherSide/DOtherSide.h b/vendor/DOtherSide/lib/include/DOtherSide/DOtherSide.h index b6d78207ce..14849bb41a 100644 --- a/vendor/DOtherSide/lib/include/DOtherSide/DOtherSide.h +++ b/vendor/DOtherSide/lib/include/DOtherSide/DOtherSide.h @@ -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() 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 /// \note A QGuiApplication should have been already created through dos_qguiapplication_create() DOS_API void DOS_CALL dos_qguiapplication_delete(void); diff --git a/vendor/DOtherSide/lib/src/DOtherSide.cpp b/vendor/DOtherSide/lib/src/DOtherSide.cpp index 31b98b05fc..13f3b808aa 100644 --- a/vendor/DOtherSide/lib/src/DOtherSide.cpp +++ b/vendor/DOtherSide/lib/src/DOtherSide.cpp @@ -371,6 +371,12 @@ void dos_qguiapplication_quit() QMetaObject::invokeMethod(qGuiApp, "quit", Qt::QueuedConnection); } +void dos_qguiapplication_restart() +{ + QProcess::startDetached(QCoreApplication::applicationFilePath()); + dos_qguiapplication_quit(); +} + void dos_qguiapplication_icon(const char *filename) { qGuiApp->setWindowIcon(QIcon(filename)); diff --git a/vendor/nimqml b/vendor/nimqml index 2d733c5ec6..13a8890db4 160000 --- a/vendor/nimqml +++ b/vendor/nimqml @@ -1 +1 @@ -Subproject commit 2d733c5ec6977edac451c4d0017911b59d01a310 +Subproject commit 13a8890db484d3ff40b410c2ff4b3e3bd2e0e880