From 8a69f3bc63a41625393f422c939f08929d539a88 Mon Sep 17 00:00:00 2001 From: Alex Jbanca <47811206+alexjba@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:50:14 +0200 Subject: [PATCH] fix(SeedPhrase): Fixing seed phrase validation (#13496) * fix(SeedPhrase): Fixing seed phrase validation 1. Validate each word after the editing is finished 2. Fixing the seed phrase suggestions where the suggestions box was hidden behind other ui elements 3. Propagate editingFinished signal in StatusBaseInput, StatusInput, StatusSeedPhraseInput 4. Fixing undefined `mnemonicIndex` errors * fix: Refactoring of SeedPhraseInputView Remove duplicated code and use EnterSeedPhrase component + Added storybook page * fix(Onboarding): Fixing seed phrase validation on windows The seed phrase validation fails on windows due to the dictionary line endings * chore(squish): Update e2e tests to the new enter seed phrase panel construction * fix: Load english dictionary from local file using StringUtils --- storybook/pages/EnterSeedPhrasePage.qml | 14 + storybook/pages/SeedPhraseInputViewPage.qml | 72 +++ .../qmlTests/tests/tst_EnterSeedPhrase.qml | 413 ++++++++++++++++++ storybook/stubs/shared/stores/BIP39_en.qml | 18 + storybook/stubs/shared/stores/qmldir | 1 + .../components/wallet_account_popups.py | 2 +- .../global_shared/scripts/onboarding_names.py | 50 +-- .../global_shared/scripts/wallet_names.py | 50 +-- .../include/StatusQ/modelutilsinternal.h | 2 +- .../src/StatusQ/Controls/StatusBaseInput.qml | 20 +- .../src/StatusQ/Controls/StatusInput.qml | 8 + .../Controls/StatusSeedPhraseInput.qml | 114 ++--- ui/app/AppLayouts/Onboarding/controls/qmldir | 2 + .../Onboarding/views/SeedPhraseInputView.qml | 300 +------------ ui/app/AppLayouts/Onboarding/views/qmldir | 3 +- ui/imports/shared/panels/EnterSeedPhrase.qml | 119 +++-- ui/imports/shared/stores/BIP39_en.qml | 20 +- ui/imports/utils/Utils.qml | 4 + 18 files changed, 777 insertions(+), 435 deletions(-) create mode 100644 storybook/pages/EnterSeedPhrasePage.qml create mode 100644 storybook/pages/SeedPhraseInputViewPage.qml create mode 100644 storybook/qmlTests/tests/tst_EnterSeedPhrase.qml create mode 100644 storybook/stubs/shared/stores/BIP39_en.qml create mode 100644 ui/app/AppLayouts/Onboarding/controls/qmldir diff --git a/storybook/pages/EnterSeedPhrasePage.qml b/storybook/pages/EnterSeedPhrasePage.qml new file mode 100644 index 0000000000..301d8eb661 --- /dev/null +++ b/storybook/pages/EnterSeedPhrasePage.qml @@ -0,0 +1,14 @@ +import QtQuick 2.15 + +import shared.panels 1.0 + +import Storybook 1.0 + + +Item { + EnterSeedPhrase { + anchors.centerIn: parent + } +} + +// category: Panels \ No newline at end of file diff --git a/storybook/pages/SeedPhraseInputViewPage.qml b/storybook/pages/SeedPhraseInputViewPage.qml new file mode 100644 index 0000000000..c950599ce7 --- /dev/null +++ b/storybook/pages/SeedPhraseInputViewPage.qml @@ -0,0 +1,72 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import AppLayouts.Onboarding.views 1.0 +import AppLayouts.Onboarding.stores 1.0 + +import utils 1.0 + +import Storybook 1.0 + +SplitView { + Logs { id: logs } + + SeedPhraseInputView { + SplitView.fillWidth: true + SplitView.fillHeight: true + + startupStore: StartupStore { + id: startupStore + function validMnemonic(mnemonic) { + return true + } + property QtObject startupModuleInst: QtObject { + property int keycardData: keycardDataCheckbox.checked ? 0 : Constants.predefinedKeycardData.wrongSeedPhrase + property string flowType: flowTypeComboBox.currentText + } + property QtObject currentStartupState: QtObject { + property string flowType: flowTypeComboBox.currentText + } + + function doPrimaryAction() { + logs.logEvent("Primary action clicked") + } + } + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 200 + SplitView.preferredWidth: 300 + + logsView.logText: logs.logText + + ColumnLayout { + spacing: 10 + width: parent.width + + CheckBox { + id: keycardDataCheckbox + text: "Has keycard data" + checked: startupStore.startupModuleInst.keycardData != Constants.predefinedKeycardData.wrongSeedPhrase + } + + + Label { + text: "Current startup flow type" + } + ComboBox { + id: flowTypeComboBox + Layout.fillWidth: true + model: Object.values(Constants.startupFlow).filter(flow => flow != "" && typeof flow === "string") + } + + + } + } +} + +// category: Panels diff --git a/storybook/qmlTests/tests/tst_EnterSeedPhrase.qml b/storybook/qmlTests/tests/tst_EnterSeedPhrase.qml new file mode 100644 index 0000000000..0b369f0ed3 --- /dev/null +++ b/storybook/qmlTests/tests/tst_EnterSeedPhrase.qml @@ -0,0 +1,413 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQml 2.15 +import QtQml.Models 2.15 + +import StatusQ.Core.Utils 0.1 + +import QtTest 1.15 + +import shared.panels 1.0 +import utils 1.0 + +Item { + id: root + width: 600 + height: 400 + + TestCase { + name: "EnterSeedPhraseTest" + when: windowShown + + Component { + id: enterSeedPhraseComponent + EnterSeedPhrase { + id: enterSeedPhrase + anchors.fill: parent + dictionary: ListModel {} + + readonly property SignalSpy submitSpy: SignalSpy { target: enterSeedPhrase; signalName: "submitSeedPhrase" } + readonly property SignalSpy seedPhraseUpdatedSpy: SignalSpy { target: enterSeedPhrase; signalName: "seedPhraseUpdated" } + } + } + + property EnterSeedPhrase itemUnderTest: null + + function generateDictionaryVariation(baseDictionary) { + let dictionaryVariation = baseDictionary.map((word) => word + "a") + dictionaryVariation = baseDictionary.map((word) => word + "b").concat(dictionaryVariation) + dictionaryVariation = baseDictionary.map((word) => word + "c").concat(dictionaryVariation) + dictionaryVariation = baseDictionary.map((word) => word + "d").concat(dictionaryVariation) + dictionaryVariation.sort() + return dictionaryVariation + } + + function init() { + itemUnderTest = createTemporaryObject(enterSeedPhraseComponent, root) + waitForItemPolished(itemUnderTest) + waitForRendering(itemUnderTest) + } + + function test_componentCreation() { + verify(itemUnderTest !== null, "Component creation failed") + } + + // Test seed phrase input by typing on the keyboard + // The seed phrase is valid and the typing is done without any space between words + // This is the most common way to input a seed phrase + function test_seedPhraseKeyboardInput() { + //generate a seed phrase + const expectedSeedPhrase = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"] + expectedSeedPhrase.sort(); + + let isSeedPhraseValidCalled = false + itemUnderTest.isSeedPhraseValid = (seedPhrase) => { + verify(seedPhrase === expectedSeedPhrase.join(" "), "Seed phrase is not valid") + isSeedPhraseValidCalled = true + return true + } + + itemUnderTest.dictionary.append(expectedSeedPhrase.map((word) => ({seedWord: word}))) + + //Type the seed phrase. No space is needed between words + const str = expectedSeedPhrase.join("") + for (let i = 0; i < str.length; i++) { + keyPress(str.charAt(i)) + } + + verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called") + + keyClick(Qt.Key_Enter) + verify(itemUnderTest.submitSpy.count === 1, "submitSeedPhrase signal was not emitted") + // This signal is emitted multiple times due to the way the seed phrase is updated and validated + // The minimum is the length if the seed phrase + verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted") + } + + // Test seed phrase input by typing on the keyboard + // The seed phrase is valid and the typing is done with a space between words + // The space between words is ignored and the seed should be valid + function test_seedPhraseKeyboardInputWithExtraSpace() { + //generate a seed phrase + const expectedSeedPhrase = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"] + expectedSeedPhrase.sort(); + + let isSeedPhraseValidCalled = false + itemUnderTest.isSeedPhraseValid = (seedPhrase) => { + verify(seedPhrase === expectedSeedPhrase.join(" "), "Seed phrase is not valid") + isSeedPhraseValidCalled = true + return true + } + + itemUnderTest.dictionary.append(expectedSeedPhrase.map((word) => ({seedWord: word}))) + + //Type the seed phrase. A space is needed between words + const str = expectedSeedPhrase.join(" ") + for (let i = 0; i < str.length; i++) { + keyPress(str.charAt(i)) + } + + verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called") + keyClick(Qt.Key_Enter) + verify(itemUnderTest.submitSpy.count === 1, "submitSeedPhrase signal was not emitted") + // This signal is emitted multiple times due to the way the seed phrase is updated and validated + // The minimum is the length if the seed phrase + verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted") + } + + // Test seed phrase input by pasting from clipboard + // The seed phrase is valid and the clipboard seed is space separated + function test_seedPhrasePaste() { + //generate a seed phrase + const expectedSeedPhrase = ["abandona", "abilityb", "ablec", "aboutd", "abovea", "absentb", "absorbc", "abstractd", "absurda", "abuseb", "accessc", "accidentd"] + const baseDictionary = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"] + + expectedSeedPhrase.sort(); + + let isSeedPhraseValidCalled = false + itemUnderTest.isSeedPhraseValid = (seedPhrase) => { + verify(seedPhrase === expectedSeedPhrase.join(" "), "Seed phrase is not valid") + isSeedPhraseValidCalled = true + return true + } + + itemUnderTest.dictionary.append(expectedSeedPhrase.map((word) => ({seedWord: word}))) + + const clipboardHelper = createTemporaryQmlObject("import QtQuick 2.15; QtObject { property var getFromClipboard }", root) + clipboardHelper.getFromClipboard = () => expectedSeedPhrase.join(" ") + Utils.globalUtilsInst = clipboardHelper + + // Trigger the paste action + keyClick("v", Qt.ControlModifier) + + verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called") + + keyClick(Qt.Key_Enter) + verify(itemUnderTest.submitSpy.count === 1, "submitSeedPhrase signal was not emitted") + // This signal is emitted multiple times due to the way the seed phrase is updated and validated + // The minimum is the length if the seed phrase + verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted") + } + + // Test the seed phrase by choosing from the suggestions + // The seed phrase is valid and the user selects the words from the suggestions + function test_seedPhraseChooseFromSuggestions() { + //generate a seed phrase + const expectedSeedPhrase = ["abandona", "abilityb", "ablec", "aboutd", "abovea", "absentb", "absorbc", "abstractd", "absurda", "abuseb", "accessc", "accidentd"] + expectedSeedPhrase.sort() + + const baseDictionary = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"] + let dictionaryVariation = generateDictionaryVariation(baseDictionary) + + let isSeedPhraseValidCalled = false + itemUnderTest.isSeedPhraseValid = (seedPhrase) => { + verify(seedPhrase === expectedSeedPhrase.join(" "), "Seed phrase is not valid") + isSeedPhraseValidCalled = true + return true + } + + itemUnderTest.dictionary.append(dictionaryVariation.map((word) => ({seedWord: word}))) + + // Suggestions dialog is expected to receive key events when there's multiple suggestions + let downKeyEvents = 0 + for (let i = 0; i < expectedSeedPhrase.length; i++) { + keySequence(expectedSeedPhrase[i].substring(0, 4).split('').join(',')) + for (let j = 0; j < downKeyEvents; j++) { + keyClick(Qt.Key_Down) + } + downKeyEvents = downKeyEvents === 3 ? 0 : downKeyEvents + 1 + keyClick(Qt.Key_Tab) + } + + verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called") + + keyPress(Qt.Key_Enter) + verify(itemUnderTest.submitSpy.count === 1, "submitSeedPhrase signal was not emitted") + // This signal is emitted multiple times due to the way the seed phrase is updated and validated + // The minimum is the length if the seed phrase + verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted") + } + + // Test seed phrase input by typing on the keyboard + // The seed phrase is invalidated by the external isSeedPhraseValid + function test_invalidatedSeedPhraseKeyboardInput() { + //generate a seed phrase + const expectedSeedPhrase = ["abandona", "abilityb", "ablec", "aboutd", "abovea", "absentb", "absorbc", "abstractd", "absurda", "abuseb", "accessc", "accidentd"] + expectedSeedPhrase.sort() + + const baseDictionary = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"] + let dictionaryVariation = generateDictionaryVariation(baseDictionary) + + let isSeedPhraseValidCalled = false + itemUnderTest.isSeedPhraseValid = (seedPhrase) => { + verify(seedPhrase === expectedSeedPhrase.join(" "), "Seed phrase is not valid") + isSeedPhraseValidCalled = true + return false + } + + itemUnderTest.dictionary.append(dictionaryVariation.map((word) => ({seedWord: word}))) + + //Type the seed phrase + const str = expectedSeedPhrase.join("") + for (let i = 0; i < str.length; i++) { + keyPress(str.charAt(i)) + } + + verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called") + + keyClick(Qt.Key_Enter) + verify(itemUnderTest.submitSpy.count === 0, "submitSeedPhrase signal was emitted") + verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted") + } + + // Test seed phrase input by pasting from clipboard + // The seed phrase is invalidated by the external isSeedPhraseValid + function test_invalidatedSeedPhrasePaste() { + //generate a seed phrase + const expectedSeedPhrase = ["abandona", "abilityb", "ablec", "aboutd", "abovea", "absentb", "absorbc", "abstractd", "absurda", "abuseb", "accessc", "accidentd"] + expectedSeedPhrase.sort() + + const baseDictionary = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"] + let dictionaryVariation = generateDictionaryVariation(baseDictionary) + + let isSeedPhraseValidCalled = false + itemUnderTest.isSeedPhraseValid = (seedPhrase) => { + verify(seedPhrase === expectedSeedPhrase.join(" "), "Seed phrase is not valid") + isSeedPhraseValidCalled = true + return false + } + + itemUnderTest.dictionary.append(dictionaryVariation.map((word) => ({seedWord: word}))) + + const clipboardHelper = createTemporaryQmlObject("import QtQuick 2.15; QtObject { property var getFromClipboard }", root) + clipboardHelper.getFromClipboard = () => expectedSeedPhrase.join(" ") + Utils.globalUtilsInst = clipboardHelper + + // Trigger the paste action + keyClick("v", Qt.ControlModifier) + + verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called") + + keyClick(Qt.Key_Enter) + verify(itemUnderTest.submitSpy.count === 0, "submitSeedPhrase signal was emitted") + verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted") + } + + // Test seed phrase input by typing on the keyboard + // The seed phrase is invalid due to the length + function test_invalidLengthSeedPhrase() { + const expectedSeedPhrase = ["abandona", "abilityb", "ablec"] + expectedSeedPhrase.sort() + + const baseDictionary = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"] + let dictionaryVariation = generateDictionaryVariation(baseDictionary) + + let isSeedPhraseValidCalled = false + itemUnderTest.isSeedPhraseValid = (seedPhrase) => { + verify(seedPhrase === expectedSeedPhrase.join(" "), "Seed phrase is not valid") + isSeedPhraseValidCalled = true + return true + } + + itemUnderTest.dictionary.append(dictionaryVariation.map((word) => ({seedWord: word}))) + + //Type the seed phrase + const str = expectedSeedPhrase.join("") + for (let i = 0; i < str.length; i++) { + keyPress(str.charAt(i)) + } + + keyClick(Qt.Key_Enter) + verify(!isSeedPhraseValidCalled, "isSeedPhraseValid was called when it should not have been") + verify(itemUnderTest.submitSpy.count === 0, "submitSeedPhrase signal was emitted") + verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted") + } + + // Test seed phrase input by typing on the keyboard + // The seed phrase is invalid due to the dictionary word + function test_invalidDictionarySeedPhrase() { + const expectedSeedPhrase = ["abandonna", "abilityb", "ablec", "aboutd", "abovea", "absentb", "absorbc", "abstractd", "absurda", "abuseb", "accessc", "accidentd"] + // ^^ invalid word + expectedSeedPhrase.sort() + + const baseDictionary = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"] + let dictionaryVariation = generateDictionaryVariation(baseDictionary) + + let isSeedPhraseValidCalled = false + itemUnderTest.isSeedPhraseValid = (seedPhrase) => { + verify(seedPhrase === expectedSeedPhrase.join(" "), "Seed phrase is not valid") + isSeedPhraseValidCalled = true + return true + } + + itemUnderTest.dictionary.append(dictionaryVariation.map((word) => ({seedWord: word}))) + + //Type the seed phrase + const str = expectedSeedPhrase.join("") + for (let i = 0; i < str.length; i++) { + keyPress(str.charAt(i)) + if (i === 8) { + // The first word is invalid. Move on to the next word + keyPress(Qt.Key_Tab) + } + } + + keyClick(Qt.Key_Enter) + print (itemUnderTest.seedPhraseUpdatedSpy.count) + verify(itemUnderTest.submitSpy.count === 0, "submitSeedPhrase signal was emitted") + verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted") + } + + // Test suggestions are active after the seed phrase word is updated + function test_suggestionsActiveAfterUpdatingWord() { + const expectedSeedPhrase = ["abandona", "abilityb", "ablec", "aboutd", "abovea", "absentb", "absorbc", "abstractd", "absurda", "abuseb", "accessc", "accidentd"] + expectedSeedPhrase.sort() + + const baseDictionary = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"] + let dictionaryVariation = generateDictionaryVariation(baseDictionary) + + let isSeedPhraseValidCalled = false + let lastVerifiedSeedPhrase = "" + itemUnderTest.isSeedPhraseValid = (seedPhrase) => { + lastVerifiedSeedPhrase = seedPhrase + isSeedPhraseValidCalled = true + return true + } + + itemUnderTest.dictionary.append(dictionaryVariation.map((word) => ({seedWord: word}))) + + // Suggestions dialog is expected to receive key events when there's multiple suggestions + let downKeyEvents = 0 + for (let i = 0; i < expectedSeedPhrase.length; i++) { + keySequence(expectedSeedPhrase[i].substring(0, 4).split('').join(',')) + for (let j = 0; j < downKeyEvents; j++) { + keyClick(Qt.Key_Down) + } + downKeyEvents = downKeyEvents === 3 ? 0 : downKeyEvents + 1 + keyClick(Qt.Key_Tab) + } + + verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called") + + isSeedPhraseValidCalled = false + + keyPress(Qt.Key_Backspace) + wait (500) // Wait for the suggestions to appear + keyClick(Qt.Key_Tab) + keyClick(Qt.Key_Enter) + + verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called") + print (lastVerifiedSeedPhrase) + verify(lastVerifiedSeedPhrase === expectedSeedPhrase.join(" ").slice(0, -1) + "a", "Seed phrase is not updated") + verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted") + + } + + // Test suggestions are active after the seed phrase word is fixed + function test_suggestionsActiveAfterFixingWord() { + const expectedSeedPhrase = ["abandona", "abilityb", "ablec", "aboutd", "abovea", "absentb", "absorbc", "abstractd", "absurda", "abuseb", "accessc", "accidenntd"] + expectedSeedPhrase.sort() + + const baseDictionary = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"] + let dictionaryVariation = generateDictionaryVariation(baseDictionary) + + let isSeedPhraseValidCalled = false + let lastVerifiedSeedPhrase = "" + itemUnderTest.isSeedPhraseValid = (seedPhrase) => { + lastVerifiedSeedPhrase = seedPhrase + isSeedPhraseValidCalled = true + return true + } + + itemUnderTest.dictionary.append(dictionaryVariation.map((word) => ({seedWord: word}))) + + // Suggestions dialog is expected to receive key events when there's multiple suggestions + let downKeyEvents = 0 + for (let i = 0; i < expectedSeedPhrase.length; i++) { + keySequence(expectedSeedPhrase[i].substring(0, 4).split('').join(',')) + for (let j = 0; j < downKeyEvents; j++) { + keyClick(Qt.Key_Down) + } + downKeyEvents = downKeyEvents === 3 ? 0 : downKeyEvents + 1 + keyClick(Qt.Key_Tab) + } + + verify(isSeedPhraseValidCalled, "isSeedPhraseValid is not called") + + isSeedPhraseValidCalled = false + + for (let i = 0; i < 2; i++) { + keyPress(Qt.Key_Backspace) + } + + wait (500) // Wait for the suggestions to appear + keyClick(Qt.Key_Tab) + keyClick(Qt.Key_Enter) + + verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called") + verify(lastVerifiedSeedPhrase === expectedSeedPhrase.join(" ").slice(0, -3) + "ta", "Seed phrase is not updated") + verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted") + verify(itemUnderTest.submitSpy.count === 1, "submitSeedPhrase signal was emitted") + } + } +} diff --git a/storybook/stubs/shared/stores/BIP39_en.qml b/storybook/stubs/shared/stores/BIP39_en.qml new file mode 100644 index 0000000000..0a116044fb --- /dev/null +++ b/storybook/stubs/shared/stores/BIP39_en.qml @@ -0,0 +1,18 @@ +import QtQuick 2.13 + +ListModel { + id: root + + Component.onCompleted: { + var englishWords = [ + "apple", "banana", "cat", "dog", "elephant", "fish", "grape", "horse", "ice cream", "jellyfish", + "kiwi", "lemon", "mango", "nut", "orange", "pear", "quail", "rabbit", "strawberry", "turtle", + "umbrella", "violet", "watermelon", "xylophone", "yogurt", "zebra" + // Add more English words here... + ]; + + for (var i = 0; i < englishWords.length; i++) { + root.append({ seedWord: englishWords[i] }); + } + } +} diff --git a/storybook/stubs/shared/stores/qmldir b/storybook/stubs/shared/stores/qmldir index 863b8d52b9..f7439fc0fd 100644 --- a/storybook/stubs/shared/stores/qmldir +++ b/storybook/stubs/shared/stores/qmldir @@ -3,3 +3,4 @@ CurrenciesStore 1.0 CurrenciesStore.qml NetworkConnectionStore 1.0 NetworkConnectionStore.qml singleton RootStore 1.0 RootStore.qml TokenBalanceHistoryStore 1.0 TokenBalanceHistoryStore.qml +BIP39_en 1.0 BIP39_en.qml \ No newline at end of file diff --git a/test/ui-test/src/screens/components/wallet_account_popups.py b/test/ui-test/src/screens/components/wallet_account_popups.py index 5a425619a4..c9e5452008 100644 --- a/test/ui-test/src/screens/components/wallet_account_popups.py +++ b/test/ui-test/src/screens/components/wallet_account_popups.py @@ -83,7 +83,7 @@ class AddNewAccountPopup(BasePopup): else: raise RuntimeError("Wrong amount of seed words", len(seed_phrase_words)) for count, word in enumerate(seed_phrase_words, start=1): - self._seed_phrase_word_text_edit.object_name['objectName'] = f'statusSeedPhraseInputField{count}' + self._seed_phrase_word_text_edit.object_name['objectName'] = f'enterSeedPhraseInputField{count}' self._seed_phrase_word_text_edit.text = word seed_phrase_name = ''.join([word[0] for word in seed_phrase_words[:10]]) self._seed_phrase_phrase_key_name_text_edit.text = seed_phrase_name diff --git a/test/ui-test/testSuites/global_shared/scripts/onboarding_names.py b/test/ui-test/testSuites/global_shared/scripts/onboarding_names.py index 231faec582..6d70d70f18 100644 --- a/test/ui-test/testSuites/global_shared/scripts/onboarding_names.py +++ b/test/ui-test/testSuites/global_shared/scripts/onboarding_names.py @@ -30,34 +30,34 @@ onboarding_back_button = {"container": statusDesktop_mainWindow, "objectName": " # Seed phrase form: import_a_seed_phrase_StatusBaseText = {"container": statusDesktop_mainWindow, "text": "Import a seed phrase", "type": "StatusBaseText", "unnamed": 1, "visible": True} -mainWindow_switchTabBar_StatusSwitchTabBar = {"container": statusDesktop_mainWindow, "objectName": "onboardingSeedPhraseSwitchBar", "type": "StatusSwitchTabBar"} +mainWindow_switchTabBar_StatusSwitchTabBar = {"container": statusDesktop_mainWindow, "objectName": "enterSeedPhraseSwitchBar", "type": "StatusSwitchTabBar"} switchTabBar_12_words_Button = {"container": mainWindow_switchTabBar_StatusSwitchTabBar, "objectName": "12SeedButton", "type": "StatusSwitchTabButton"} switchTabBar_18_words_Button = {"container": mainWindow_switchTabBar_StatusSwitchTabBar, "objectName": "18SeedButton", "type": "StatusSwitchTabButton"} switchTabBar_24_words_Button = {"container": mainWindow_switchTabBar_StatusSwitchTabBar, "objectName": "24SeedButton", "type": "StatusSwitchTabButton"} seedPhraseView_Submit_Button = {"container": statusDesktop_mainWindow, "objectName": "seedPhraseViewSubmitButton", "type": "StatusButton"} onboarding_InvalidSeed_Text = {"container": statusDesktop_mainWindow, "objectName": "onboardingInvalidSeedText", "type": "StatusBaseText"} -onboarding_SeedPhrase_Input_TextField_1 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField1"} -onboarding_SeedPhrase_Input_TextField_2 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField2"} -onboarding_SeedPhrase_Input_TextField_3 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField3"} -onboarding_SeedPhrase_Input_TextField_4 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField4"} -onboarding_SeedPhrase_Input_TextField_5 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField5"} -onboarding_SeedPhrase_Input_TextField_6 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField6"} -onboarding_SeedPhrase_Input_TextField_7 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField7"} -onboarding_SeedPhrase_Input_TextField_8 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField8"} -onboarding_SeedPhrase_Input_TextField_9 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField9"} -onboarding_SeedPhrase_Input_TextField_10 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField10"} -onboarding_SeedPhrase_Input_TextField_11 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField11"} -onboarding_SeedPhrase_Input_TextField_12 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField12"} -onboarding_SeedPhrase_Input_TextField_13 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField13"} -onboarding_SeedPhrase_Input_TextField_14 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField14"} -onboarding_SeedPhrase_Input_TextField_15 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField15"} -onboarding_SeedPhrase_Input_TextField_16 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField16"} -onboarding_SeedPhrase_Input_TextField_17 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField17"} -onboarding_SeedPhrase_Input_TextField_18 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField18"} -onboarding_SeedPhrase_Input_TextField_19 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField19"} -onboarding_SeedPhrase_Input_TextField_20 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField20"} -onboarding_SeedPhrase_Input_TextField_21 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField21"} -onboarding_SeedPhrase_Input_TextField_22 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField22"} -onboarding_SeedPhrase_Input_TextField_23 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField23"} -onboarding_SeedPhrase_Input_TextField_24 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "statusSeedPhraseInputField24"} \ No newline at end of file +onboarding_SeedPhrase_Input_TextField_1 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField1"} +onboarding_SeedPhrase_Input_TextField_2 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField2"} +onboarding_SeedPhrase_Input_TextField_3 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField3"} +onboarding_SeedPhrase_Input_TextField_4 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField4"} +onboarding_SeedPhrase_Input_TextField_5 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField5"} +onboarding_SeedPhrase_Input_TextField_6 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField6"} +onboarding_SeedPhrase_Input_TextField_7 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField7"} +onboarding_SeedPhrase_Input_TextField_8 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField8"} +onboarding_SeedPhrase_Input_TextField_9 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField9"} +onboarding_SeedPhrase_Input_TextField_10 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField10"} +onboarding_SeedPhrase_Input_TextField_11 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField11"} +onboarding_SeedPhrase_Input_TextField_12 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField12"} +onboarding_SeedPhrase_Input_TextField_13 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField13"} +onboarding_SeedPhrase_Input_TextField_14 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField14"} +onboarding_SeedPhrase_Input_TextField_15 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField15"} +onboarding_SeedPhrase_Input_TextField_16 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField16"} +onboarding_SeedPhrase_Input_TextField_17 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField17"} +onboarding_SeedPhrase_Input_TextField_18 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField18"} +onboarding_SeedPhrase_Input_TextField_19 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField19"} +onboarding_SeedPhrase_Input_TextField_20 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField20"} +onboarding_SeedPhrase_Input_TextField_21 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField21"} +onboarding_SeedPhrase_Input_TextField_22 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField22"} +onboarding_SeedPhrase_Input_TextField_23 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField23"} +onboarding_SeedPhrase_Input_TextField_24 = {"container": statusDesktop_mainWindow, "type": "TextEdit", "objectName": "enterSeedPhraseInputField24"} \ No newline at end of file diff --git a/test/ui-test/testSuites/global_shared/scripts/wallet_names.py b/test/ui-test/testSuites/global_shared/scripts/wallet_names.py index 3dae835948..af90470356 100644 --- a/test/ui-test/testSuites/global_shared/scripts/wallet_names.py +++ b/test/ui-test/testSuites/global_shared/scripts/wallet_names.py @@ -126,34 +126,34 @@ mainWallet_AddEditAccountPopup_SeedPhraseWordAtIndex_Placeholder = {"container": mainWallet_AddEditAccountPopup_EnterSeedPhraseWordComponent = {"container": mainWallet_AddEditAccountPopup_Content, "objectName": "AddAccountPopup-EnterSeedPhraseWord", "type": "StatusInput", "visible": True} mainWallet_AddEditAccountPopup_EnterSeedPhraseWord = {"container": mainWallet_AddEditAccountPopup_EnterSeedPhraseWordComponent, "type": "TextEdit", "unnamed": 1, "visible": True} confirmSeedPhrasePanel_StatusSeedPhraseInput = {"container": statusDesktop_mainWindow, "type": "StatusSeedPhraseInput", "visible": True} -mainWallet_AddEditAccountPopup_SPWord = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": RegularExpression("statusSeedPhraseInputField*")} +mainWallet_AddEditAccountPopup_SPWord = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": RegularExpression("enterSeedPhraseInputField*")} mainWallet_AddEditAccountPopup_12WordsButton = {"container": mainWallet_AddEditAccountPopup_Content, "objectName": "12SeedButton", "type": "StatusSwitchTabButton"} mainWallet_AddEditAccountPopup_18WordsButton = {"container": mainWallet_AddEditAccountPopup_Content, "objectName": "18SeedButton", "type": "StatusSwitchTabButton"} mainWallet_AddEditAccountPopup_24WordsButton = {"container": mainWallet_AddEditAccountPopup_Content, "objectName": "24SeedButton", "type": "StatusSwitchTabButton"} -mainWallet_AddEditAccountPopup_SPWord_1 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField1"} -mainWallet_AddEditAccountPopup_SPWord_2 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField2"} -mainWallet_AddEditAccountPopup_SPWord_3 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField3"} -mainWallet_AddEditAccountPopup_SPWord_4 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField4"} -mainWallet_AddEditAccountPopup_SPWord_5 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField5"} -mainWallet_AddEditAccountPopup_SPWord_6 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField6"} -mainWallet_AddEditAccountPopup_SPWord_7 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField7"} -mainWallet_AddEditAccountPopup_SPWord_8 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField8"} -mainWallet_AddEditAccountPopup_SPWord_9 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField9"} -mainWallet_AddEditAccountPopup_SPWord_10 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField10"} -mainWallet_AddEditAccountPopup_SPWord_11 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField11"} -mainWallet_AddEditAccountPopup_SPWord_12 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField12"} -mainWallet_AddEditAccountPopup_SPWord_13 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField13"} -mainWallet_AddEditAccountPopup_SPWord_14 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField14"} -mainWallet_AddEditAccountPopup_SPWord_15 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField15"} -mainWallet_AddEditAccountPopup_SPWord_16 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField16"} -mainWallet_AddEditAccountPopup_SPWord_17 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField17"} -mainWallet_AddEditAccountPopup_SPWord_18 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField18"} -mainWallet_AddEditAccountPopup_SPWord_19 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField19"} -mainWallet_AddEditAccountPopup_SPWord_20 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField20"} -mainWallet_AddEditAccountPopup_SPWord_21 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField21"} -mainWallet_AddEditAccountPopup_SPWord_22 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField22"} -mainWallet_AddEditAccountPopup_SPWord_23 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField23"} -mainWallet_AddEditAccountPopup_SPWord_24 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "statusSeedPhraseInputField24"} +mainWallet_AddEditAccountPopup_SPWord_1 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField1"} +mainWallet_AddEditAccountPopup_SPWord_2 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField2"} +mainWallet_AddEditAccountPopup_SPWord_3 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField3"} +mainWallet_AddEditAccountPopup_SPWord_4 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField4"} +mainWallet_AddEditAccountPopup_SPWord_5 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField5"} +mainWallet_AddEditAccountPopup_SPWord_6 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField6"} +mainWallet_AddEditAccountPopup_SPWord_7 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField7"} +mainWallet_AddEditAccountPopup_SPWord_8 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField8"} +mainWallet_AddEditAccountPopup_SPWord_9 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField9"} +mainWallet_AddEditAccountPopup_SPWord_10 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField10"} +mainWallet_AddEditAccountPopup_SPWord_11 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField11"} +mainWallet_AddEditAccountPopup_SPWord_12 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField12"} +mainWallet_AddEditAccountPopup_SPWord_13 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField13"} +mainWallet_AddEditAccountPopup_SPWord_14 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField14"} +mainWallet_AddEditAccountPopup_SPWord_15 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField15"} +mainWallet_AddEditAccountPopup_SPWord_16 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField16"} +mainWallet_AddEditAccountPopup_SPWord_17 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField17"} +mainWallet_AddEditAccountPopup_SPWord_18 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField18"} +mainWallet_AddEditAccountPopup_SPWord_19 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField19"} +mainWallet_AddEditAccountPopup_SPWord_20 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField20"} +mainWallet_AddEditAccountPopup_SPWord_21 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField21"} +mainWallet_AddEditAccountPopup_SPWord_22 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField22"} +mainWallet_AddEditAccountPopup_SPWord_23 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField23"} +mainWallet_AddEditAccountPopup_SPWord_24 = {"container": mainWallet_AddEditAccountPopup_Content, "type": "TextEdit", "objectName": "enterSeedPhraseInputField24"} # Remove account popup: mainWallet_Remove_Account_Popup_Account_Notification = {"container": statusDesktop_mainWindow, "objectName": "RemoveAccountPopup-Notification", "type": "StatusBaseText", "visible": True} diff --git a/ui/StatusQ/include/StatusQ/modelutilsinternal.h b/ui/StatusQ/include/StatusQ/modelutilsinternal.h index 96d4e0afcc..feb40b8e58 100644 --- a/ui/StatusQ/include/StatusQ/modelutilsinternal.h +++ b/ui/StatusQ/include/StatusQ/modelutilsinternal.h @@ -4,8 +4,8 @@ #include #include #include +#include -class QAbstractItemModel; class QJSEngine; class QQmlEngine; diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml index 456bbc7d95..00f913b826 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml @@ -267,6 +267,11 @@ Item { This signal is emitted when the text edit is clicked. */ signal editClicked() + /*! + \qmlsignal editingFinished + This signal is emitted when the text edit loses focus. + */ + signal editingFinished() onFocusChanged: { if(focus) edit.forceActiveFocus() @@ -368,12 +373,20 @@ Item { wrapMode: root.multiline ? Text.WrapAtWordBoundaryOrAnywhere : TextEdit.NoWrap Keys.onReturnPressed: { - root.keyPressed(event) event.accepted = !multiline && !acceptReturn + // Special case for single line inputs, where we want to accept the return key, but notify the parend + // Enter and return can be used to accept the input + if (event.accepted) { + root.keyPressed(event) + } } Keys.onEnterPressed: { - root.keyPressed(event) event.accepted = !multiline && !acceptReturn + // Special case for single line inputs, where we want to accept the return key, but notify the parend + // Enter and return can be used to accept the input + if (event.accepted) { + root.keyPressed(event) + } } Keys.forwardTo: [root] KeyNavigation.priority: !!root.tabNavItem ? KeyNavigation.BeforeItem : KeyNavigation.AfterItem @@ -409,6 +422,9 @@ Item { previousText = text } } + onEditingFinished: { + root.editingFinished() + } cursorDelegate: StatusCursorDelegate { cursorVisible: edit.cursorVisible diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml index 8ce0c93563..52a2127c47 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml @@ -215,6 +215,11 @@ Item { This signal is emitted when the text edit is clicked. */ signal editClicked() + /*! + \qmlsignal editingFinished + This signal is emitted when the text edit loses focus. + */ + signal editingFinished() /*! \qmltype ValidationMode @@ -453,6 +458,9 @@ Item { onEditChanged: { root.editClicked(); } + onEditingFinished: { + root.editingFinished() + } } StatusBaseText { diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml index 63ca85af64..5e8f516479 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml @@ -97,12 +97,16 @@ Item { The corresponding handler is \c onEditClicked */ signal editClicked() + /*! + \qmlsignal editingFinished + This signal is emitted when the text edit loses focus. + */ + signal editingFinished() function setWord(seedWord) { let seedWordTrimmed = seedWord.trim() seedWordInput.input.edit.text = seedWordTrimmed seedWordInput.input.edit.cursorPosition = seedWordInput.text.length - seedSuggestionsList.model = 0 root.doneInsertingWord(seedWordTrimmed) } @@ -134,72 +138,77 @@ Item { d.isInputValidWord = false filteredList.clear(); let textToCheck = text.trim().toLowerCase() - if (textToCheck !== "") { - for (var i = 0; i < inputList.count; i++) { - if (inputList.get(i).seedWord.startsWith(textToCheck)) { - filteredList.insert(filteredList.count, {"seedWord": inputList.get(i).seedWord}); - if(inputList.get(i).seedWord === textToCheck) - d.isInputValidWord = true - } + + if (textToCheck === "") { + return; + } + + for (var i = 0; i < inputList.count; i++) { + if (inputList.get(i).seedWord.startsWith(textToCheck)) { + filteredList.insert(filteredList.count, {"seedWord": inputList.get(i).seedWord}); + if(inputList.get(i).seedWord === textToCheck) + d.isInputValidWord = true } - seedSuggestionsList.model = filteredList; - if (filteredList.count === 1 && input.edit.keyEvent !== Qt.Key_Backspace - && input.edit.keyEvent !== Qt.Key_Delete - && filteredList.get(0).seedWord.trim() === textToCheck) { - seedWordInput.input.edit.cursorPosition = textToCheck.length; - seedSuggestionsList.model = 0; - root.doneInsertingWord(textToCheck); - } - } else { - seedSuggestionsList.model = 0; + } + + if (filteredList.count === 1 && input.edit.keyEvent !== Qt.Key_Backspace + && input.edit.keyEvent !== Qt.Key_Delete + && filteredList.get(0).seedWord.trim() === textToCheck) { + seedWordInput.input.edit.cursorPosition = textToCheck.length; + filteredList.clear(); + root.doneInsertingWord(textToCheck); } } onKeyPressed: { - if (input.edit.keyEvent === Qt.Key_Tab || input.edit.keyEvent === Qt.Key_Return || input.edit.keyEvent === Qt.Key_Enter) { - if (!!text && seedSuggestionsList.count > 0) { - root.setWord(filteredList.get(seedSuggestionsList.currentIndex).seedWord) - event.accepted = true - return + switch (input.edit.keyEvent) { + case Qt.Key_Tab: + case Qt.Key_Return: + case Qt.Key_Enter: { + if (!!text && filteredList.count > 0) { + root.setWord(filteredList.get(seedSuggestionsList.currentIndex).seedWord) + event.accepted = true + return + } + break; + } + case Qt.Key_Down: { + seedSuggestionsList.incrementCurrentIndex() + input.edit.keyEvent = null + break; + } + case Qt.Key_Up: { + seedSuggestionsList.decrementCurrentIndex() + input.edit.keyEvent = null + break; + } + case Qt.Key_Space: { + event.accepted = !event.text.match(/^[a-zA-Z]$/) + break; } } - if (input.edit.keyEvent === Qt.Key_Down) { - seedSuggestionsList.incrementCurrentIndex() - input.edit.keyEvent = null - } - if (input.edit.keyEvent === Qt.Key_Up) { - seedSuggestionsList.decrementCurrentIndex() - input.edit.keyEvent = null - } + root.keyPressed(event); } onEditClicked: { root.editClicked(); } - // Consider word inserted if input looses focus while a valid word is present ("user" clicks outside) - Connections { - target: seedWordInput.input.edit - function onActiveFocusChanged() { - if (!seedWordInput.input.edit.activeFocus && d.isInputValidWord) { - // There are so many side effects regarding focus and doneInsertingWord that we need to reset this flag not to be processed again. - d.isInputValidWord = false - root.doneInsertingWord(root.text.trim()) - } - } + + onEditingFinished: { + root.editingFinished(); } } - Item { + Popup { id: suggListContainer - width: seedSuggestionsList.width - height: (((seedSuggestionsList.count <= 5) ? seedSuggestionsList.count : 5) *34) + 16 - anchors.left: parent.left - anchors.leftMargin: 16 - anchors.top: seedWordInput.bottom - anchors.topMargin: 4 - visible: ((seedSuggestionsList.count > 0) && seedWordInput.input.edit.activeFocus) - Rectangle { + contentWidth: seedSuggestionsList.width + contentHeight: ((seedSuggestionsList.count <= 5) ? seedSuggestionsList.count : 5) *34 + 16 + x: 16 + y: seedWordInput.height + 4 + topPadding: 8 + bottomPadding: 8 + visible: ((filteredList.count > 0) && seedWordInput.input.edit.activeFocus) + background: Rectangle { id: statusMenuBackgroundContent - anchors.fill: parent color: Theme.palette.statusMenu.backgroundColor radius: 8 layer.enabled: true @@ -219,9 +228,7 @@ Item { width: ((seedSuggestionsList.contentItem.childrenRect.width + 24) > root.width) ? root.width : (seedSuggestionsList.contentItem.childrenRect.width + 24) anchors.top: parent.top - anchors.topMargin: 8 anchors.bottom: parent.bottom - anchors.bottomMargin: 8 onCountChanged: { seedSuggestionsList.currentIndex = 0 @@ -229,6 +236,7 @@ Item { clip: true ScrollBar.vertical: ScrollBar { } + model: root.filteredList delegate: Item { id: txtDelegate width: suggWord.contentWidth diff --git a/ui/app/AppLayouts/Onboarding/controls/qmldir b/ui/app/AppLayouts/Onboarding/controls/qmldir new file mode 100644 index 0000000000..87da99d316 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding/controls/qmldir @@ -0,0 +1,2 @@ +AccountViewDelegate 1.0 AccountViewDelegate.qml +OnboardingBasePage 1.0 OnboardingBasePage.qml \ No newline at end of file diff --git a/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml b/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml index 1b35992031..30f70e0d1d 100644 --- a/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml +++ b/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml @@ -1,35 +1,26 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.13 -import QtQuick.Dialogs 1.3 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 import StatusQ.Controls 0.1 import StatusQ.Popups 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import AppLayouts.Onboarding.controls 1.0 +import AppLayouts.Onboarding.stores 1.0 + import utils 1.0 +import shared.panels 1.0 import shared.stores 1.0 import shared.controls 1.0 -import "../controls" -import "../stores" - Item { id: root property StartupStore startupStore - property var mnemonicInput: [] - - signal seedValidated() - - readonly property var tabs: [12, 18, 24] - - Timer { - id: timer - } - QtObject { id: d @@ -38,272 +29,43 @@ Item { onWrongSeedPhraseChanged: { if (wrongSeedPhrase) { if (root.startupStore.startupModuleInst.flowType === Constants.startupFlow.firstRunOldUserImportSeedPhrase) { - invalidSeedTxt.text = qsTr("Profile keypair for the inserted seed phrase is already set up") + seedPhraseView.setWrongSeedPhraseMessage(qsTr("Profile keypair for the inserted seed phrase is already set up")) return } - invalidSeedTxt.text = qsTr("Seed phrase doesn’t match the profile of an existing Keycard user on this device") + seedPhraseView.setWrongSeedPhraseMessage(qsTr("Seed phrase doesn’t match the profile of an existing Keycard user on this device")) } else { - invalidSeedTxt.text = "" + seedPhraseView.setWrongSeedPhraseMessage("") } } } - function pasteWords () { - const clipboardText = globalUtils.getFromClipboard() - // Split words separated by commas and or blank spaces (spaces, enters, tabs) - const words = clipboardText.split(/[, \s]+/) - - let index = root.tabs.indexOf(words.length) - if (index === -1) { - return false - } - - let timeout = 0 - if (switchTabBar.currentIndex !== index) { - switchTabBar.currentIndex = index - // Set the teimeout to 100 so the grid has time to generate the new items - timeout = 100 - } - - root.mnemonicInput = [] - timer.setTimeout(() => { - // Populate mnemonicInput - for (let i = 0; i < words.length; i++) { - grid.addWord(i + 1, words[i], true) - } - // Populate grid - for (let j = 0; j < grid.count; j++) { - const item = grid.itemAtIndex(j) - if (!item || !item.leftComponentText) { - // The grid has gaps in it and also sometimes doesn't return the item correctly when offscreen - // in those cases, we just add the word in the array but not in the grid. - // The button will still work and import correctly. The Grid itself will be partly empty, but offscreen - // With the re-design of the grid, this should be fixed - continue - } - const pos = item.mnemonicIndex - item.setWord(words[pos - 1]) - } - submitButton.checkMnemonicLength() - }, timeout) - return true - } - - Item { - implicitWidth: 565 - implicitHeight: parent.height - anchors.horizontalCenter: parent.horizontalCenter + ColumnLayout { + width: 565 + implicitHeight: contentItem.implicitHeight + anchors.centerIn: parent + spacing: 24 StatusBaseText { id: headlineText font.pixelSize: 22 font.weight: Font.Bold color: Theme.palette.directColor1 - anchors.top: parent.top - anchors.topMargin: (root.height - parent.childrenRect.height)/2 - anchors.horizontalCenter: parent.horizontalCenter + Layout.alignment: Qt.AlignHCenter text: qsTr("Enter seed phrase") } - StatusSwitchTabBar { - id: switchTabBar - objectName: "onboardingSeedPhraseSwitchBar" - anchors.top: headlineText.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: 24 - Repeater { - model: root.tabs - StatusSwitchTabButton { - text: qsTr("%n word(s)", "", modelData) - id: seedPhraseWords - objectName: `${modelData}SeedButton` - } - } - onCurrentIndexChanged: { - root.mnemonicInput = root.mnemonicInput.filter(function(value) { - return value.pos <= root.tabs[switchTabBar.currentIndex] - }) - submitButton.checkMnemonicLength() - } - } - clip: true - - StatusGridView { - id: grid - objectName: "seedPhraseGridView" - width: parent.width - readonly property var wordIndex: [ - ["1", "3", "5", "7", "9", "11", "2", "4", "6", "8", "10", "12"] - ,["1", "4", "7", "10", "13", "16", "2", "5", "8", - "11", "14", "17", "3", "6", "9", "12", "15", "18"] - ,["1", "5", "9", "13", "17", "21", "2", "6", "10", "14", "18", "22", - "3", "7", "11", "15", "19", "23", "4", "8", "12", "16", "20", "24"] - ] - height: 312 - clip: false - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.top: switchTabBar.bottom - anchors.topMargin: 24 - flow: GridView.FlowTopToBottom - cellWidth: (parent.width/(count/6)) - 8 - cellHeight: 52 - interactive: false - z: 100000 - cacheBuffer: 9999 - model: switchTabBar.currentItem.text.substring(0,2) - - function addWord(pos, word, ignoreGoingNext = false) { - mnemonicInput.push({pos: pos, seed: word.replace(/\s/g, '')}) - - for (let j = 0; j < mnemonicInput.length; j++) { - if (mnemonicInput[j].pos === pos && mnemonicInput[j].seed !== word) { - mnemonicInput[j].seed = word - break - } - } - //remove duplicates - const valueArr = mnemonicInput.map(item => item.pos) - const isDuplicate = valueArr.some((item, idx) => { - if (valueArr.indexOf(item) !== idx) { - root.mnemonicInput.splice(idx, 1) - } - return valueArr.indexOf(item) !== idx - }) - if (!ignoreGoingNext) { - for (let i = 0; i < grid.count; i++) { - if (grid.itemAtIndex(i).mnemonicIndex !== (pos + 1)) { - continue - } - - grid.currentIndex = grid.itemAtIndex(i).itemIndex - grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus() - - if (grid.currentIndex !== 12) { - continue - } - - grid.positionViewAtEnd() - - if (grid.count === 20) { - grid.contentX = 1500 - } - } - } - submitButton.checkMnemonicLength() - } - - delegate: StatusSeedPhraseInput { - id: seedWordInput - textEdit.input.edit.objectName: `statusSeedPhraseInputField${seedWordInput.leftComponentText}` - width: (grid.cellWidth - 8) - height: (grid.cellHeight - 8) - Behavior on width { NumberAnimation { duration: 180 } } - textEdit.text: { - const pos = seedWordInput.mnemonicIndex - for (let i in root.mnemonicInput) { - const p = root.mnemonicInput[i] - if (p.pos === pos) { - return p.seed - } - } - return "" - } - - readonly property int mnemonicIndex: grid.wordIndex[(grid.count / 6) - 2][index] - - leftComponentText: mnemonicIndex - inputList: BIP39_en {} - - property int itemIndex: index - z: (grid.currentIndex === index) ? 150000000 : 0 - onTextChanged: { - invalidSeedTxt.text = "" - } - onDoneInsertingWord: { - grid.addWord(mnemonicIndex, word) - } - onEditClicked: { - grid.currentIndex = index - grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus() - } - onKeyPressed: { - grid.currentIndex = index - - if (event.key === Qt.Key_Backtab) { - for (let i = 0; i < grid.count; i++) { - if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex - 1) >= 0 ? (mnemonicIndex - 1) : 0)) { - grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.BacktabFocusReason) - textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit - event.accepted = true - break - } - } - } else if (event.key === Qt.Key_Tab) { - for (let i = 0; i < grid.count; i++) { - if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex + 1) <= grid.count ? (mnemonicIndex + 1) : grid.count)) { - grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.TabFocusReason) - textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit - event.accepted = true - break - } - } - } - - if (event.matches(StandardKey.Paste)) { - if (root.pasteWords()) { - // Paste was done by splitting the words - event.accepted = true - } - return - } - - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - event.accepted = true - if (submitButton.enabled) { - submitButton.clicked(null) - return - } - } - - if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) { - const wordIndex = mnemonicInput.findIndex(x => x.pos === mnemonicIndex) - if (wordIndex > -1) { - mnemonicInput.splice(wordIndex, 1) - submitButton.checkMnemonicLength() - } - } - } - Component.onCompleted: { - const item = grid.itemAtIndex(0) - if (item) { - item.textEdit.input.edit.forceActiveFocus() - } - } - } - } - - StatusBaseText { - id: invalidSeedTxt - objectName: "onboardingInvalidSeedText" - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: grid.bottom - anchors.topMargin: 24 - color: Theme.palette.dangerColor1 - visible: text !== "" + EnterSeedPhrase { + id: seedPhraseView + isSeedPhraseValid: root.startupStore.validMnemonic + Layout.alignment: Qt.AlignHCenter } StatusButton { id: submitButton objectName: "seedPhraseViewSubmitButton" - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: invalidSeedTxt.bottom - anchors.topMargin: 24 - enabled: false - function checkMnemonicLength() { - submitButton.enabled = (root.mnemonicInput.length === root.tabs[switchTabBar.currentIndex]) - } + Layout.alignment: Qt.AlignHCenter + enabled: seedPhraseView.seedPhraseIsValid text: { if (root.startupStore.currentStartupState.flowType === Constants.startupFlow.firstRunNewUserImportSeedPhrase) { return qsTr("Import") @@ -322,21 +84,7 @@ Item { } return "" } - onClicked: { - let mnemonicString = "" - const sortTable = mnemonicInput.sort((a, b) => a.pos - b.pos) - for (let i = 0; i < mnemonicInput.length; i++) { - mnemonicString += sortTable[i].seed.toLowerCase() + ((i === (grid.count-1)) ? "" : " ") - } - - if (Utils.isMnemonic(mnemonicString) && root.startupStore.validMnemonic(mnemonicString)) { - root.mnemonicInput = [] - root.startupStore.doPrimaryAction() - } else { - invalidSeedTxt.text = qsTr("Invalid seed") - enabled = false - } - } + onClicked: root.startupStore.doPrimaryAction() } } } diff --git a/ui/app/AppLayouts/Onboarding/views/qmldir b/ui/app/AppLayouts/Onboarding/views/qmldir index cafd427e45..d37b11fb4d 100644 --- a/ui/app/AppLayouts/Onboarding/views/qmldir +++ b/ui/app/AppLayouts/Onboarding/views/qmldir @@ -1,2 +1,3 @@ LoginView 1.0 LoginView.qml -ProfileFetchingView 1.0 ProfileFetchingView.qml \ No newline at end of file +ProfileFetchingView 1.0 ProfileFetchingView.qml +SeedPhraseInputView 1.0 SeedPhraseInputView.qml \ No newline at end of file diff --git a/ui/imports/shared/panels/EnterSeedPhrase.qml b/ui/imports/shared/panels/EnterSeedPhrase.qml index 41a872ca91..f8450ac1a6 100644 --- a/ui/imports/shared/panels/EnterSeedPhrase.qml +++ b/ui/imports/shared/panels/EnterSeedPhrase.qml @@ -3,6 +3,7 @@ import QtQuick.Layouts 1.14 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 import StatusQ.Controls 0.1 import StatusQ.Controls.Validators 0.1 @@ -16,17 +17,26 @@ ColumnLayout { //************************************************************************** //* This component is not refactored, just pulled out to a shared location * //************************************************************************** - spacing: Style.current.padding clip: true + readonly property bool seedPhraseIsValid: d.allEntriesValid && invalidSeedTxt.text === "" property var isSeedPhraseValid: function (mnemonic) { return false } + property ListModel dictionary: BIP39_en {} signal submitSeedPhrase() signal seedPhraseUpdated(bool valid, string seedPhrase) function setWrongSeedPhraseMessage(message) { invalidSeedTxt.text = message + // Validate again the seed phrase + // This is needed because the message can be set to empty and the seed phrase is still invalid + if (message === "") + d.validate() + } + + function getSeedPhraseAsString() { + return d.buildMnemonicString() } QtObject { @@ -34,18 +44,18 @@ ColumnLayout { property bool allEntriesValid: false property var mnemonicInput: [] + property var incorrectWordAtIndex: [] readonly property var tabs: [12, 18, 24] - readonly property ListModel seedPhrases_en: BIP39_en {} + readonly property alias seedPhrases_en: root.dictionary + + onIncorrectWordAtIndexChanged: d.validate() onAllEntriesValidChanged: { let mnemonicString = "" - if (d.allEntriesValid) { - const sortTable = mnemonicInput.sort((a, b) => a.pos - b.pos) - for (let i = 0; i < mnemonicInput.length; i++) { - d.checkWordExistence(sortTable[i].seed) - mnemonicString += sortTable[i].seed + ((i === (grid.count-1)) ? "" : " ") - } + if (d.allEntriesValid) { + + mnemonicString = buildMnemonicString() if (!Utils.isMnemonic(mnemonicString) || !root.isSeedPhraseValid(mnemonicString)) { root.setWrongSeedPhraseMessage(qsTr("Invalid seed phrase")) d.allEntriesValid = false @@ -54,25 +64,43 @@ ColumnLayout { root.seedPhraseUpdated(d.allEntriesValid, mnemonicString) } - function checkMnemonicLength() { - d.allEntriesValid = d.mnemonicInput.length === d.tabs[switchTabBar.currentIndex] + function validate() { + if (d.incorrectWordAtIndex.length > 0) { + invalidSeedTxt.text = qsTr("The phrase you’ve entered is invalid") + return + } + + invalidSeedTxt.text = "" } - function checkWordExistence(word) { - d.allEntriesValid = d.allEntriesValid && d.seedPhrases_en.words.includes(word) - if (d.allEntriesValid) { - root.setWrongSeedPhraseMessage("") - } - else { - root.setWrongSeedPhraseMessage(qsTr("The phrase you’ve entered is invalid")) + function checkMnemonicLength() { + d.allEntriesValid = d.mnemonicInput.length === d.tabs[switchTabBar.currentIndex] && d.incorrectWordAtIndex.length === 0 + } + + function buildMnemonicString() { + const sortTable = mnemonicInput.sort((a, b) => a.pos - b.pos) + return sortTable.map(x => x.seed).join(" ") + } + + function checkWordExistence(word, pos) { + if (word !== "" && !ModelUtils.contains(d.seedPhrases_en, "seedWord", word)) { + const incorrectWordAtIndex = d.incorrectWordAtIndex + incorrectWordAtIndex.push(pos) + d.incorrectWordAtIndex = incorrectWordAtIndex + return } + + d.incorrectWordAtIndex = d.incorrectWordAtIndex.filter(function(value) { + return value !== pos + }) } function pasteWords () { - const clipboardText = globalUtils.getFromClipboard() + const clipboardText = Utils.getFromClipboard() + // Split words separated by commas and or blank spaces (spaces, enters, tabs) const words = clipboardText.trim().split(/[, \s]+/) - + let index = d.tabs.indexOf(words.length) if (index === -1) { return false @@ -118,7 +146,8 @@ ColumnLayout { StatusSwitchTabBar { id: switchTabBar - Layout.preferredWidth: parent.width + objectName: "enterSeedPhraseSwitchBar" + Layout.alignment: Qt.AlignHCenter Repeater { model: d.tabs StatusSwitchTabButton { @@ -131,6 +160,9 @@ ColumnLayout { d.mnemonicInput = d.mnemonicInput.filter(function(value) { return value.pos <= d.tabs[switchTabBar.currentIndex] }) + d.incorrectWordAtIndex = d.incorrectWordAtIndex.filter(function(value) { + return value <= d.tabs[switchTabBar.currentIndex] + }) d.checkMnemonicLength() } } @@ -144,42 +176,46 @@ ColumnLayout { ,["1", "5", "9", "13", "17", "21", "2", "6", "10", "14", "18", "22", "3", "7", "11", "15", "19", "23", "4", "8", "12", "16", "20", "24"] ] - Layout.preferredWidth: parent.width + + objectName: "enterSeedPhraseGridView" + Layout.fillWidth: true Layout.preferredHeight: 312 clip: false flow: GridView.FlowTopToBottom cellWidth: (parent.width/(count/6)) cellHeight: 52 interactive: false - z: 100000 - cacheBuffer: 9999 model: switchTabBar.currentItem.text.substring(0,2) function addWord(pos, word, ignoreGoingNext = false) { - d.mnemonicInput.push({pos: pos, seed: word.replace(/\s/g, '')}) - for (let j = 0; j < d.mnemonicInput.length; j++) { - if (d.mnemonicInput[j].pos === pos && d.mnemonicInput[j].seed !== word) { - d.mnemonicInput[j].seed = word + const words = d.mnemonicInput + + words.push({pos: pos, seed: word.replace(/\s/g, '')}) + + for (let j = 0; j < words.length; j++) { + if (words[j].pos === pos && words[j].seed !== word) { + words[j].seed = word break } } //remove duplicates - const valueArr = d.mnemonicInput.map(item => item.pos) + const valueArr = words.map(item => item.pos) const isDuplicate = valueArr.some((item, idx) => { if (valueArr.indexOf(item) !== idx) { - d.mnemonicInput.splice(idx, 1) + words.splice(idx, 1) } return valueArr.indexOf(item) !== idx }) if (!ignoreGoingNext) { for (let i = 0; i < grid.count; i++) { - if (grid.itemAtIndex(i).mnemonicIndex !== (pos + 1)) { + const item = grid.itemAtIndex(i) + if (!item || item.mnemonicIndex !== (pos + 1)) { continue } - grid.currentIndex = grid.itemAtIndex(i).itemIndex - grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus() + grid.currentIndex = item.itemIndex + item.textEdit.input.edit.forceActiveFocus() if (grid.currentIndex !== 12) { continue @@ -192,12 +228,16 @@ ColumnLayout { } } } + d.mnemonicInput = words + d.checkWordExistence(word, pos) d.checkMnemonicLength() + root.seedPhraseUpdated(d.allEntriesValid, d.buildMnemonicString()) } delegate: StatusSeedPhraseInput { id: seedWordInput - textEdit.input.edit.objectName: `statusSeedPhraseInputField${seedWordInput.leftComponentText}` + + textEdit.input.edit.objectName: `enterSeedPhraseInputField${seedWordInput.leftComponentText}` width: (grid.cellWidth - 8) height: (grid.cellHeight - 8) Behavior on width { NumberAnimation { duration: 180 } } @@ -218,13 +258,16 @@ ColumnLayout { inputList: d.seedPhrases_en property int itemIndex: index - z: (grid.currentIndex === index) ? 150000000 : 0 - onTextChanged: { - d.checkWordExistence(text) - } onDoneInsertingWord: { grid.addWord(mnemonicIndex, word) } + onEditingFinished: { + if (text === "") { + return + } + + grid.addWord(mnemonicIndex, text, true) + } onEditClicked: { grid.currentIndex = index grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus() @@ -287,7 +330,7 @@ ColumnLayout { StatusBaseText { id: invalidSeedTxt - visible: text !== "" + objectName: "enterSeedPhraseInvalidSeedText" Layout.alignment: Qt.AlignHCenter color: Theme.palette.dangerColor1 } diff --git a/ui/imports/shared/stores/BIP39_en.qml b/ui/imports/shared/stores/BIP39_en.qml index fa6941cc73..7fb49b05b2 100644 --- a/ui/imports/shared/stores/BIP39_en.qml +++ b/ui/imports/shared/stores/BIP39_en.qml @@ -1,23 +1,17 @@ import QtQuick 2.13 +import StatusQ.Core.Utils 0.1 + ListModel { id: root - property var words: [] - Component.onCompleted: { - var xhr = new XMLHttpRequest(); - xhr.open("GET", "english.txt"); - xhr.onreadystatechange = function () { - if (xhr.readyState === XMLHttpRequest.DONE) { - root.words = xhr.responseText.split('\n'); - for (var i = 0; i < root.words.length; i++) { - if (root.words[i] !== "") { - insert(count, {"seedWord": root.words[i]}); - } - } + const words = StringUtils.readTextFile(":/imports/shared/stores/english.txt").split(/\r?\n|\r/); + for (var i = 0; i < words.length; i++) { + let word = words[i] + if (word !== "") { + insert(count, {"seedWord": word}); } } - xhr.send(); } } diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index 2944e64790..1c514c5435 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -737,6 +737,10 @@ QtObject { globalUtilsInst.copyToClipboard(text) } + function getFromClipboard() { + return globalUtilsInst.getFromClipboard() + } + function copyImageToClipboardByUrl(content) { globalUtilsInst.copyImageToClipboardByUrl(content) }