From 2852d70731ef1f82ca8704f0500d8ddea9809453 Mon Sep 17 00:00:00 2001 From: Khushboo Mehta Date: Mon, 28 Mar 2022 10:19:57 +0200 Subject: [PATCH] feat(@desktop/Wallet): Add acccounts Modal as per new design fixes #5073 --- ui/StatusQ | 2 +- .../AppLayouts/Wallet/panels/WalletFooter.qml | 2 +- .../Wallet/popups/AddAccountModal.qml | 224 +++++++++++ .../popups/AddAccountWithPrivateKeyModal.qml | 211 ----------- .../Wallet/popups/AddAccountWithSeedModal.qml | 200 ---------- .../Wallet/popups/AddNewAccountMenu.qml | 60 --- .../popups/AddWatchOnlyAccountModal.qml | 150 -------- .../Wallet/popups/GenerateAccountModal.qml | 173 --------- ui/app/AppLayouts/Wallet/stores/RootStore.qml | 1 + .../Wallet/views/AdvancedAddAccountView.qml | 354 ++++++++++++++++++ .../AppLayouts/Wallet/views/LeftTabView.qml | 75 +--- 11 files changed, 598 insertions(+), 854 deletions(-) create mode 100644 ui/app/AppLayouts/Wallet/popups/AddAccountModal.qml delete mode 100644 ui/app/AppLayouts/Wallet/popups/AddAccountWithPrivateKeyModal.qml delete mode 100644 ui/app/AppLayouts/Wallet/popups/AddAccountWithSeedModal.qml delete mode 100644 ui/app/AppLayouts/Wallet/popups/AddNewAccountMenu.qml delete mode 100644 ui/app/AppLayouts/Wallet/popups/AddWatchOnlyAccountModal.qml delete mode 100644 ui/app/AppLayouts/Wallet/popups/GenerateAccountModal.qml create mode 100644 ui/app/AppLayouts/Wallet/views/AdvancedAddAccountView.qml diff --git a/ui/StatusQ b/ui/StatusQ index b2ac2794bb..46dfd594df 160000 --- a/ui/StatusQ +++ b/ui/StatusQ @@ -1 +1 @@ -Subproject commit b2ac2794bbee350539e20d95f1e3adf626e8feb7 +Subproject commit 46dfd594dfa889448fbeefc9a011bea38042f41e diff --git a/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml b/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml index 644c604c78..88bb971ac4 100644 --- a/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml +++ b/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml @@ -25,7 +25,7 @@ Rectangle { RowLayout { anchors.centerIn: parent - height: sendBtn.height + height: parent.height spacing: Style.current.padding StatusFlatButton { diff --git a/ui/app/AppLayouts/Wallet/popups/AddAccountModal.qml b/ui/app/AppLayouts/Wallet/popups/AddAccountModal.qml new file mode 100644 index 0000000000..1df2b63c06 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/popups/AddAccountModal.qml @@ -0,0 +1,224 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Dialogs 1.3 +import QtQuick.Layouts 1.14 + +import utils 1.0 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as StatusQUtils +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 +import StatusQ.Popups 0.1 +import StatusQ.Components 0.1 + +import shared.controls 1.0 + +import "../stores" +import "../views" + +StatusModal { + id: popup + + property int marginBetweenInputs: 38 + property string passwordValidationError: "" + property bool loading: false + property var emojiPopup: null + + signal afterAddAccount() + + //% "Generate an account" + header.title: qsTrId("generate-a-new-account") + + function validate() { + if (passwordInput.text === "") { + //% "You need to enter a password" + passwordValidationError = qsTrId("you-need-to-enter-a-password") + } else if (passwordInput.text.length < 6) { + //% "Password needs to be 6 characters or more" + passwordValidationError = qsTrId("password-needs-to-be-6-characters-or-more") + } else { + passwordValidationError = "" + } + return passwordValidationError === "" && accountNameInput.valid + } + + onOpened: { + passwordValidationError = ""; + passwordInput.text = ""; + accountNameInput.text = ""; + accountNameInput.reset() + accountNameInput.input.icon.emoji = StatusQUtils.Emoji.getRandomEmoji() + colorSelectionGrid.selectedColorIndex = Math.floor(Math.random() * colorSelectionGrid.model.length) + advancedSelection.expanded = false + advancedSelection.reset() + passwordInput.forceActiveFocus(Qt.MouseFocusReason) + } + + Connections { + enabled: popup.opened + target: emojiPopup + onEmojiSelected: function (emojiText, atCursor) { + accountNameInput.input.icon.emoji = emojiText + } + } + + contentItem: ScrollView { + width: popup.width + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + topPadding: Style.current.halfPadding + bottomPadding: Style.current.halfPadding + height: 400 + clip: true + + Column { + property alias accountNameInput: accountNameInput + width: popup.width + spacing: Style.current.halfPadding + topPadding: 20 + + // To-Do Password hidden option not supported in StatusQ StatusBaseInput + Item { + width: parent.width + height: passwordInput.height + Input { + id: passwordInput + anchors.fill: parent + anchors.leftMargin: Style.current.padding + anchors.rightMargin: Style.current.padding + + //% "Enter your password…" + placeholderText: qsTrId("enter-your-password…") + //% "Password" + label: qsTrId("password") + textField.echoMode: TextInput.Password + validationError: popup.passwordValidationError + inputLabel.font.pixelSize: 15 + inputLabel.font.weight: Font.Normal + } + } + + StatusInput { + id: accountNameInput + //% "Enter an account name..." + input.placeholderText: qsTrId("enter-an-account-name...") + //% "Account name" + label: qsTrId("account-name") + input.isIconSelectable: true + input.icon.color: colorSelectionGrid.selectedColor ? colorSelectionGrid.selectedColor : Theme.palette.directColor1 + onIconClicked: { + popup.emojiPopup.open() + popup.emojiPopup.x = popup.x + accountNameInput.x + Style.current.padding + popup.emojiPopup.y = popup.y + contentItem.y + accountNameInput.y + accountNameInput.height + Style.current.halfPadding + } + validators: [ + StatusMinLengthValidator { + //% "You need to enter an account name" + errorMessage: qsTrId("you-need-to-enter-an-account-name") + minLength: 1 + } + ] + } + + StatusColorSelectorGrid { + id: colorSelectionGrid + anchors.horizontalCenter: parent.horizontalCenter + //% "color" + titleText: qsTr("color").toUpperCase() + } + + StatusExpandableItem { + id: advancedSelection + + property bool isValid: true + + function validate() { + if(expandableItem) { + return expandableItem.validate() + } + } + + function reset() { + if(expandableItem) { + return expandableItem.reset() + } + } + + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + + //% "Advanced" + primaryText: qsTr("Advanced") + type: StatusExpandableItem.Type.Tertiary + expandable: true + expandableComponent: AdvancedAddAccountView { + width: parent.width + Layout.margins: Style.current.padding + Component.onCompleted: advancedSelection.isValid = Qt.binding(function(){return isValid}) + } + } + } + } + + rightButtons: [ + StatusButton { + id: nextButton + text: loading ? + //% "Loading..." + qsTrId("loading") : + //% "Add account" + qsTrId("add-account") + + enabled: !loading && passwordInput.text !== "" && accountNameInput.text !== "" && advancedSelection.isValid + + MessageDialog { + id: accountError + title: "Adding the account failed" + icon: StandardIcon.Critical + standardButtons: StandardButton.Ok + } + + onClicked : { + // TODO the loaidng doesn't work because the function freezes th eview. Might need to use threads + loading = true + if (!validate() || !advancedSelection.validate()) { + Global.playErrorSound(); + return loading = false + } + + var errMessage = "" + if(advancedSelection.expandableItem) { + switch(advancedSelection.expandableItem.addAccountType) { + case AdvancedAddAccountView.AddAccountType.GenerateNew: + errMessage = RootStore.generateNewAccount(passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.icon.emoji) + break + case AdvancedAddAccountView.AddAccountType.ImportSeedPhrase: + errMessage = RootStore.addAccountsFromSeed(advancedSelection.expandableItem.mnemonicText, passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.icon.emoji) + break + case AdvancedAddAccountView.AddAccountType.ImportPrivateKey: + errMessage = RootStore.addAccountsFromPrivateKey(advancedSelection.expandableItem.privateKey, passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.icon.emoji) + break + } + } else { + errMessage = RootStore.generateNewAccount(passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.icon.emoji) + } + + loading = false + if (errMessage) { + Global.playErrorSound(); + if (Utils.isInvalidPasswordMessage(errMessage)) { + //% "Wrong password" + popup.passwordValidationError = qsTrId("wrong-password") + } else { + accountError.text = errMessage; + accountError.open(); + } + return + } + popup.afterAddAccount(); + popup.close(); + } + } + ] +} diff --git a/ui/app/AppLayouts/Wallet/popups/AddAccountWithPrivateKeyModal.qml b/ui/app/AppLayouts/Wallet/popups/AddAccountWithPrivateKeyModal.qml deleted file mode 100644 index dea002d245..0000000000 --- a/ui/app/AppLayouts/Wallet/popups/AddAccountWithPrivateKeyModal.qml +++ /dev/null @@ -1,211 +0,0 @@ -import QtQuick 2.13 -import QtQuick.Controls 2.13 -import QtQuick.Dialogs 1.3 - -import utils 1.0 - -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Popups 0.1 -import StatusQ.Controls.Validators 0.1 -import StatusQ.Core.Utils 0.1 as StatusQUtils - -import shared.panels 1.0 -import shared.controls 1.0 - -import "../stores" - -StatusModal { - id: popup - - property int marginBetweenInputs: 38 - property string passwordValidationError: "" - property string privateKeyValidationError: "" - property bool loading: false - property var emojiPopup: null - - signal afterAddAccount() - - function validate() { - if (passwordInput.text === "") { - //% "You need to enter a password" - passwordValidationError = qsTrId("you-need-to-enter-a-password") - } else if (passwordInput.text.length < 6) { - //% "Password needs to be 6 characters or more" - passwordValidationError = qsTrId("password-needs-to-be-6-characters-or-more") - } else { - passwordValidationError = "" - } - - if (accountPKeyInput.text === "") { - //% "You need to enter a private key" - privateKeyValidationError = qsTrId("you-need-to-enter-a-private-key") - } else if (!Utils.isPrivateKey(accountPKeyInput.text)) { - //% "Enter a valid private key (64 characters hexadecimal string)" - privateKeyValidationError = qsTrId("enter-a-valid-private-key-(64-characters-hexadecimal-string)") - } else { - privateKeyValidationError = "" - } - - return passwordValidationError === "" && privateKeyValidationError === "" && accountNameInput.valid - } - - //% "Add account from private key" - header.title: qsTrId("add-private-key-account") - - onOpened: { - passwordInput.text = "" - accountPKeyInput.text = "" - accountNameInput.reset() - accountNameInput.text = "" - accountNameInput.input.icon.emoji = StatusQUtils.Emoji.getRandomEmoji() - passwordValidationError = "" - privateKeyValidationError = "" - accountColorInput.selectedColorIndex = Math.floor(Math.random() * accountColorInput.model.length) - passwordInput.forceActiveFocus(Qt.MouseFocusReason) - } - - Connections { - enabled: popup.opened - target: emojiPopup - onEmojiSelected: function (emojiText, atCursor) { - popup.contentItem.accountNameInput.input.icon.emoji = emojiText - } - } - - contentItem: Column { - property alias accountNameInput: accountNameInput - - width: popup.width - spacing: 8 - topPadding: 20 - - Column { - width: parent.width - spacing: Style.current.xlPadding - // To-Do Password hidden option not supported in StatusQ StatusBaseInput - Input { - id: passwordInput - anchors.leftMargin: Style.current.padding - anchors.rightMargin: Style.current.padding - width: parent.width - - //% "Enter your password…" - placeholderText: qsTrId("enter-your-password…") - //% "Password" - label: qsTrId("password") - textField.echoMode: TextInput.Password - validationError: popup.passwordValidationError - inputLabel.font.pixelSize: 15 - inputLabel.font.weight: Font.Normal - } - // To-Do use StatusInput - StyledTextArea { - id: accountPKeyInput - customHeight: 88 - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Style.current.padding - anchors.rightMargin: Style.current.padding - - validationError: popup.privateKeyValidationError - //% "Private key" - label: qsTrId("private-key") - textField.wrapMode: Text.WordWrap - textField.horizontalAlignment: TextEdit.AlignHCenter - textField.verticalAlignment: TextEdit.AlignVCenter - textField.font.weight: Font.DemiBold - //% "Paste the contents of your private key" - placeholderText: qsTrId("paste-the-contents-of-your-private-key") - textField.placeholderTextColor: Style.current.secondaryText - textField.selectByKeyboard: true - textField.selectionColor: Style.current.secondaryBackground - textField.selectedTextColor: Style.current.secondaryText - } - } - - StatusInput { - id: accountNameInput - //% "Enter an account name..." - input.placeholderText: qsTrId("enter-an-account-name...") - //% "Account name" - label: qsTrId("account-name") - input.isIconSelectable: true - input.icon.color: accountColorInput.selectedColor ? accountColorInput.selectedColor : Theme.palette.directColor1 - onIconClicked: { - popup.emojiPopup.open() - popup.emojiPopup.x = popup.x + Style.current.padding - popup.emojiPopup.y = popup.y + contentItem.y + accountNameInput.y + accountNameInput.height + Style.current.halfPadding - } - validators: [ - StatusMinLengthValidator { - //% "You need to enter an account name" - errorMessage: qsTrId("you-need-to-enter-an-account-name") - minLength: 1 - }, - StatusRegularExpressionValidator { - regularExpression: /^[^<>]+$/ - errorMessage: qsTr("This is not a valid account name") - } - ] - charLimit: 40 - } - - StatusColorSelectorGrid { - id: accountColorInput - anchors.horizontalCenter: parent.horizontalCenter - //% "color" - titleText: qsTr("color").toUpperCase() - } - - Item { - width: parent.width - height: 8 - } - } - - rightButtons: [ - StatusButton { - text: loading ? - //% "Loading..." - qsTrId("loading") : - //% "Add account" - qsTrId("add-account") - - enabled: !loading && passwordInput.text !== "" && accountNameInput.text !== "" && accountNameInput.valid && accountPKeyInput.text !== "" - - MessageDialog { - id: accountError - title: "Adding the account failed" - icon: StandardIcon.Critical - standardButtons: StandardButton.Ok - } - - onClicked : { - // TODO the loaidng doesn't work because the function freezes th eview. Might need to use threads - loading = true - if (!validate()) { - return loading = false - } - - const errMessage = RootStore.addAccountsFromPrivateKey(accountPKeyInput.text, passwordInput.text, accountNameInput.text, accountColorInput.selectedColor, accountNameInput.input.icon.emoji) - - loading = false - if (errMessage) { - Global.playErrorSound(); - if (Utils.isInvalidPasswordMessage(errMessage)) { - //% "Wrong password" - popup.passwordValidationError = qsTrId("wrong-password") - } else { - accountError.text = errMessage - accountError.open() - } - return - } - popup.afterAddAccount() - popup.close(); - } - } - ] -} diff --git a/ui/app/AppLayouts/Wallet/popups/AddAccountWithSeedModal.qml b/ui/app/AppLayouts/Wallet/popups/AddAccountWithSeedModal.qml deleted file mode 100644 index 196f83ca2e..0000000000 --- a/ui/app/AppLayouts/Wallet/popups/AddAccountWithSeedModal.qml +++ /dev/null @@ -1,200 +0,0 @@ -import QtQuick 2.13 -import QtQuick.Controls 2.13 -import QtQuick.Dialogs 1.3 - -import utils 1.0 - -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Popups 0.1 -import StatusQ.Controls.Validators 0.1 -import StatusQ.Core.Utils 0.1 as StatusQUtils - -import shared.controls 1.0 - -import "../stores" - -StatusModal { - id: popup - - property string passwordValidationError: "" - property string seedValidationError: "" - property bool loading: false - property var emojiPopup: null - - signal afterAddAccount() - - function reset() { - passwordInput.text = "" - accountNameInput.text = "" - seedPhraseTextArea.textArea.text = "" - } - - function validate() { - if (passwordInput.text === "") { - //% "You need to enter a password" - passwordValidationError = qsTrId("you-need-to-enter-a-password") - } else if (passwordInput.text.length < 6) { - //% "Password needs to be 6 characters or more" - passwordValidationError = qsTrId("password-needs-to-be-6-characters-or-more") - } else { - passwordValidationError = "" - } - - if (seedPhraseTextArea.textArea.text === "") { - //% "You need to enter a seed phrase" - seedValidationError = qsTrId("you-need-to-enter-a-seed-phrase") - } else if (!Utils.isMnemonic(seedPhraseTextArea.textArea.text)) { - //% "Enter a valid mnemonic" - seedValidationError = qsTrId("enter-a-valid-mnemonic") - } else { - seedValidationError = "" - } - - return passwordValidationError === "" && seedValidationError === "" && accountNameInput.valid - } - - //% "Add account with a seed phrase" - header.title: qsTrId("add-seed-account") - - onOpened: { - seedPhraseTextArea.textArea.text = "" - passwordInput.text = "" - accountNameInput.text = "" - accountNameInput.reset() - accountNameInput.input.icon.emoji = StatusQUtils.Emoji.getRandomEmoji() - passwordValidationError = "" - seedValidationError = "" - accountColorInput.selectedColorIndex = Math.floor(Math.random() * accountColorInput.model.length) - passwordInput.forceActiveFocus(Qt.MouseFocusReason) - } - - Connections { - enabled: popup.opened - target: emojiPopup - onEmojiSelected: function (emojiText, atCursor) { - popup.contentItem.accountNameInput.input.icon.emoji = emojiText - } - } - - contentItem: Column { - property alias accountNameInput: accountNameInput - - width: popup.width - spacing: 8 - topPadding: 20 - - Column { - width: parent.width - spacing: Style.current.xlPadding - // To-Do Password hidden option not supported in StatusQ StatusBaseInput - Input { - id: passwordInput - anchors.leftMargin: Style.current.padding - anchors.rightMargin: Style.current.padding - width: parent.width - - //% "Enter your password…" - placeholderText: qsTrId("enter-your-password…") - //% "Password" - label: qsTrId("password") - textField.echoMode: TextInput.Password - validationError: popup.passwordValidationError - inputLabel.font.pixelSize: 15 - inputLabel.font.weight: Font.Normal - } - // To-Do use StatusInput - SeedPhraseTextArea { - id: seedPhraseTextArea - anchors.left: parent.left - anchors.leftMargin: Style.current.padding - width: parent.width - 2*Style.current.padding - } - } - - StatusInput { - id: accountNameInput - //% "Enter an account name..." - input.placeholderText: qsTrId("enter-an-account-name...") - //% "Account name" - label: qsTrId("account-name") - input.isIconSelectable: true - input.icon.color: accountColorInput.selectedColor ? accountColorInput.selectedColor : Theme.palette.directColor1 - onIconClicked: { - popup.emojiPopup.open() - popup.emojiPopup.x = popup.x + Style.current.padding - popup.emojiPopup.y = popup.y + contentItem.y + accountNameInput.y + accountNameInput.height + Style.current.halfPadding - } - validators: [ - StatusMinLengthValidator { - //% "You need to enter an account name" - errorMessage: qsTrId("you-need-to-enter-an-account-name") - minLength: 1 - }, - StatusRegularExpressionValidator { - regularExpression: /^[^<>]+$/ - errorMessage: qsTr("This is not a valid account name") - } - ] - charLimit: 40 - } - - StatusColorSelectorGrid { - id: accountColorInput - anchors.horizontalCenter: parent.horizontalCenter - //% "color" - titleText: qsTr("color").toUpperCase() - } - - Item { - width: parent.width - height: 8 - } - } - - rightButtons: [ - StatusButton { - text: loading ? - //% "Loading..." - qsTrId("loading") : - //% "Add account" - qsTrId("add-account") - - enabled: !loading && passwordInput.text !== "" && accountNameInput.text !== "" && accountNameInput.valid && seedPhraseTextArea.correctWordCount - - MessageDialog { - id: accountError - title: "Adding the account failed" - icon: StandardIcon.Critical - standardButtons: StandardButton.Ok - } - - onClicked : { - // TODO the loading doesn't work because the function freezes the view. Might need to use threads - loading = true - if (!validate() || !seedPhraseTextArea.validateSeed()) { - Global.playErrorSound(); - return loading = false - } - - const errMessage = RootStore.addAccountsFromSeed(seedPhraseTextArea.textArea.text, passwordInput.text, accountNameInput.text, accountColorInput.selectedColor, accountNameInput.input.icon.emoji) - loading = false - if (errMessage) { - Global.playErrorSound(); - if (Utils.isInvalidPasswordMessage(errMessage)) { - //% "Wrong password" - popup.passwordValidationError = qsTrId("wrong-password") - } else { - accountError.text = errMessage - accountError.open() - } - return - } - popup.afterAddAccount() - popup.reset() - popup.close(); - } - } - ] -} diff --git a/ui/app/AppLayouts/Wallet/popups/AddNewAccountMenu.qml b/ui/app/AppLayouts/Wallet/popups/AddNewAccountMenu.qml deleted file mode 100644 index fa898d2029..0000000000 --- a/ui/app/AppLayouts/Wallet/popups/AddNewAccountMenu.qml +++ /dev/null @@ -1,60 +0,0 @@ -import QtQuick 2.13 -import QtQuick.Controls 2.13 - -import shared 1.0 -import shared.popups 1.0 - -import utils 1.0 - -// TODO: replace with StatusPopupMenu -PopupMenu { - id: newAccountMenu - width: 260 - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent - - signal generateNewAccountTriggered(); - signal addWatchAccountTriggered(); - signal enterSeedPhraseTriggered(); - signal enterPrivateKeyTriggered(); - - Action { - //% "Generate an account" - text: qsTrId("generate-a-new-account") - icon.source: Style.svg("generate_account") - icon.width: 19 - icon.height: 19 - onTriggered: { - newAccountMenu.generateNewAccountTriggered(); - } - } - Action { - //% "Add a watch-only address" - text: qsTrId("add-a-watch-account") - icon.source: Style.svg("eye") - icon.width: 19 - icon.height: 19 - onTriggered: { - newAccountMenu.addWatchAccountTriggered(); - } - } - Action { - //% "Enter a seed phrase" - text: qsTrId("enter-a-seed-phrase") - icon.source: Style.svg("enter_seed_phrase") - icon.width: 19 - icon.height: 19 - onTriggered: { - newAccountMenu.enterSeedPhraseTriggered(); - } - } - Action { - //% "Enter a private key" - text: qsTrId("enter-a-private-key") - icon.source: Style.svg("enter_private_key") - icon.width: 19 - icon.height: 19 - onTriggered: { - newAccountMenu.enterPrivateKeyTriggered(); - } - } -} diff --git a/ui/app/AppLayouts/Wallet/popups/AddWatchOnlyAccountModal.qml b/ui/app/AppLayouts/Wallet/popups/AddWatchOnlyAccountModal.qml deleted file mode 100644 index d156da6397..0000000000 --- a/ui/app/AppLayouts/Wallet/popups/AddWatchOnlyAccountModal.qml +++ /dev/null @@ -1,150 +0,0 @@ -import QtQuick 2.13 -import QtQuick.Controls 2.13 -import QtQuick.Dialogs 1.3 - -import utils 1.0 - -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Popups 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Controls.Validators 0.1 -import StatusQ.Core.Utils 0.1 as StatusQUtils - -import shared.controls 1.0 - -import "../stores" - -StatusModal { - id: popup - - property bool loading: false - property var emojiPopup: null - - signal afterAddAccount() - - //% "Add a watch-only account" - header.title: qsTrId("add-watch-account") - - onOpened: { - addressInput.text = "" - addressInput.reset() - accountNameInput.text = "" - accountNameInput.reset() - accountNameInput.input.icon.emoji = StatusQUtils.Emoji.getRandomEmoji() - accountColorInput.selectedColorIndex = Math.floor(Math.random() * accountColorInput.model.length) - addressInput.forceActiveFocus(Qt.MouseFocusReason) - } - - Connections { - enabled: popup.opened - target: emojiPopup - onEmojiSelected: function (emojiText, atCursor) { - popup.contentItem.accountNameInput.input.icon.emoji = emojiText - } - } - - contentItem: Column { - property alias accountNameInput: accountNameInput - - width: popup.width - spacing: 8 - topPadding: 20 - - StatusInput { - id: addressInput - // TODO add QR code reader for the address - //% "Enter address..." - input.placeholderText: qsTrId("enter-address...") - //% "Account address" - label: qsTrId("wallet-key-title") - validators: [ - StatusAddressValidator { - //% "This needs to be a valid address (starting with 0x)" - errorMessage: qsTrId("this-needs-to-be-a-valid-address-(starting-with-0x)") - }, - StatusMinLengthValidator { - //% "You need to enter an address" - errorMessage: qsTrId("you-need-to-enter-an-address") - minLength: 1 - } - ] - } - - StatusInput { - id: accountNameInput - //% "Enter an account name..." - input.placeholderText: qsTrId("enter-an-account-name...") - //% "Account name" - label: qsTrId("account-name") - input.isIconSelectable: true - input.icon.color: accountColorInput.selectedColor ? accountColorInput.selectedColor : Theme.palette.directColor1 - onIconClicked: { - popup.emojiPopup.open() - popup.emojiPopup.x = popup.x + Style.current.padding - popup.emojiPopup.y = popup.y + contentItem.y + accountNameInput.y + accountNameInput.height + Style.current.halfPadding - } - validators: [ - StatusMinLengthValidator { - //% "You need to enter an account name" - errorMessage: qsTrId("you-need-to-enter-an-account-name") - minLength: 1 - }, - StatusRegularExpressionValidator { - regularExpression: /^[^<>]+$/ - errorMessage: qsTr("This is not a valid account name") - } - ] - charLimit: 40 - } - - StatusColorSelectorGrid { - id: accountColorInput - anchors.horizontalCenter: parent.horizontalCenter - //% "color" - titleText: qsTr("color").toUpperCase() - } - - Item { - width: parent.width - height: 8 - } - } - - rightButtons: [ - StatusButton { - text: loading ? - //% "Loading..." - qsTrId("loading") : - //% "Add account" - qsTrId("add-account") - - enabled: !loading && addressInput.text !== "" && accountNameInput.text !== "" && accountNameInput.valid - - MessageDialog { - id: accountError - title: "Adding the account failed" - icon: StandardIcon.Critical - standardButtons: StandardButton.Ok - } - - onClicked : { - // TODO the loaidng doesn't work because the function freezes th eview. Might need to use threads - loading = true - if (!addressInput.valid || !accountNameInput.valid) { - Global.playErrorSound(); - return loading = false - } - const error = RootStore.addWatchOnlyAccount(addressInput.text, accountNameInput.text, accountColorInput.selectedColor, accountNameInput.input.icon.emoji); - loading = false - if (error) { - Global.playErrorSound(); - accountError.text = error - return accountError.open() - } - popup.afterAddAccount() - popup.close(); - } - } - ] -} diff --git a/ui/app/AppLayouts/Wallet/popups/GenerateAccountModal.qml b/ui/app/AppLayouts/Wallet/popups/GenerateAccountModal.qml deleted file mode 100644 index 0d7ef1ae75..0000000000 --- a/ui/app/AppLayouts/Wallet/popups/GenerateAccountModal.qml +++ /dev/null @@ -1,173 +0,0 @@ -import QtQuick 2.13 -import QtQuick.Controls 2.13 -import QtQuick.Dialogs 1.3 - -import utils 1.0 - -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Core.Utils 0.1 as StatusQUtils -import StatusQ.Controls 0.1 -import StatusQ.Controls.Validators 0.1 -import StatusQ.Popups 0.1 - -import shared.controls 1.0 - -import "../stores" - -StatusModal { - id: popup - - property int marginBetweenInputs: 38 - property string passwordValidationError: "" - property bool loading: false - property var emojiPopup: null - - signal afterAddAccount() - - //% "Generate an account" - header.title: qsTrId("generate-a-new-account") - - function validate() { - if (passwordInput.text === "") { - //% "You need to enter a password" - passwordValidationError = qsTrId("you-need-to-enter-a-password") - } else if (passwordInput.text.length < 6) { - //% "Password needs to be 6 characters or more" - passwordValidationError = qsTrId("password-needs-to-be-6-characters-or-more") - } else { - passwordValidationError = "" - } - return passwordValidationError === "" && accountNameInput.valid - } - - onOpened: { - passwordValidationError = ""; - passwordInput.text = ""; - accountNameInput.reset() - accountNameInput.text = ""; - accountNameInput.input.icon.emoji = StatusQUtils.Emoji.getRandomEmoji() - colorSelectionGrid.selectedColorIndex = Math.floor(Math.random() * colorSelectionGrid.model.length) - passwordInput.forceActiveFocus(Qt.MouseFocusReason) - } - - Connections { - enabled: popup.opened - target: emojiPopup - onEmojiSelected: function (emojiText, atCursor) { - popup.contentItem.accountNameInput.input.icon.emoji = emojiText - } - } - - contentItem: Column { - property alias accountNameInput: accountNameInput - width: popup.width - spacing: 8 - topPadding: 20 - - // To-Do Password hidden option not supported in StatusQ StatusBaseInput - Item { - width: parent.width - height: passwordInput.height - Input { - id: passwordInput - anchors.fill: parent - anchors.leftMargin: Style.current.padding - anchors.rightMargin: Style.current.padding - - //% "Enter your password…" - placeholderText: qsTrId("enter-your-password…") - //% "Password" - label: qsTrId("password") - textField.echoMode: TextInput.Password - validationError: popup.passwordValidationError - inputLabel.font.pixelSize: 15 - inputLabel.font.weight: Font.Normal - } - } - - StatusInput { - id: accountNameInput - //% "Enter an account name..." - input.placeholderText: qsTrId("enter-an-account-name...") - //% "Account name" - label: qsTrId("account-name") - input.isIconSelectable: true - input.icon.color: colorSelectionGrid.selectedColor ? colorSelectionGrid.selectedColor : Theme.palette.directColor1 - onIconClicked: { - popup.emojiPopup.open() - popup.emojiPopup.x = popup.x + accountNameInput.x + Style.current.padding - popup.emojiPopup.y = popup.y + contentItem.y + accountNameInput.y + accountNameInput.height + Style.current.halfPadding - } - validators: [ - StatusMinLengthValidator { - //% "You need to enter an account name" - errorMessage: qsTrId("you-need-to-enter-an-account-name") - minLength: 1 - }, - StatusRegularExpressionValidator { - regularExpression: /^[^<>]+$/ - errorMessage: qsTr("This is not a valid account name") - } - ] - charLimit: 40 - } - - StatusColorSelectorGrid { - id: colorSelectionGrid - anchors.horizontalCenter: parent.horizontalCenter - //% "color" - titleText: qsTr("color").toUpperCase() - } - - Item { - width: parent.width - height: 8 - } - } - - rightButtons: [ - StatusButton { - text: loading ? - //% "Loading..." - qsTrId("loading") : - //% "Add account" - qsTrId("add-account") - - enabled: !loading && passwordInput.text !== "" && accountNameInput.text !== "" && accountNameInput.valid - - MessageDialog { - id: accountError - title: "Adding the account failed" - icon: StandardIcon.Critical - standardButtons: StandardButton.Ok - } - - onClicked : { - // TODO the loaidng doesn't work because the function freezes th eview. Might need to use threads - loading = true - if (!validate()) { - Global.playErrorSound(); - return loading = false - } - - const errMessage = RootStore.generateNewAccount(passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.icon.emoji) - console.log(errMessage) - loading = false - if (errMessage) { - Global.playErrorSound(); - if (Utils.isInvalidPasswordMessage(errMessage)) { - //% "Wrong password" - popup.passwordValidationError = qsTrId("wrong-password") - } else { - accountError.text = errMessage; - accountError.open(); - } - return - } - popup.afterAddAccount(); - popup.close(); - } - } - ] -} diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index cd00754052..5d79336eb4 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -8,6 +8,7 @@ QtObject { id: root property var currentAccount: Constants.isCppApp ? walletSectionAccounts.currentAccount: walletSectionCurrent property var accounts: walletSectionAccounts.model + property var generatedAccounts: walletSectionAccounts.generated property var appSettings: localAppSettings property var accountSensitiveSettings: localAccountSensitiveSettings property string locale: appSettings.locale diff --git a/ui/app/AppLayouts/Wallet/views/AdvancedAddAccountView.qml b/ui/app/AppLayouts/Wallet/views/AdvancedAddAccountView.qml new file mode 100644 index 0000000000..dda0ef6c36 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/views/AdvancedAddAccountView.qml @@ -0,0 +1,354 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Popups 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Controls.Validators 0.1 + +import utils 1.0 +import "../stores" + +ColumnLayout { + id: advancedSection + + property alias privateKey: privateKey.text + property int addAccountType: AdvancedAddAccountView.AddAccountType.GenerateNew + property string mnemonicText: getSeedPhraseString() + property string errorString: "" + property bool isValid: addAccountType === AdvancedAddAccountView.AddAccountType.ImportSeedPhrase ? grid.isValid : + addAccountType === AdvancedAddAccountView.AddAccountType.ImportPrivateKey ? (privateKey.text !== "" && privateKey.valid) : true + + enum AddAccountType { + GenerateNew, + ImportSeedPhrase, + ImportPrivateKey + } + + function reset() { + mnemonicText = "" + errorString = "" + select.currentIndex = 0 + addAccountType = AdvancedAddAccountView.AddAccountType.GenerateNew + privateKey.text = "" + privateKey.reset() + for(var i = 0; i < grid.model; i++) { + if(grid.itemAtIndex(i)) { + grid.itemAtIndex(i).textEdit.text = "" + grid.itemAtIndex(i).textEdit.reset() + } + } + } + + function validate() { + errorString = ""; + if(addAccountType == AdvancedAddAccountView.AddAccountType.ImportSeedPhrase) { + mnemonicText = getSeedPhraseString() + + if (!Utils.isMnemonic(mnemonicText)) { + //% "Invalid seed phrase" + errorString = qsTrId("custom-seed-phrase") + } else { + errorString = onboardingModule.validateMnemonic(mnemonicText) + const regex = new RegExp('word [a-z]+ not found in the dictionary', 'i'); + if (regex.test(errorString)) { + //% "Invalid seed phrase" + errorString = qsTrId("custom-seed-phrase") + '. ' + + //% "This seed phrase doesn't match our supported dictionary. Check for misspelled words." + qsTrId("custom-seed-phrase-text-1") + } + } + return errorString === "" + } + else if(addAccountType == AdvancedAddAccountView.AddAccountType.ImportPrivateKey) { + if (privateKey.text === "") { + //% "You need to enter a private key" + errorString = qsTrId("you-need-to-enter-a-private-key") + } else if (!Utils.isPrivateKey(privateKey.text)) { + //% "Enter a valid private key (64 characters hexadecimal string)" + errorString = qsTrId("enter-a-valid-private-key-(64-characters-hexadecimal-string)") + } else { + errorString = "" + } + return errorString === "" + } + return true + } + + function getSeedPhraseString() { + var seedPhrase = "" + for(var i = 0; i < grid.model; i++) { + seedPhrase += grid.itemAtIndex(i).text + " " + } + return seedPhrase + } + + QtObject { + id: _internal + property int seedPhraseInputHeight: 44 + property int seedPhraseInputWidth: 220 + } + + spacing: Style.current.padding + + StatusSelect { + id: select + //% "Origin" + label: qsTr("Origin") + Layout.margins: Style.current.padding + property int currentIndex: 0 + selectedItemComponent: StatusListItem { + id: selectedItem + icon.background.color: "transparent" + border.width: 1 + border.color: Theme.palette.baseColor2 + tagsDelegate: StatusListItemTag { + color: model.color + height: Style.current.bigPadding + radius: 6 + closeButtonVisible: false + icon.emoji: model.emoji + icon.emojiSize: Emoji.size.verySmall + icon.isLetterIdenticon: true + title: model.name + titleText.font.pixelSize: 12 + titleText.color: Theme.palette.indirectColor1 + } + } + model: ListModel { + Component.onCompleted: { + //% "Default" + append({"name": qsTr("Default"), "iconName": "status", "accountsModel": RootStore.generatedAccounts, "enabled": true}) + //% "Add new" + append({"name": qsTr("Add new"), "iconName": "", "enabled": false}) + //% "Import new Seed Phrase" + append({"name": qsTr("Import new Seed Phrase"), "iconName": "seed-phrase", "enabled": true}) + //% "Import new Private Key" + append({"name": qsTr("Import new Private Key"), "iconName": "password", "enabled": true}) + selectedItem.title = Qt.binding(function() {return get(select.currentIndex).name}) + selectedItem.icon.name = Qt.binding(function() {return get(select.currentIndex).iconName}) + selectedItem.tagsModel = Qt.binding(function() {return get(select.currentIndex).accountsModel}) + } + } + selectMenu.delegate: StatusListItem { + id: defaultListItem + title: model.name + icon.name: model.iconName + tagsModel : model.accountsModel + enabled: model.enabled + icon.background.color: "transparent" + icon.color: model.accountsModel ? Theme.palette.primaryColor1 : Theme.palette.directColor5 + tagsDelegate: StatusListItemTag { + color: model.color + height: 24 + radius: 6 + closeButtonVisible: false + icon.emoji: model.emoji + icon.emojiSize: Emoji.size.verySmall + icon.isLetterIdenticon: true + title: model.name + titleText.font.pixelSize: 12 + titleText.color: Theme.palette.indirectColor1 + } + onClicked: { + advancedSection.addAccountType = (index === 2) ? AdvancedAddAccountView.AddAccountType.ImportSeedPhrase : + (index === 3) ? AdvancedAddAccountView.AddAccountType.ImportPrivateKey : + AdvancedAddAccountView.AddAccountType.GenerateNew + select.currentIndex = index + select.selectMenu.close() + } + } + } + + StatusInput { + id: privateKey + //% "Private key" + label: qsTrId("private-key") + charLimit: 64 + input.multiline: true + input.minimumHeight: 80 + input.maximumHeight: 108 + //% "Paste the contents of your private key" + input.placeholderText: qsTrId("paste-the-contents-of-your-private-key") + visible: advancedSection.addAccountType === AdvancedAddAccountView.AddAccountType.ImportPrivateKey && advancedSection.visible + errorMessage: advancedSection.errorString + validators: [ + StatusMinLengthValidator { + minLength: 1 + //% "You need to enter a private key" + errorMessage: qsTrId("you-need-to-enter-a-private-key") + }, + StatusValidator { + property var validate: function (value) { + return Utils.isPrivateKey(value) + } + //% "Enter a valid private key (64 characters hexadecimal string)" + errorMessage: qsTrId("enter-a-valid-private-key-(64-characters-hexadecimal-string)") + } + ] + onVisibleChanged: { + if(visible) + privateKey.input.edit.forceActiveFocus(); + } + } + + GridView { + id: grid + Layout.preferredWidth: parent.width + Layout.preferredHeight: visible ? (cellHeight * model/2) + footerItem.height: 0 + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding + visible: advancedSection.addAccountType === AdvancedAddAccountView.AddAccountType.ImportSeedPhrase && advancedSection.visible + cellHeight: _internal.seedPhraseInputHeight + Style.current.halfPadding + cellWidth: _internal.seedPhraseInputWidth + Style.current.halfPadding + model: 12 + interactive: false + property bool isValid: checkIsValid() + function checkIsValid() { + var valid = model > 0 ? true: false + for(var i = 0; i < model; i++) { + if(grid.itemAtIndex(i)) + valid &= grid.itemAtIndex(i).isValid + } + return valid + } + + onVisibleChanged: { + if(visible) + grid.itemAtIndex(0).textEdit.input.edit.forceActiveFocus(); + } + + // To-do Alex has introduced a model for bip39 dictonary, need to use it once its available + // https://github.com/status-im/status-desktop/pull/5058 + delegate: StatusSeedPhraseInput { + id: statusSeedInput + width: _internal.seedPhraseInputWidth + height: _internal.seedPhraseInputHeight + textEdit.errorMessageCmp.visible: false + textEdit.input.anchors.topMargin: 11 + leftComponentText: index + 1 + property bool isValid: !!text + onIsValidChanged: { + grid.isValid = grid.checkIsValid() + } + onTextChanged: { + if (text !== "") { + grid.currentIndex = index; + } + } + // To-do Alex has introduced a model for bip39 dictonary, need to use it once its available + // https://github.com/status-im/status-desktop/pull/5058 + // onDoneInsertingWord: { + // advancedSection.mnemonicText += (index === 0) ? word : (" " + word); + // for (var i = !grid.atXBeginning ? 12 : 0; i < grid.count; i++) { + // if (parseInt(grid.itemAtIndex(i).leftComponentText) === (parseInt(leftComponentText)+1)) { + // grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(); + // } + // } + // } + onEditClicked: { + grid.currentIndex = index; + grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus(); + } + onKeyPressed: { + if (event.key === Qt.Key_Tab || event.key === Qt.Key_Right) { + for (var i = !grid.atXBeginning ? 12 : 0; i < grid.count; i++) { + if (parseInt(grid.itemAtIndex(i).leftComponentText) === ((parseInt(leftComponentText)+1) <= grid.count ? (parseInt(leftComponentText)+1) : grid.count)) { + grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(); + textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit; + } + } + } else if (event.key === Qt.Key_Left) { + for (var i = !grid.atXBeginning ? 12 : 0; i < grid.count; i++) { + if (parseInt(grid.itemAtIndex(i).leftComponentText) === ((parseInt(leftComponentText)-1) >= 0 ? (parseInt(leftComponentText)-1) : 0)) { + grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(); + } + } + } else if (event.key === Qt.Key_Down) { + grid.itemAtIndex((index+1 < grid.count) ? (index+1) : (grid.count-1)).textEdit.input.edit.forceActiveFocus(); + } else if (event.key === Qt.Key_Up) { + grid.itemAtIndex((index-1 >= 0) ? (index-1) : 0).textEdit.input.edit.forceActiveFocus(); + } + } + textEdit.validators: [ + StatusMinLengthValidator { + errorMessage: qsTr("Enter a valid word") + minLength: 3 + } + ] + } + footer: Item { + width: grid.width - Style.current.padding + height: button.height + errorMessage.height + 16*2 + StatusBaseText { + id: errorMessage + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: Style.current.padding + + height: visible ? implicitHeight : 0 + visible: !!text + text: errorString + + font.pixelSize: 12 + color: Theme.palette.dangerColor1 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + StatusButton { + id: button + anchors.top: errorMessage.bottom + anchors.topMargin: Style.current.padding + anchors.horizontalCenter: parent.horizontalCenter + //% "Use 24 word seed phrase" + text: grid.model === 12 ? qsTr("Use 24 word seed phrase"): + qsTr("Use 12 word seed phrase") + onClicked: grid.model = grid.model === 12 ? 24 : 12 + } + } + } + + + RowLayout { + Layout.margins: Style.current.padding + Layout.preferredWidth: parent.width + spacing: Style.current.bigPadding + StatusSelect { + Layout.preferredWidth: 213 + //% "Origin" + label: qsTr("Derivation Path") + selectedItemComponent: StatusListItem { + width: parent.width + icon.background.color: "transparent" + border.width: 1 + border.color: Theme.palette.baseColor2 + title: "Default" + subTitle: "m/44’/61’/0’/1" + enabled: false + } + enabled: false + } + StatusSelect { + Layout.preferredWidth: 213 + //% "Origin" + label: qsTr("Account") + width: parent.width + enabled: false + selectedItemComponent: StatusListItem { + icon.background.color: "transparent" + border.width: 1 + border.color: Theme.palette.baseColor2 + title: "0x1234...abcd" + subTitle: "No activity" + enabled: false + } + } + } +} + diff --git a/ui/app/AppLayouts/Wallet/views/LeftTabView.qml b/ui/app/AppLayouts/Wallet/views/LeftTabView.qml index 8d96706a5e..a7fd79b847 100644 --- a/ui/app/AppLayouts/Wallet/views/LeftTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/LeftTabView.qml @@ -10,6 +10,7 @@ import shared.controls 1.0 import StatusQ.Components 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 import "../controls" import "../popups" @@ -73,66 +74,10 @@ Rectangle { font.weight: Font.Medium font.pixelSize: 13 } - - AddAccountButton { - id: addAccountButton - anchors.top: parent.top - anchors.right: parent.right - onClicked: { - if (newAccountMenu.opened) { - newAccountMenu.close() - } else { - newAccountMenu.popup(addAccountButton.x + addAccountButton.width/2 - newAccountMenu.width/2 , - addAccountButton.y + addAccountButton.height + 55) - } - } - } } - AddNewAccountMenu { - id: newAccountMenu - onAboutToShow: addAccountButton.state = "pressed" - onAboutToHide: { - addAccountButton.state = "default"; - addAccountButton.checked = false; - } - onGenerateNewAccountTriggered: { - generateAccountModal.open(); - } - onAddWatchAccountTriggered: { - addWatchOnlyAccountModal.open(); - } - onEnterSeedPhraseTriggered: { - addAccountWithSeedModal.open(); - } - onEnterPrivateKeyTriggered: { - addAccountWithPrivateKeydModal.open(); - } - } - - GenerateAccountModal { - id: generateAccountModal - anchors.centerIn: parent - onAfterAddAccount: walletInfoContainer.onAfterAddAccount() - emojiPopup: walletInfoContainer.emojiPopup - } - - AddAccountWithSeedModal { - id: addAccountWithSeedModal - anchors.centerIn: parent - onAfterAddAccount: walletInfoContainer.onAfterAddAccount() - emojiPopup: walletInfoContainer.emojiPopup - } - - AddAccountWithPrivateKeyModal { - id: addAccountWithPrivateKeydModal - anchors.centerIn: parent - onAfterAddAccount: walletInfoContainer.onAfterAddAccount() - emojiPopup: walletInfoContainer.emojiPopup - } - - AddWatchOnlyAccountModal { - id: addWatchOnlyAccountModal + AddAccountModal { + id: addAccountModal anchors.centerIn: parent onAfterAddAccount: walletInfoContainer.onAfterAddAccount() emojiPopup: walletInfoContainer.emojiPopup @@ -176,6 +121,20 @@ Rectangle { } } + footer: Item { + width: parent.width + height: addAccountBtn.height + Style.current.xlPadding + StatusButton { + id: addAccountBtn + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: Style.current.bigPadding + //% "Add account" + text: qsTrId("add-account") + onClicked: addAccountModal.open() + } + } + model: RootStore.accounts // model: RootStore.exampleWalletModel }