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
This commit is contained in:
Alex Jbanca 2024-02-14 15:50:14 +02:00 committed by GitHub
parent 46e256673c
commit 8a69f3bc63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 777 additions and 435 deletions

View File

@ -0,0 +1,14 @@
import QtQuick 2.15
import shared.panels 1.0
import Storybook 1.0
Item {
EnterSeedPhrase {
anchors.centerIn: parent
}
}
// category: Panels

View File

@ -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

View File

@ -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")
}
}
}

View File

@ -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] });
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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"}
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"}

View File

@ -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}

View File

@ -4,8 +4,8 @@
#include <QObject>
#include <QString>
#include <QVariant>
#include <QAbstractItemModel>
class QAbstractItemModel;
class QJSEngine;
class QQmlEngine;

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -0,0 +1,2 @@
AccountViewDelegate 1.0 AccountViewDelegate.qml
OnboardingBasePage 1.0 OnboardingBasePage.qml

View File

@ -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 doesnt match the profile of an existing Keycard user on this device")
seedPhraseView.setWrongSeedPhraseMessage(qsTr("Seed phrase doesnt 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()
}
}
}

View File

@ -1,2 +1,3 @@
LoginView 1.0 LoginView.qml
ProfileFetchingView 1.0 ProfileFetchingView.qml
SeedPhraseInputView 1.0 SeedPhraseInputView.qml

View File

@ -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,22 +64,40 @@ 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 youve 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 youve 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]+/)
@ -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
}

View File

@ -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();
}
}

View File

@ -737,6 +737,10 @@ QtObject {
globalUtilsInst.copyToClipboard(text)
}
function getFromClipboard() {
return globalUtilsInst.getFromClipboard()
}
function copyImageToClipboardByUrl(content) {
globalUtilsInst.copyImageToClipboardByUrl(content)
}