From dcab50fe0944f65a1fafef3fdb9e7abe66277b6c Mon Sep 17 00:00:00 2001 From: Alexandra Betouni Date: Tue, 8 Mar 2022 00:59:38 +0200 Subject: [PATCH] feat(onboarding): ading create new keys screens Closes #4956 --- src/app/modules/startup/onboarding/item.nim | 7 +- src/app/modules/startup/onboarding/model.nim | 4 + src/app/modules/startup/onboarding/module.nim | 2 +- src/app/modules/startup/onboarding/view.nim | 7 + .../Onboarding/OnboardingLayout.qml | 133 +++++----- .../controls/OnboardingBasePage.qml | 27 ++ .../popups/MnemonicRecoverySuccessModal.qml | 6 +- .../popups/UploadProfilePicModal.qml | 98 +++++++ .../Onboarding/shared/CreatePasswordModal.qml | 10 +- .../Onboarding/stores/OnboardingStore.qml | 61 ++++- .../Onboarding/views/ConfirmPasswordView.qml | 69 +++-- .../Onboarding/views/CreatePasswordView.qml | 27 +- .../Onboarding/views/GenKeyView.qml | 107 ++++++-- .../Onboarding/views/InsertDetailsView.qml | 251 ++++++++++++++++++ .../Onboarding/views/KeysMainView.qml | 31 +-- .../AppLayouts/Onboarding/views/LoginView.qml | 1 - .../Onboarding/views/TouchIDAuthView.qml | 129 +++++++++ .../Onboarding/views/WelcomeView.qml | 11 +- .../Profile/popups/ChangePasswordModal.qml | 2 +- .../popups/StoreToKeychainSelectionModal.qml | 4 +- .../Profile/stores/PrivacyStore.qml | 4 - .../assets/images/onboarding/chains.png | Bin 0 -> 1222 bytes .../assets/images/onboarding/fingerprint.png | Bin 0 -> 17903 bytes .../images/{ => onboarding}/welcome.png | Bin ui/imports/shared/controls/EmojiHash.qml | 24 ++ .../shared/controls/chat/ProfileHeader.qml | 30 +-- ui/imports/shared/controls/qmldir | 1 + ui/imports/shared/panels/ImageLoader.qml | 5 +- ui/imports/shared/stores/RootStore.qml | 12 + .../shared}/views/PasswordView.qml | 5 +- ui/imports/shared/views/qmldir | 1 + ui/main.qml | 52 +--- 32 files changed, 867 insertions(+), 254 deletions(-) create mode 100644 ui/app/AppLayouts/Onboarding/controls/OnboardingBasePage.qml create mode 100644 ui/app/AppLayouts/Onboarding/popups/UploadProfilePicModal.qml create mode 100644 ui/app/AppLayouts/Onboarding/views/InsertDetailsView.qml create mode 100644 ui/app/AppLayouts/Onboarding/views/TouchIDAuthView.qml create mode 100644 ui/imports/assets/images/onboarding/chains.png create mode 100644 ui/imports/assets/images/onboarding/fingerprint.png rename ui/imports/assets/images/{ => onboarding}/welcome.png (100%) create mode 100644 ui/imports/shared/controls/EmojiHash.qml rename ui/{app/AppLayouts/Profile => imports/shared}/views/PasswordView.qml (98%) diff --git a/src/app/modules/startup/onboarding/item.nim b/src/app/modules/startup/onboarding/item.nim index 7030df840a..a359ffa756 100644 --- a/src/app/modules/startup/onboarding/item.nim +++ b/src/app/modules/startup/onboarding/item.nim @@ -4,13 +4,15 @@ type alias: string identicon: string address: string + pubKey: string keyUid: string -proc initItem*(id, alias, identicon, address, keyUid: string): Item = +proc initItem*(id, alias, identicon, address, pubKey, keyUid: string): Item = result.id = id result.alias = alias result.identicon = identicon result.address = address + result.pubKey = pubKey result.keyUid = keyUid proc getId*(self: Item): string = @@ -25,5 +27,8 @@ proc getIdenticon*(self: Item): string = proc getAddress*(self: Item): string = return self.address +proc getPubKey*(self: Item): string = + return self.pubKey + proc getKeyUid*(self: Item): string = return self.keyUid diff --git a/src/app/modules/startup/onboarding/model.nim b/src/app/modules/startup/onboarding/model.nim index 6c8eb0b272..071486f849 100644 --- a/src/app/modules/startup/onboarding/model.nim +++ b/src/app/modules/startup/onboarding/model.nim @@ -8,6 +8,7 @@ type Alias Identicon Address + PubKey KeyUid QtObject: @@ -35,6 +36,7 @@ QtObject: ModelRole.Alias.int:"username", ModelRole.Identicon.int:"identicon", ModelRole.Address.int:"address", + ModelRole.PubKey.int:"pubKey", ModelRole.KeyUid.int:"keyUid" }.toTable @@ -57,6 +59,8 @@ QtObject: result = newQVariant(item.getIdenticon()) of ModelRole.Address: result = newQVariant(item.getAddress()) + of ModelRole.PubKey: + result = newQVariant(item.getPubKey()) of ModelRole.KeyUid: result = newQVariant(item.getKeyUid()) diff --git a/src/app/modules/startup/onboarding/module.nim b/src/app/modules/startup/onboarding/module.nim index 6727a81d6a..6d2dc86f39 100644 --- a/src/app/modules/startup/onboarding/module.nim +++ b/src/app/modules/startup/onboarding/module.nim @@ -41,7 +41,7 @@ method load*(self: Module) = let generatedAccounts = self.controller.getGeneratedAccounts() var accounts: seq[Item] for acc in generatedAccounts: - accounts.add(initItem(acc.id, acc.alias, acc.identicon, acc.address, acc.keyUid)) + accounts.add(initItem(acc.id, acc.alias, acc.identicon, acc.address, acc.publicKey, acc.keyUid)) self.view.setAccountList(accounts) diff --git a/src/app/modules/startup/onboarding/view.nim b/src/app/modules/startup/onboarding/view.nim index c8c6cffe8d..0e70bfa72c 100644 --- a/src/app/modules/startup/onboarding/view.nim +++ b/src/app/modules/startup/onboarding/view.nim @@ -60,6 +60,13 @@ QtObject: read = getImportedAccountAddress notify = importedAccountChanged + proc getImportedAccountPubKey*(self: View): string {.slot.} = + return self.delegate.getImportedAccount().publicKey + + QtProperty[string] importedAccountPubKey: + read = getImportedAccountPubKey + notify = importedAccountChanged + proc setDisplayName*(self: View, displayName: string) {.slot.} = self.delegate.setDisplayName(displayName) diff --git a/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml b/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml index a62d8e19e4..6e0c6d926b 100644 --- a/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml +++ b/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml @@ -8,6 +8,8 @@ import "stores" QtObject { id: root property bool hasAccounts + property string keysMainSetState: "" + signal loadApp() signal onBoardingStepChanged(var view, string state) @@ -18,38 +20,78 @@ QtObject { DSM.State { id: onboardingState - initialState: root.hasAccounts ? stateLogin : keysMainState + initialState: root.hasAccounts ? stateLogin : welcomeMainState + + DSM.State { + id: welcomeMainState + onEntered: { onBoardingStepChanged(welcomeMain, ""); } + + DSM.SignalTransition { + targetState: keysMainState + signal: Global.applicationWindow.navigateTo + guard: path === "KeyMain" + } + } DSM.State { id: keysMainState - onEntered: { onBoardingStepChanged(welcomeMain, ""); } + onEntered: { onBoardingStepChanged(keysMain, root.keysMainSetState); } DSM.SignalTransition { targetState: genKeyState signal: Global.applicationWindow.navigateTo guard: path === "GenKey" } + + DSM.SignalTransition { + targetState: welcomeMainState + signal: Global.applicationWindow.navigateTo + guard: path === "Welcome" + } } DSM.State { id: existingKeyState onEntered: { onBoardingStepChanged(existingKey, ""); } + DSM.SignalTransition { + targetState: genKeyState + signal: Global.applicationWindow.navigateTo + guard: path === "GenKey" + } + DSM.SignalTransition { targetState: appState signal: startupModule.appStateChanged guard: state === Constants.appState.main } + + DSM.SignalTransition { + targetState: welcomeMainState + signal: Global.applicationWindow.navigateTo + guard: path === "Welcome" + } } DSM.State { id: genKeyState onEntered: { onBoardingStepChanged(genKey, ""); } + DSM.SignalTransition { + targetState: welcomeMainState + signal: Global.applicationWindow.navigateTo + guard: path === "Welcome" + } DSM.SignalTransition { targetState: appState - signal: startupModule.appStateChanged - guard: state === Constants.appState.main + signal: Global.applicationWindow.navigateTo + guard: path === "LoggedIn" + } + + DSM.SignalTransition { + targetState: stateLogin + signal: Global.applicationWindow.navigateTo + guard: path === "LogIn" } } @@ -109,36 +151,24 @@ QtObject { guard: path === "InitialState" } - DSM.SignalTransition { - targetState: existingKeyState - signal: Global.applicationWindow.navigateTo - guard: path === "ExistingKey" - } - DSM.SignalTransition { targetState: keysMainState signal: Global.applicationWindow.navigateTo guard: path === "KeysMain" } + DSM.SignalTransition { + targetState: existingKeyState + signal: Global.applicationWindow.navigateTo + guard: path === "ExistingKey" + } + DSM.SignalTransition { targetState: keycardState signal: Global.applicationWindow.navigateTo 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 } @@ -159,10 +189,12 @@ QtObject { id: welcomeMain WelcomeView { onBtnNewUserClicked: { - onBoardingStepChanged(keysMain, "getkeys"); + root.keysMainSetState = "getkeys"; + Global.applicationWindow.navigateTo("KeyMain"); } onBtnExistingUserClicked: { - onBoardingStepChanged(keysMain, "connectkeys"); + root.keysMainSetState = "connectkeys"; + Global.applicationWindow.navigateTo("KeyMain"); } } } @@ -180,7 +212,7 @@ QtObject { Global.applicationWindow.navigateTo("ExistingKey"); } onBackClicked: { - onBoardingStepChanged(welcomeMain, ""); + Global.applicationWindow.navigateTo("Welcome"); } } } @@ -188,7 +220,9 @@ QtObject { property var existingKeyComponent: Component { id: existingKey ExistingKeyView { - onShowCreatePasswordView: { Global.applicationWindow.navigateTo("CreatePassword") } + onShowCreatePasswordView: { + Global.applicationWindow.navigateTo("GenKey"); + } onClosed: function () { if (root.hasAccounts) { Global.applicationWindow.navigateTo("InitialState") @@ -202,14 +236,16 @@ QtObject { property var genKeyComponent: Component { id: genKey GenKeyView { - onShowCreatePasswordView: { Global.applicationWindow.navigateTo("CreatePassword") } - onClosed: function () { - if (root.hasAccounts) { - Global.applicationWindow.navigateTo("InitialState") + onFinished: { + if (LoginStore.currentAccount.username !== "") { + Global.applicationWindow.navigateTo("LogIn"); } else { - Global.applicationWindow.navigateTo("KeysMain") + Global.applicationWindow.navigateTo("KeysMain"); } } + onKeysGenerated: { + Global.applicationWindow.navigateTo("LoggedIn") + } } } @@ -237,39 +273,4 @@ QtObject { } } } - - property var d: QtObject { - property string newPassword - property string confirmationPassword - } - - property var createPasswordComponent: Component { - id: createPassword - CreatePasswordView { - store: OnboardingStore - 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") } - } - } } diff --git a/ui/app/AppLayouts/Onboarding/controls/OnboardingBasePage.qml b/ui/app/AppLayouts/Onboarding/controls/OnboardingBasePage.qml new file mode 100644 index 0000000000..1362123906 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/controls/OnboardingBasePage.qml @@ -0,0 +1,27 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 + +import StatusQ.Controls 0.1 + +import utils 1.0 + +Page { + id: root + signal backClicked() + signal finished() + + background: Rectangle { + color: Style.current.background + } + + StatusRoundButton { + 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(); + } + } +} diff --git a/ui/app/AppLayouts/Onboarding/popups/MnemonicRecoverySuccessModal.qml b/ui/app/AppLayouts/Onboarding/popups/MnemonicRecoverySuccessModal.qml index 474290a4a8..19b7bc55f6 100644 --- a/ui/app/AppLayouts/Onboarding/popups/MnemonicRecoverySuccessModal.qml +++ b/ui/app/AppLayouts/Onboarding/popups/MnemonicRecoverySuccessModal.qml @@ -43,7 +43,7 @@ ModalPopup { anchors.top: info.bottom anchors.topMargin: Style.current.bigPadding anchors.horizontalCenter: parent.horizontalCenter - image.source: OnboardingStore.onBoardingModul.importedAccountIdenticon + image.source: OnboardingStore.onboardingModuleInst.importedAccountIdenticon image.width: 60 image.height: 60 } @@ -53,7 +53,7 @@ ModalPopup { anchors.top: identicon.bottom anchors.topMargin: Style.current.padding anchors.horizontalCenter: identicon.horizontalCenter - text: OnboardingStore.onBoardingModul.importedAccountAlias + text: OnboardingStore.onboardingModuleInst.importedAccountAlias font.weight: Font.Bold font.pixelSize: 15 } @@ -62,7 +62,7 @@ ModalPopup { anchors.top: username.bottom anchors.topMargin: Style.current.halfPadding anchors.horizontalCenter: username.horizontalCenter - text: OnboardingStore.onBoardingModul.importedAccountAddress + text: OnboardingStore.onboardingModuleInst.importedAccountAddress width: 120 } diff --git a/ui/app/AppLayouts/Onboarding/popups/UploadProfilePicModal.qml b/ui/app/AppLayouts/Onboarding/popups/UploadProfilePicModal.qml new file mode 100644 index 0000000000..4cc518560c --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/popups/UploadProfilePicModal.qml @@ -0,0 +1,98 @@ +import QtQuick 2.13 +import QtQuick.Dialogs 1.3 + +import utils 1.0 + +import StatusQ.Controls 0.1 + +import shared 1.0 +import shared.panels 1.0 +import shared.popups 1.0 + +import "../stores" + +// TODO: replace with StatusModal +ModalPopup { + id: popup + title: qsTr("Upload profile picture") + property string selectedImage + property string uploadError + + onSelectedImageChanged: { + if (!selectedImage) { + return; + } + cropImageModal.open(); + } + + Item { + anchors.fill: parent + + RoundedImage { + id: profilePic + source: selectedImage + width: 160 + height: 160 + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + border.width: 1 + border.color: Style.current.border + onClicked: imageDialog.open(); + } + + StyledText { + visible: !!uploadError + text: uploadError + anchors.left: parent.left + anchors.right: parent.right + anchors.top: profilePic.bottom + horizontalAlignment: Text.AlignHCenter + font.pixelSize: 13 + wrapMode: Text.WordWrap + anchors.topMargin: 13 + font.weight: Font.Thin + color: Style.current.danger + } + + ImageCropperModal { + id: cropImageModal + selectedImage: popup.selectedImage + ratio: "1:1" + onCropFinished: { + OnboardingStore.uploadImage(selectedImage, aX, aY, bX, bY); + } + } + } + + footer: Item { + width: parent.width + height: uploadBtn.height + + StatusButton { + id: uploadBtn + text: !!selectedImage ? qsTr("Done") : qsTr("Upload") + anchors.right: parent.right + anchors.bottom: parent.bottom + onClicked: { + if (!!selectedImage) { + close(); + } else { + imageDialog.open(); + } + } + + FileDialog { + id: imageDialog + title: qsTrId("please-choose-an-image") + folder: shortcuts.pictures + nameFilters: [ + qsTrId("image-files----jpg---jpeg---png-") + ] + onAccepted: { + selectedImage = imageDialog.fileUrls[0]; + } + } + } + } +} + diff --git a/ui/app/AppLayouts/Onboarding/shared/CreatePasswordModal.qml b/ui/app/AppLayouts/Onboarding/shared/CreatePasswordModal.qml index 1c9eb34f0e..302e7591bb 100644 --- a/ui/app/AppLayouts/Onboarding/shared/CreatePasswordModal.qml +++ b/ui/app/AppLayouts/Onboarding/shared/CreatePasswordModal.qml @@ -10,6 +10,8 @@ import shared.panels 1.0 import shared.popups 1.0 import shared.controls 1.0 +import "../stores" + // TODO: replace with StatusModal ModalPopup { property var privacyStore @@ -153,7 +155,7 @@ ModalPopup { } Connections { - target: onboardingModule + target: OnboardingStore.onboardingModuleInst onAccountSetupError: { importLoginError.open() } @@ -169,15 +171,15 @@ ModalPopup { passwordValidationError = qsTr("Incorrect password") } else { - Global.applicationWindow.prepareForStoring(repeatPasswordField.text, true) + //Global.applicationWindow.prepareForStoring(repeatPasswordField.text, true) popup.close() } } else { loading = true - onboardingModule.storeSelectedAccountAndLogin(repeatPasswordField.text); - Global.applicationWindow.prepareForStoring(repeatPasswordField.text, false) + OnboardingStore.onboardingModuleInst.storeSelectedAccountAndLogin(repeatPasswordField.text); + //Global.applicationWindow.prepareForStoring(repeatPasswordField.text, false) } } } diff --git a/ui/app/AppLayouts/Onboarding/stores/OnboardingStore.qml b/ui/app/AppLayouts/Onboarding/stores/OnboardingStore.qml index fe966294f6..75eb114d69 100644 --- a/ui/app/AppLayouts/Onboarding/stores/OnboardingStore.qml +++ b/ui/app/AppLayouts/Onboarding/stores/OnboardingStore.qml @@ -1,22 +1,69 @@ pragma Singleton import QtQuick 2.13 +import utils 1.0 QtObject { - property var onBoardingModul: onboardingModule + id: root + property var profileSectionModuleInst: profileSectionModule + property var profileModule: profileSectionModuleInst.profileModule + property var onboardingModuleInst: onboardingModule + property var mainModuleInst: !!mainModule ? mainModule : undefined + property var accountSettings: localAccountSettings + property var privacyModule: profileSectionModuleInst.privacyModule + property string displayName: userProfile !== undefined ? userProfile.displayName : "" + + property url profImgUrl: "" + property real profImgAX: 0.0 + property real profImgAY: 0.0 + property real profImgBX: 0.0 + property real profImgBY: 0.0 + property bool accountCreated: false + + property bool showBeforeGetStartedPopup: true function importMnemonic(mnemonic) { - onBoardingModul.importMnemonic(mnemonic) + onboardingModuleInst.importMnemonic(mnemonic) } function setCurrentAccountAndDisplayName(selectedAccountIdx, displayName) { - onBoardingModul.setDisplayName(displayName) - onBoardingModul.setSelectedAccountByIndex(selectedAccountIdx) + onboardingModuleInst.setDisplayName(displayName) + onboardingModuleInst.setSelectedAccountByIndex(selectedAccountIdx) } - function getPasswordStrengthScore(password) { - let userName = onBoardingModul.importedAccountAlias - return onBoardingModul.getPasswordStrengthScore(password, userName) + function updatedDisplayName(displayName) { + if (displayName !== root.displayName) { + print(displayName, root.displayName) + root.profileModule.setDisplayName(displayName); + } + } + + function saveImage() { + root.profileModule.upload(root.profImgUrl, root.profImgAX, root.profImgAY, root.profImgBX, root.profImgBY); + } + + function uploadImage(source, aX, aY, bX, bY) { + root.profImgUrl = source; + root.profImgAX = aX; + root.profImgAY = aY; + root.profImgBX = bX; + root.profImgBY = bY; + } + + function removeImage() { + return root.profileModule.remove(); + } + + function finishCreatingAccount(pass) { + root.onboardingModuleInst.storeSelectedAccountAndLogin(pass); + } + + function storeToKeyChain(pass) { + mainModule.storePassword(pass); + } + + function changePassword(password, newPassword) { + root.privacyModule.changePassword(password, newPassword) } property ListModel accountsSampleData: ListModel { diff --git a/ui/app/AppLayouts/Onboarding/views/ConfirmPasswordView.qml b/ui/app/AppLayouts/Onboarding/views/ConfirmPasswordView.qml index b1b697e6b2..fa5882155a 100644 --- a/ui/app/AppLayouts/Onboarding/views/ConfirmPasswordView.qml +++ b/ui/app/AppLayouts/Onboarding/views/ConfirmPasswordView.qml @@ -12,18 +12,15 @@ import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import "../stores" +import "../controls" -Page { +OnboardingBasePage { id: root property string password - - signal backClicked() - - anchors.fill: parent - background: null - - Component.onCompleted: confPswInput.forceActiveFocus(Qt.MouseFocusReason) + property string tmpPass + property string displayName + function forcePswInputFocus() { confPswInput.forceActiveFocus(Qt.MouseFocusReason)} Column { id: view @@ -73,9 +70,7 @@ Page { width: parent.width enabled: !submitBtn.loading - placeholderText: submitBtn.loading ? - qsTr("Connecting...") : - qsTr("Confirm you password (again)") + placeholderText: qsTr("Confirm you password (again)") textField.echoMode: showPassword ? TextInput.Normal : TextInput.Password textField.validator: RegExpValidator { regExp: /^[!-~]{0,64}$/ } // That incudes NOT extended ASCII printable characters less space and a maximum of 64 characters allowed keepHeight: true @@ -106,24 +101,34 @@ Page { id: submitBtn anchors.horizontalCenter: parent.horizontalCenter text: qsTr("Finalize Status Password Creation") - enabled: !submitBtn.loading && confPswInput.text === root.password + 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) + // Create account operation blocks the UI so loading = true; will never have any affect until it is done. + // Getting around it with a small pause (timer) in order to get the desired behavior + OnboardingStore.finishCreatingAccount(root.password) } } 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() + //confPswInput.text = "" + if (OnboardingStore.accountCreated) { + if (root.password !== root.tmpPass) { + OnboardingStore.changePassword(root.tmpPass, root.password); + root.tmpPass = root.password; + } else { + submitBtn.loading = false + root.finished(); + } + } else { + root.tmpPass = root.password; + submitBtn.loading = true + OnboardingStore.setCurrentAccountAndDisplayName(0, root.displayName); + pause.start(); + } } } } @@ -138,4 +143,28 @@ Page { icon.name: "arrow-left" onClicked: { root.backClicked() } } + + Connections { + target: startupModule + onAppStateChanged: { + if (state === Constants.appState.main) { + if (!!OnboardingStore.profImgUrl) { + OnboardingStore.saveImage() + OnboardingStore.accountCreated = true; + } + submitBtn.loading = false + root.finished() + } + } + } + + Connections { + target: OnboardingStore.privacyModule + onPasswordChanged: { + if (success) { + submitBtn.loading = false + root.finished(); + } + } + } } diff --git a/ui/app/AppLayouts/Onboarding/views/CreatePasswordView.qml b/ui/app/AppLayouts/Onboarding/views/CreatePasswordView.qml index ed5f6867b5..e664f074a9 100644 --- a/ui/app/AppLayouts/Onboarding/views/CreatePasswordView.qml +++ b/ui/app/AppLayouts/Onboarding/views/CreatePasswordView.qml @@ -1,28 +1,20 @@ 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 shared.views 1.0 import "../../Profile/views" +import "../controls" -Page { +OnboardingBasePage { id: root - property var store property string newPassword property string confirmationPassword - - signal passwordCreated(string newPassword, string confirmationPassword) - signal backClicked() - - anchors.fill: parent - background: null - - Component.onCompleted: { view.forceNewPswInputFocus() } + function forceNewPswInputFocus() { view.forceNewPswInputFocus() } QtObject { id: d @@ -34,21 +26,23 @@ Page { spacing: 4 * Style.current.padding anchors.centerIn: parent z: view.zFront - PasswordView { id: view - store: root.store + onboarding: true 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) } + onClicked: { + root.newPassword = view.newPswText + root.confirmationPassword = view.confirmationPswText + root.finished() + } } } @@ -62,7 +56,6 @@ Page { 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 diff --git a/ui/app/AppLayouts/Onboarding/views/GenKeyView.qml b/ui/app/AppLayouts/Onboarding/views/GenKeyView.qml index d71b749add..17c3c38828 100644 --- a/ui/app/AppLayouts/Onboarding/views/GenKeyView.qml +++ b/ui/app/AppLayouts/Onboarding/views/GenKeyView.qml @@ -1,33 +1,102 @@ import QtQuick 2.13 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 -import "../popups" +import shared.panels 1.0 + +import utils 1.0 + +import "../controls" +import "../panels" import "../stores" -import "../shared" -Item { - property var onClosed: function () {} - signal showCreatePasswordView() - - id: genKeyView +OnboardingBasePage { + id: root anchors.fill: parent + Behavior on opacity { NumberAnimation { duration: 200 }} + state: "username" - Component.onCompleted: { - genKeyModal.open() + signal keysGenerated() + + function gotoKeysStack(stackIndex) { createKeysStack.currentIndex = stackIndex } + + enum KeysStack { + DETAILS, + CREATE_PWD, + CONFRIM_PWD, + TOUCH_ID } - GenKeyModal { - property bool wentNext: false - id: genKeyModal - onNextClick: function (selectedIndex, displayName) { - wentNext = true - OnboardingStore.setCurrentAccountAndDisplayName(selectedIndex, displayName) - showCreatePasswordView() + QtObject { + id: d + + property string newPassword + property string confirmationPassword + } + + StackLayout { + id: createKeysStack + anchors.fill: parent + currentIndex: GenKeyView.KeysStack.DETAILS + + onCurrentIndexChanged: { + // Set focus: + if(currentIndex === GenKeyView.KeysStack.CREATE_PWD) + createPswView.forceNewPswInputFocus() + else if(currentIndex === GenKeyView.KeysStack.CONFRIM_PWD) + confirmPswView.forcePswInputFocus() } - onClosed: function () { - if (!wentNext) { - genKeyView.onClosed() + + InsertDetailsView { + id: userDetailsPanel + onCreatePassword: { gotoKeysStack(GenKeyView.KeysStack.CREATE_PWD) } + } + CreatePasswordView { + id: createPswView + newPassword: d.newPassword + confirmationPassword: d.confirmationPassword + + onFinished: { + d.newPassword = newPassword + d.confirmationPassword = confirmationPassword + gotoKeysStack(GenKeyView.KeysStack.CONFRIM_PWD) } + onBackClicked: { + d.newPassword = "" + d.confirmationPassword = "" + gotoKeysStack(GenKeyView.KeysStack.DETAILS) + } + } + ConfirmPasswordView { + id: confirmPswView + password: d.newPassword + displayName: userDetailsPanel.displayName + onFinished: { + if (Qt.platform.os == "osx") { + gotoKeysStack(GenKeyView.KeysStack.TOUCH_ID); + } else { + root.keysGenerated(); + } + } + onBackClicked: { gotoKeysStack(GenKeyView.KeysStack.CREATE_PWD) } + } + TouchIDAuthView { + userPass: d.newPassword + onBackClicked: { gotoKeysStack(GenKeyView.KeysStack.CONFRIM_PWD) } + onGenKeysDone: { root.keysGenerated() } + } + } + + onBackClicked: { + if (userDetailsPanel.state === "chatkey") { + userDetailsPanel.state = "username"; + } else { + root.finished(); } } } diff --git a/ui/app/AppLayouts/Onboarding/views/InsertDetailsView.qml b/ui/app/AppLayouts/Onboarding/views/InsertDetailsView.qml new file mode 100644 index 0000000000..501eb188e0 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/views/InsertDetailsView.qml @@ -0,0 +1,251 @@ +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import shared.panels 1.0 + +import utils 1.0 +import shared.controls 1.0 +import "../popups" +import "../stores" + +Item { + id: root + + property string pubKey + property string address + property string displayName + signal createPassword() + + state: "username" + + ListView { + id: accountsList + model: OnboardingStore.onboardingModuleInst.accountsModel + delegate: Item { + Component.onCompleted: { + root.pubKey = model.pubKey; + root.address = model.address; + } + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.current.padding + + StyledText { + id: usernameText + text: qsTr("Your profile") + font.weight: Font.Bold + font.pixelSize: 22 + Layout.alignment: Qt.AlignHCenter + } + + StyledText { + id: txtDesc + Layout.preferredWidth: (root.state === "username") ? 338 : 643 + color: Style.current.secondaryText + text: qsTr("Longer and unusual names are better as they are less likely to be used by someone else.") + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter + font.pixelSize: 15 + } + + Item { + implicitWidth: 100 + implicitHeight: 100 + Layout.alignment: Qt.AlignHCenter + StatusSmartIdenticon { + id: userImage + image.width: 80 + image.height: 80 + icon.width: 80 + icon.height: 80 + icon.letterSize: 32 + icon.color: Theme.palette.miscColor5 + icon.charactersLen: 2 + image.isIdenticon: false + image.source: uploadProfilePicPopup.selectedImage + ringSettings { ringSpecModel: Utils.getColorHashAsJson(root.pubKey) } + } + StatusRoundButton { + id: updatePicButton + width: 40 + height: 40 + anchors.top: parent.top + anchors.right: parent.right + type: StatusFlatRoundButton.Type.Secondary + icon.name: "add" + onClicked: { + uploadProfilePicPopup.open(); + } + } + } + + StatusInput { + id: nameInput + implicitWidth: 328 + Layout.alignment: Qt.AlignHCenter + input.placeholderText: qsTr("Display name") + input.edit.font.capitalization: Font.Capitalize + input.rightComponent: RoundedIcon { + width: 14 + height: 14 + iconWidth: 14 + iconHeight: 14 + color: "transparent" + source: Style.svg("close-filled") + onClicked: { + nameInput.input.edit.clear(); + } + } + onTextChanged: { + userImage.name = text; + } + } + + StyledText { + id: chatKeyTxt + color: Style.current.secondaryText + text: "Chatkey:" + root.address + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter + font.pixelSize: 15 + } + + Item { + id: chainsChatKeyImg + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 181 + Layout.preferredHeight: 84 + Image { + anchors.horizontalCenter: parent.horizontalCenter + source: Style.png("onboarding/chains") + } + EmojiHash { + anchors.bottom: parent.bottom + publicKey: root.pubKey + } + StatusSmartIdenticon { + id: userImageCopy + anchors.bottom: parent.bottom + anchors.right: parent.right + icon.width: 44 + icon.height: 44 + icon.color: "transparent" + ringSettings { ringSpecModel: Utils.getColorHashAsJson(root.pubKey) } + } + } + StatusButton { + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.topMargin: 125 + enabled: !!nameInput.text + text: qsTr("Next") + onClicked: { + if (root.state === "username") { + if (OnboardingStore.accountCreated) { + OnboardingStore.updatedDisplayName(nameInput.text); + } + root.displayName = nameInput.text; + root.state = "chatkey"; + } else { + createPassword(); + } + } + } + + UploadProfilePicModal { + id: uploadProfilePicPopup + } + } + + states: [ + State { + name: "username" + PropertyChanges { + target: usernameText + text: qsTr("Your profile") + } + PropertyChanges { + target: txtDesc + text: qsTr("Longer and unusual names are better as they are less likely to be used by someone else.") + } + PropertyChanges { + target: chatKeyTxt + visible: false + } + PropertyChanges { + target: chainsChatKeyImg + visible: false + } + PropertyChanges { + target: userImageCopy + visible: false + } + PropertyChanges { + target: updatePicButton + visible: true + } + PropertyChanges { + target: nameInput + visible: true + } + }, + State { + name: "chatkey" + PropertyChanges { + target: usernameText + text: qsTr("Your emojihash and identicon ring") + } + PropertyChanges { + target: txtDesc + text: qsTr("This set of emojis and coloured ring around your avatar are unique and represent your chat key, so your friends can easily distinguish you from potential impersonators.") + } + PropertyChanges { + target: chatKeyTxt + visible: true + } + PropertyChanges { + target: chainsChatKeyImg + visible: true + } + PropertyChanges { + target: userImageCopy + visible: true + } + PropertyChanges { + target: updatePicButton + visible: false + } + PropertyChanges { + target: nameInput + visible: false + } + } + ] + + transitions: [ + Transition { + from: "*" + to: "*" + SequentialAnimation { + PropertyAction { + target: root + property: "opacity" + value: 0.0 + } + PropertyAction { + target: root + property: "opacity" + value: 1.0 + } + } + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml b/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml index 05d48093eb..a540df50d9 100644 --- a/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml +++ b/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml @@ -10,31 +10,16 @@ import StatusQ.Core.Theme 0.1 import shared 1.0 import shared.panels 1.0 import "../popups" +import "../controls" import utils 1.0 -Page { +OnboardingBasePage { id: root signal buttonClicked() signal keycardLinkClicked() signal seedLinkClicked() - signal backClicked() - - background: Rectangle { - color: Style.current.background - } - - Component.onCompleted: { - if(displayBeforeGetStartedModal) { - displayBeforeGetStartedModal = false - beforeGetStartedModal.open() - } - } - - BeforeGetStartedModal { - id: beforeGetStartedModal - } Item { id: container @@ -141,17 +126,6 @@ Page { } } - StatusRoundButton { - 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(); - } - } - states: [ State { name: "connectkeys" @@ -168,6 +142,7 @@ Page { PropertyChanges { target: button text: qsTr("Scan sync code") + enabled: false } PropertyChanges { diff --git a/ui/app/AppLayouts/Onboarding/views/LoginView.qml b/ui/app/AppLayouts/Onboarding/views/LoginView.qml index 1485ca8f1c..6c25a2da84 100644 --- a/ui/app/AppLayouts/Onboarding/views/LoginView.qml +++ b/ui/app/AppLayouts/Onboarding/views/LoginView.qml @@ -31,7 +31,6 @@ Item { loading = true LoginStore.login(password) - Global.applicationWindow.prepareForStoring(password, false) txtPassword.textField.clear() } diff --git a/ui/app/AppLayouts/Onboarding/views/TouchIDAuthView.qml b/ui/app/AppLayouts/Onboarding/views/TouchIDAuthView.qml new file mode 100644 index 0000000000..e875f8833a --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/views/TouchIDAuthView.qml @@ -0,0 +1,129 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import shared.panels 1.0 + +import utils 1.0 + +import "../controls" +import "../panels" +import "../stores" + +OnboardingBasePage { + id: root + + property string userPass + signal genKeysDone(); + + Item { + id: container + enabled: !dimBackground.active + anchors.centerIn: parent + width: 425 + height: { + let h = 0 + const children = this.children + Object.keys(children).forEach(function (key) { + const child = children[key] + h += child.height + Style.current.padding + }) + return h + } + Image { + id: keysImg + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + fillMode: Image.PreserveAspectFit + source: Style.png("onboarding/fingerprint") + width: 160 + height: 160 + mipmap: true + } + + StyledText { + id: txtTitle + text: qsTr("Biometrics") + anchors.topMargin: Style.current.padding + font.bold: true + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: keysImg.bottom + font.letterSpacing: -0.2 + font.pixelSize: 22 + } + + StyledText { + id: txtDesc + width: 426 + anchors.top: txtTitle.bottom + anchors.topMargin: Style.current.padding + color: Style.current.secondaryText + text: qsTrId("Would you like to use your TouchID to login to Status?") + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + font.pixelSize: 15 + } + ColumnLayout { + anchors.topMargin: 40 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: txtDesc.bottom + spacing: Style.current.bigPadding + StatusButton { + id: button + Layout.alignment: Qt.AlignHCenter + text: qsTr("Yes, use TouchID ") + onClicked: { + OnboardingStore.accountSettings.storeToKeychainValue = Constants.storeToKeychainValueStore; + dimBackground.active = true; + OnboardingStore.storeToKeyChain(userPass); + } + } + StatusBaseText { + id: keycardLink + Layout.alignment: Qt.AlignHCenter + color: Theme.palette.primaryColor1 + text: qsTr("I prefer to use my PIN") + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: { + parent.font.underline = true + } + onExited: { + parent.font.underline = false + } + onClicked: { + OnboardingStore.accountSettings.storeToKeychainValue = Constants.storeToKeychainValueNever; + root.genKeysDone(); + } + } + } + } + } + + Loader { + id: dimBackground + anchors.fill: parent + active: false + sourceComponent: Rectangle { + color: Qt.rgba(0, 0, 0, 0.4) + } + } + + Connections { + enabled: !!OnboardingStore.mainModuleInst + target: OnboardingStore.mainModuleInst + onStoringPasswordSuccess: { + dimBackground.active = false; + root.genKeysDone(); + } + onStoringPasswordError: { + dimBackground.active = false; + } + } +} diff --git a/ui/app/AppLayouts/Onboarding/views/WelcomeView.qml b/ui/app/AppLayouts/Onboarding/views/WelcomeView.qml index 1c72af46ed..f1fb93355b 100644 --- a/ui/app/AppLayouts/Onboarding/views/WelcomeView.qml +++ b/ui/app/AppLayouts/Onboarding/views/WelcomeView.qml @@ -7,6 +7,7 @@ import StatusQ.Controls 0.1 import shared 1.0 import shared.panels 1.0 import "../popups" +import "../stores" import utils 1.0 @@ -21,14 +22,16 @@ Page { } Component.onCompleted: { - if(displayBeforeGetStartedModal) { - displayBeforeGetStartedModal = false - beforeGetStartedModal.open() + if (OnboardingStore.showBeforeGetStartedPopup) { + beforeGetStartedModal.open(); } } BeforeGetStartedModal { id: beforeGetStartedModal + onClosed: { + OnboardingStore.showBeforeGetStartedPopup = false; + } } Item { @@ -52,7 +55,7 @@ Page { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top fillMode: Image.PreserveAspectFit - source: Style.png("welcome") + source: Style.png("onboarding/welcome") width: 256 height: 256 mipmap: true diff --git a/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml b/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml index 97bd24150d..2f5866ef27 100644 --- a/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml +++ b/ui/app/AppLayouts/Profile/popups/ChangePasswordModal.qml @@ -5,6 +5,7 @@ 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 @@ -45,7 +46,6 @@ StatusModal { PasswordView { id: view - store: root.privacyStore anchors.topMargin: Style.current.padding anchors.centerIn: parent titleVisible: false diff --git a/ui/app/AppLayouts/Profile/popups/StoreToKeychainSelectionModal.qml b/ui/app/AppLayouts/Profile/popups/StoreToKeychainSelectionModal.qml index ab255f4942..84ddb7f27e 100644 --- a/ui/app/AppLayouts/Profile/popups/StoreToKeychainSelectionModal.qml +++ b/ui/app/AppLayouts/Profile/popups/StoreToKeychainSelectionModal.qml @@ -58,7 +58,9 @@ ModalPopup { checked: localAccountSettings.storeToKeychainValue === Constants.storeToKeychainValueStore onCheckedChanged: { if (checked && localAccountSettings.storeToKeychainValue !== Constants.storeToKeychainValueStore) { - // TODO: REFACTOR TO NEW PASWORD VIEW + // TODO: REFACTOR TO NEW PASWORD VIEW AND + // DELETE StoreToKeychainSelectionModal.qml + // AND CreatePasswordModal.qml IF NOT NEEDED var storePassPopup = Global.openPopup(storePasswordModal) if(storePassPopup) { diff --git a/ui/app/AppLayouts/Profile/stores/PrivacyStore.qml b/ui/app/AppLayouts/Profile/stores/PrivacyStore.qml index 1c589b2d7c..e3c17320a8 100644 --- a/ui/app/AppLayouts/Profile/stores/PrivacyStore.qml +++ b/ui/app/AppLayouts/Profile/stores/PrivacyStore.qml @@ -28,8 +28,4 @@ QtObject { function validatePassword(password) { return root.privacyModule.validatePassword(password) } - - function getPasswordStrengthScore(password) { - return root.privacyModule.getPasswordStrengthScore(password) - } } diff --git a/ui/imports/assets/images/onboarding/chains.png b/ui/imports/assets/images/onboarding/chains.png new file mode 100644 index 0000000000000000000000000000000000000000..0b00308337be832512dfa27bca73fb7223e77570 GIT binary patch literal 1222 zcmV;%1UdVOP)X-Em%ATO?36`8-$pI=rN>-}6ByfU|6A*{4sw9;R?E$iTg0MHBCs>X^ zzf^6oXFBt2+Nul0*sQd>_D@2_PheoiZ{|NU091VPAncLhPhrjT%>9z+sQaIsHi^EsivJGc%5W6Zgvg1~jm3noG|un-3} z8N@{r3(!`ERw zBCBH3T8JA2P>ZELf|8?h+W|TfNBMxrn5Efjsm>tV5WwSvhDuY8S1Q(o?YyiQQ!U2KUHsHJLHDFHkk~U7v+q>kW_HrmP1sg> z9WUuncTM-W(=*0-u1OEbI7o>#3e~T!!(P_QWOkCs$}`PB+Mm+% zRkuN^>!1pC>ruN9Q6O!z-YUdJ4`M&WH^wn%S+A2>I9%OK-Xq{FQ7m+*TUK_Fe|p?{ z3Kd(iwnLl)#sRRE=2OY$zLYD^vb;ZgF^d%Z8%wpKmZmD%oK*1$h-xQu5A1;L@JY!6 ziTKL){>NS*VXYzUo)%N0`?XM$us4??_U)^rH&u}V&FQk6M2@46+D?-(hFE{G{4<{P zf7m{I+v+6k&n4TF4~Y#F1^B{~I~pQO1L>hN^adp0^rI}0kgBN+uJEq4(MlKc>zCs0ucWEBx~ zL z?AfN>wAM+kXD5ucTY!N%U#-PSBLrf#|oX>L&aW@oIjR> z{ujjBS5wQYJDjJM=A*VHITtD5Z#Ws{# khzba(_~@vew~a6U2DNMb1X&GBTL1t607*qoM6N<$g6&I4i2wiq literal 0 HcmV?d00001 diff --git a/ui/imports/assets/images/onboarding/fingerprint.png b/ui/imports/assets/images/onboarding/fingerprint.png new file mode 100644 index 0000000000000000000000000000000000000000..2d55850a769d51cae04aac39b512ee25ae1ba9ae GIT binary patch literal 17903 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 e`(&SNxBP#wfsSoFHwOy<0000" + + StatusQUtils.Emoji.parse(emojiHashSecondLine, size) + } +} diff --git a/ui/imports/shared/controls/chat/ProfileHeader.qml b/ui/imports/shared/controls/chat/ProfileHeader.qml index f22148e9cd..e1551553de 100644 --- a/ui/imports/shared/controls/chat/ProfileHeader.qml +++ b/ui/imports/shared/controls/chat/ProfileHeader.qml @@ -3,6 +3,7 @@ import QtQuick.Layouts 1.14 import utils 1.0 import shared.panels 1.0 +import shared.controls 1.0 import StatusQ.Components 0.1 import StatusQ.Core.Utils 0.1 as StatusQUtils @@ -88,33 +89,14 @@ Item { color: Style.current.secondaryText } - Text { + EmojiHash { id: emojihash - - readonly property size finalSize: supersampling ? Qt.size(emojiSize.width * 2, emojiSize.height * 2) : emojiSize - property string size: `${finalSize.width}x${finalSize.height}` - Layout.fillWidth: true - renderType: Text.NativeRendering - scale: supersampling ? 0.5 : 1 - - text: { - const emojiHash = Utils.getEmojiHashAsJson(root.pubkey) - var emojiHashFirstLine = "" - var emojiHashSecondLine = "" - for (var i = 0; i < 7; i++) { - emojiHashFirstLine += emojiHash[i] - } - for (var i = 7; i < emojiHash.length; i++) { - emojiHashSecondLine += emojiHash[i] - } - - return StatusQUtils.Emoji.parse(emojiHashFirstLine, size) + "
" + - StatusQUtils.Emoji.parse(emojiHashSecondLine, size) - } - horizontalAlignment: Text.AlignHCenter - font.pointSize: 1 // make sure there is no padding for emojis due to 'style: "vertical-align: top"' + publicKey: root.pubkey + readonly property size finalSize: supersampling ? Qt.size(emojiSize.width * 2, emojiSize.height * 2) : emojiSize + size: `${finalSize.width}x${finalSize.height}` + scale: supersampling ? 0.5 : 1 } } } diff --git a/ui/imports/shared/controls/qmldir b/ui/imports/shared/controls/qmldir index f9c647452d..9db2509f68 100644 --- a/ui/imports/shared/controls/qmldir +++ b/ui/imports/shared/controls/qmldir @@ -23,3 +23,4 @@ StyledTextEdit 1.0 StyledTextEdit.qml StyledTextField 1.0 StyledTextField.qml Timer 1.0 Timer.qml TransactionFormGroup 1.0 TransactionFormGroup.qml +EmojiHash 1.0 EmojiHash.qml diff --git a/ui/imports/shared/panels/ImageLoader.qml b/ui/imports/shared/panels/ImageLoader.qml index e7a8f2ab9a..d275503af3 100644 --- a/ui/imports/shared/panels/ImageLoader.qml +++ b/ui/imports/shared/panels/ImageLoader.qml @@ -1,4 +1,4 @@ -import QtQuick 2.3 +import QtQuick 2.13 import QtGraphicalEffects 1.13 import StatusQ.Components 0.1 @@ -53,7 +53,8 @@ Rectangle { ] Connections { - target: mainModule + enabled: !!mainModule + target: enabled ? mainModule : undefined onOnlineStatusChanged: { if (connected && root.state !== "ready" && root.visible && diff --git a/ui/imports/shared/stores/RootStore.qml b/ui/imports/shared/stores/RootStore.qml index 8da246f9a2..617afc1162 100644 --- a/ui/imports/shared/stores/RootStore.qml +++ b/ui/imports/shared/stores/RootStore.qml @@ -10,6 +10,9 @@ QtObject { // property var keycardModelInst: !!keycardModel ? keycardModel : null // property var profileModelInst: !!profileModel ? profileModel : null + property var profileSectionModuleInst: profileSectionModule + property var privacyModule: profileSectionModuleInst.privacyModule + property var onboardingModuleInst: onboardingModule property var userProfileInst: !!userProfile ? userProfile : null property var walletSectionInst: !!walletSection ? walletSection : null property var appSettings: !!localAppSettings ? localAppSettings : null @@ -97,4 +100,13 @@ QtObject { function addToRecentsGif(id) { chatSectionChatContentInputArea.addToRecentsGif(id) } + + function getPasswordStrengthScore(password, onboarding = false) { + if (onboarding) { + let userName = root.onboardingModuleInst.importedAccountAlias; + return root.onboardingModuleInst.getPasswordStrengthScore(password, userName); + } else { + return root.privacyModule.getPasswordStrengthScore(password); + } + } } diff --git a/ui/app/AppLayouts/Profile/views/PasswordView.qml b/ui/imports/shared/views/PasswordView.qml similarity index 98% rename from ui/app/AppLayouts/Profile/views/PasswordView.qml rename to ui/imports/shared/views/PasswordView.qml index 78608fdede..4dd2f29e1c 100644 --- a/ui/app/AppLayouts/Profile/views/PasswordView.qml +++ b/ui/imports/shared/views/PasswordView.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts 1.12 import shared.panels 1.0 import shared.controls 1.0 +import shared.stores 1.0 import utils 1.0 import StatusQ.Controls 0.1 @@ -13,7 +14,6 @@ import StatusQ.Components 0.1 Column { id: root - property var store property bool ready: newPswInput.text.length >= root.minPswLen && newPswInput.text === confirmPswInput.text && errorTxt.text === "" property int minPswLen: 6 property bool createNewPsw: true @@ -22,6 +22,7 @@ Column { 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:") + property bool onboarding: false readonly property int zBehind: 1 readonly property int zFront: 100 @@ -208,7 +209,7 @@ Column { d.containsSymbols = d.symbolsValidator(text) // Update strength indicator: - strengthInditactor.strength = d.convertStrength(root.store.getPasswordStrengthScore(newPswInput.text)) + strengthInditactor.strength = d.convertStrength(RootStore.getPasswordStrengthScore(newPswInput.text, root.onboarding)) } StatusFlatRoundButton { diff --git a/ui/imports/shared/views/qmldir b/ui/imports/shared/views/qmldir index 00701829ec..99e17b062d 100644 --- a/ui/imports/shared/views/qmldir +++ b/ui/imports/shared/views/qmldir @@ -5,3 +5,4 @@ SearchResults 1.0 SearchResults.qml TransactionPreview 1.0 TransactionPreview.qml TransactionSigner 1.0 TransactionSigner.qml TransactionStackView 1.0 TransactionStackView.qml +PasswordView 1.0 PasswordView.qml diff --git a/ui/main.qml b/ui/main.qml index 8fe2209a14..dae2d3609a 100644 --- a/ui/main.qml +++ b/ui/main.qml @@ -20,7 +20,6 @@ import AppLayouts.Onboarding 1.0 StatusWindow { property bool hasAccounts: startupModule.appState !== Constants.appState.onboarding - property bool displayBeforeGetStartedModal: !hasAccounts property bool appIsReady: false Universal.theme: Universal.System @@ -109,9 +108,6 @@ StatusWindow { // We set main module to the Global singleton once user is logged in and we move to the main app. Global.mainModuleInst = mainModule - mainModule.openStoreToKeychainPopup.connect(function(){ - storeToKeychainConfirmationPopup.open() - }) if(localAccountSensitiveSettings.recentEmojis === "") { localAccountSensitiveSettings.recentEmojis = []; } @@ -253,50 +249,6 @@ StatusWindow { } } - function prepareForStoring(password, runStoreToKeychainPopup) { - if(Qt.platform.os == "osx") - { - storeToKeychainConfirmationPopup.password = password - - if(runStoreToKeychainPopup) - storeToKeychainConfirmationPopup.open() - } - } - - ConfirmationDialog { - id: storeToKeychainConfirmationPopup - property string password: "" - height: 200 - confirmationText: qsTr("Would you like to store password to the Keychain?") - showRejectButton: true - showCancelButton: true - confirmButtonLabel: qsTr("Store") - rejectButtonLabel: qsTr("Not now") - cancelButtonLabel: qsTr("Never") - - function finish() - { - password = "" - storeToKeychainConfirmationPopup.close() - } - - onConfirmButtonClicked: { - localAccountSettings.storeToKeychainValue = Constants.storeToKeychainValueStore - mainModule.storePassword(password) - finish() - } - - onRejectButtonClicked: { - localAccountSettings.storeToKeychainValue = Constants.storeToKeychainValueNotNow - finish() - } - - onCancelButtonClicked: { - localAccountSettings.storeToKeychainValue = Constants.storeToKeychainValueNever - finish() - } - } - Loader { id: loader anchors.fill: parent @@ -321,7 +273,9 @@ StatusWindow { onOnBoardingStepChanged: { loader.sourceComponent = view; - loader.item.state = state; + if (!!state) { + loader.item.state = state; + } } }