status-desktop/ui/imports/shared/panels/EnterSeedPhrase.qml

338 lines
12 KiB
QML
Raw Normal View History

import QtQuick 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
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
import utils 1.0
import shared.stores 1.0
import shared.controls 1.0
ColumnLayout {
id: root
//**************************************************************************
//* This component is not refactored, just pulled out to a shared location *
//**************************************************************************
spacing: Theme.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 {
id: d
property bool allEntriesValid: false
property var mnemonicInput: []
property var incorrectWordAtIndex: []
readonly property var tabs: [12, 18, 24]
readonly property alias seedPhrases_en: root.dictionary
onIncorrectWordAtIndexChanged: d.validate()
onAllEntriesValidChanged: {
let mnemonicString = ""
if (d.allEntriesValid) {
mnemonicString = buildMnemonicString()
if (!Utils.isMnemonic(mnemonicString) || !root.isSeedPhraseValid(mnemonicString)) {
root.setWrongSeedPhraseMessage(qsTr("Invalid seed phrase"))
d.allEntriesValid = false
}
}
root.seedPhraseUpdated(d.allEntriesValid, mnemonicString)
}
function validate() {
if (d.incorrectWordAtIndex.length > 0) {
invalidSeedTxt.text = qsTr("The phrase youve entered is invalid")
return
}
invalidSeedTxt.text = ""
}
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 = ClipboardUtils.text
// 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
}
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
}
d.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])
}
d.checkMnemonicLength()
}, timeout)
return true
}
}
Timer {
id: timer
}
StatusSwitchTabBar {
id: switchTabBar
objectName: "enterSeedPhraseSwitchBar"
Layout.alignment: Qt.AlignHCenter
Repeater {
model: d.tabs
StatusSwitchTabButton {
text: qsTr("%n word(s)", "", modelData)
id: seedPhraseWords
objectName: `${modelData}SeedButton`
}
}
onCurrentIndexChanged: {
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()
}
}
StatusGridView {
id: grid
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"]
]
objectName: "enterSeedPhraseGridView"
Layout.fillWidth: true
Layout.preferredHeight: 312
clip: false
flow: GridView.FlowTopToBottom
cellWidth: (parent.width/(count/6))
cellHeight: 52
interactive: false
model: switchTabBar.currentItem.text.substring(0,2)
function addWord(pos, word, ignoreGoingNext = false) {
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 = words.map(item => item.pos)
const isDuplicate = valueArr.some((item, idx) => {
if (valueArr.indexOf(item) !== idx) {
words.splice(idx, 1)
}
return valueArr.indexOf(item) !== idx
})
if (!ignoreGoingNext) {
for (let i = 0; i < grid.count; i++) {
const item = grid.itemAtIndex(i)
if (!item || item.mnemonicIndex !== (pos + 1)) {
continue
}
grid.currentIndex = item.itemIndex
item.textEdit.input.edit.forceActiveFocus()
if (grid.currentIndex !== 12) {
continue
}
grid.positionViewAtEnd()
if (grid.count === 20) {
grid.contentX = 1500
}
}
}
d.mnemonicInput = words
d.checkWordExistence(word, pos)
d.checkMnemonicLength()
root.seedPhraseUpdated(d.allEntriesValid, d.buildMnemonicString())
}
delegate: StatusSeedPhraseInput {
id: seedWordInput
textEdit.input.edit.objectName: `enterSeedPhraseInputField${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 d.mnemonicInput) {
const p = d.mnemonicInput[i]
if (p.pos === pos) {
return p.seed
}
}
return ""
}
readonly property int mnemonicIndex: grid.wordIndex[(grid.count / 6) - 2][index]
leftComponentText: mnemonicIndex
inputList: d.seedPhrases_en
property int itemIndex: index
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()
}
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 (d.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 (d.allEntriesValid) {
root.submitSeedPhrase()
return
}
}
if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) {
const wordIndex = d.mnemonicInput.findIndex(x => x.pos === mnemonicIndex)
if (wordIndex > -1) {
d.mnemonicInput.splice(wordIndex, 1)
d.checkMnemonicLength()
}
}
}
Component.onCompleted: {
const item = grid.itemAtIndex(0)
if (item) {
item.textEdit.input.edit.forceActiveFocus()
}
}
}
}
StatusBaseText {
id: invalidSeedTxt
objectName: "enterSeedPhraseInvalidSeedText"
Layout.alignment: Qt.AlignHCenter
color: Theme.palette.dangerColor1
}
}