feat(Onboarding): Create Profile & Login flows
- implement the basic Onboarding UI skeleton and the Create Profile flows - adjust the PasswordView and EnterSeedPhrase views to the latest design - add the main OnboardingLayout and StatusPinInput pages to Storybook - change terminology app-wide: "Seed phrase" -> "Recovery phrase" - implement the Login flows (seed, sync, keycard) - amend the keycard flow sequences with separate (non) empty page Fixes #16719 Fixes #16742 Fixes #16743
@ -17,7 +17,7 @@ import Status.Core.Theme
|
||||
width: 240
|
||||
text: qsTr("Hello World!")
|
||||
font.pixelSize: 24
|
||||
color: Theme.pallete.directColor1
|
||||
color: Theme.palette.directColor1
|
||||
}
|
||||
\endqml
|
||||
|
||||
|
141
storybook/pages/BackupSeedphraseFlowPage.qml
Normal file
@ -0,0 +1,141 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Utils 0.1 as SQUtils
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
import utils 1.0
|
||||
|
||||
import AppLayouts.Onboarding2.pages 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
readonly property var seedWords: ["apple", "banana", "cat", "cow", "catalog", "catch", "category", "cattle", "dog", "elephant", "fish", "grape"]
|
||||
readonly property int numWordsToVerify: 4
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stack
|
||||
anchors.fill: parent
|
||||
initialItem: backupSeedIntroPage
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.BackButton
|
||||
enabled: stack.depth > 1 && !stack.busy
|
||||
cursorShape: undefined // fall thru
|
||||
onClicked: stack.pop()
|
||||
}
|
||||
|
||||
StatusBackButton {
|
||||
width: 44
|
||||
height: 44
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padding
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Theme.padding
|
||||
opacity: stack.depth > 1 && !stack.busy ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity { NumberAnimation { duration: 100 } }
|
||||
onClicked: stack.pop()
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
text: !!stack.currentItem && stack.currentItem.pageClassName === "BackupSeedphraseVerify" ?
|
||||
"Hint: %1".arg(stack.currentItem.seedWordsToVerify.map((entry) => entry.seedWord))
|
||||
: ""
|
||||
}
|
||||
|
||||
Connections {
|
||||
id: mainHandler
|
||||
target: stack.currentItem
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onBackupSeedphraseRequested() {
|
||||
stack.push(backupSeedAcksPage)
|
||||
}
|
||||
|
||||
function onBackupSeedphraseContinue() {
|
||||
stack.push(backupSeedRevealPage)
|
||||
}
|
||||
|
||||
function onBackupSeedphraseConfirmed() {
|
||||
stack.push(backupSeedVerifyPage)
|
||||
}
|
||||
|
||||
function onBackupSeedphraseVerified() {
|
||||
stack.push(backupSeedOutroPage)
|
||||
}
|
||||
|
||||
function onBackupSeedphraseRemovalConfirmed() {
|
||||
console.warn("!!! FLOW FINISHED; RESTART")
|
||||
stack.pop(null)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: backupSeedIntroPage
|
||||
BackupSeedphraseIntro {
|
||||
onBackupSeedphraseRequested: console.warn("!!! SEED BACKUP REQUESTED")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: backupSeedAcksPage
|
||||
BackupSeedphraseAcks {
|
||||
onBackupSeedphraseContinue: console.warn("!!! SEED ACKED")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: backupSeedRevealPage
|
||||
BackupSeedphraseReveal {
|
||||
seedWords: d.seedWords
|
||||
onBackupSeedphraseConfirmed: console.warn("!!! SEED CONFIRMED")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: backupSeedVerifyPage
|
||||
BackupSeedphraseVerify {
|
||||
seedWordsToVerify: {
|
||||
let result = []
|
||||
const randomIndexes = SQUtils.Utils.nSamples(d.numWordsToVerify, d.seedWords.length)
|
||||
for (const i of randomIndexes) {
|
||||
result.push({seedWordNumber: i+1, seedWord: d.seedWords[i]})
|
||||
}
|
||||
return result
|
||||
}
|
||||
onBackupSeedphraseVerified: console.warn("!!! ALL VERIFIED")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: backupSeedOutroPage
|
||||
BackupSeedphraseOutro {
|
||||
onBackupSeedphraseRemovalConfirmed: console.warn("!!! SEED REMOVAL CONFIRMED")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Onboarding
|
||||
// status: good
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=944-40428&node-type=instance&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=944-40730&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=522-36751&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=522-37165&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=783-33987&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=944-44817&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=783-34183&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=944-44231&node-type=frame&m=dev
|
@ -146,6 +146,10 @@ SplitView {
|
||||
enabled: searchField.searchText !== ""
|
||||
onClicked: searchField.clear()
|
||||
}
|
||||
Label {
|
||||
text: "INFO: Reload the page after selecting 'Dark mode'"
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
ColorFlow {
|
||||
|
@ -19,23 +19,29 @@ SplitView {
|
||||
SplitView.fillHeight: true
|
||||
SplitView.fillWidth: true
|
||||
progress: progressSlider.position
|
||||
messagesEnabled: ctrlMessagesEnabled.checked
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
SplitView.minimumWidth: 300
|
||||
SplitView.preferredWidth: 300
|
||||
RowLayout {
|
||||
Label {
|
||||
text: "Progress"
|
||||
}
|
||||
Slider {
|
||||
SplitView.minimumWidth: 300
|
||||
SplitView.preferredWidth: 300
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: "Progress"
|
||||
}
|
||||
Slider {
|
||||
id: progressSlider
|
||||
}
|
||||
}
|
||||
}
|
||||
Switch {
|
||||
id: ctrlMessagesEnabled
|
||||
text: "Messages enabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Panels
|
||||
|
||||
// status: good
|
||||
// https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba⎜Desktop?node-id=25878%3A518438&t=C7xTpNib38t7s7XU-4
|
||||
|
44
storybook/pages/KeycardAddKeyPairPagePage.qml
Normal file
@ -0,0 +1,44 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import StatusQ.Core.Backpressure 0.1
|
||||
|
||||
import AppLayouts.Onboarding2.pages 1.0
|
||||
import AppLayouts.Onboarding.enums 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
KeycardAddKeyPairPage {
|
||||
id: progressPage
|
||||
anchors.fill: parent
|
||||
addKeyPairState: Onboarding.AddKeyPairState.InProgress
|
||||
timeoutInterval: 5000
|
||||
onKeypairAddTryAgainRequested: {
|
||||
console.warn("!!! onKeypairAddTryAgainRequested")
|
||||
addKeyPairState = Onboarding.AddKeyPairState.InProgress
|
||||
Backpressure.debounce(root, 2000, function() {
|
||||
console.warn("!!! SIMULATION: SUCCESS")
|
||||
addKeyPairState = Onboarding.AddKeyPairState.Success
|
||||
})()
|
||||
}
|
||||
onKeypairAddContinueRequested: console.warn("!!! onKeypairAddContinueRequested")
|
||||
onReloadKeycardRequested: console.warn("!!! onReloadKeycardRequested")
|
||||
onCreateProfilePageRequested: console.warn("!!! onCreateProfilePageRequested")
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: ctrlState
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
width: 350
|
||||
model: ["Onboarding.AddKeyPairState.InProgress", "Onboarding.AddKeyPairState.Success", "Onboarding.AddKeyPairState.Failed"]
|
||||
onCurrentIndexChanged: progressPage.addKeyPairState = currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
// category: Onboarding
|
||||
// status: good
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1305-48023&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1305-48081&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1305-48102&node-type=frame&m=dev
|
20
storybook/pages/KeycardCreatePinPagePage.qml
Normal file
@ -0,0 +1,20 @@
|
||||
import QtQuick 2.15
|
||||
|
||||
import AppLayouts.Onboarding2.pages 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
KeycardCreatePinPage {
|
||||
anchors.fill: parent
|
||||
onKeycardPinCreated: (pin) => console.warn("!!! PIN CREATED:", pin)
|
||||
}
|
||||
}
|
||||
|
||||
// category: Onboarding
|
||||
// status: good
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=595-57785&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=595-57989&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=595-58027&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=507-34789&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1053-53693&node-type=frame&m=dev
|
51
storybook/pages/KeycardEnterPinPagePage.qml
Normal file
@ -0,0 +1,51 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import AppLayouts.Onboarding2.pages 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property string existingPin: "111111"
|
||||
|
||||
KeycardEnterPinPage {
|
||||
id: page
|
||||
anchors.fill: parent
|
||||
tryToSetPinFunction: (pin) => {
|
||||
const valid = pin === root.existingPin
|
||||
if (!valid)
|
||||
remainingAttempts--
|
||||
return valid
|
||||
}
|
||||
remainingAttempts: 3
|
||||
onKeycardPinEntered: (pin) => {
|
||||
console.warn("!!! PIN:", pin)
|
||||
console.warn("!!! RESETTING FLOW")
|
||||
state = "entering"
|
||||
}
|
||||
onReloadKeycardRequested: {
|
||||
console.warn("!!! RELOAD KEYCARD")
|
||||
remainingAttempts--
|
||||
state = "entering"
|
||||
}
|
||||
onKeycardFactoryResetRequested: {
|
||||
console.warn("!!! FACTORY RESET KEYCARD")
|
||||
remainingAttempts = 3
|
||||
state = "entering"
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
text: "Hint: %1".arg(root.existingPin)
|
||||
}
|
||||
}
|
||||
|
||||
// category: Onboarding
|
||||
// status: good
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1281-45942&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1281-45950&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1281-45959&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1281-45966&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1281-45996&node-type=frame&m=dev
|
104
storybook/pages/KeycardIntroPagePage.qml
Normal file
@ -0,0 +1,104 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import AppLayouts.Onboarding2.pages 1.0
|
||||
import AppLayouts.Onboarding.enums 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
sourceComponent: {
|
||||
switch (ctrlKeycardState.currentValue) {
|
||||
case Onboarding.KeycardState.Empty: return emptyPage
|
||||
case Onboarding.KeycardState.NotEmpty: return notEmptyPage
|
||||
default: introPage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: introPage
|
||||
KeycardIntroPage {
|
||||
keycardState: ctrlKeycardState.currentValue
|
||||
displayPromoBanner: ctrlDisplayPromo.checked
|
||||
onEmptyKeycardDetected: console.warn("!!! EMPTY DETECTED")
|
||||
onNotEmptyKeycardDetected: console.warn("!!! NOT EMPTY DETECTED")
|
||||
onReloadKeycardRequested: console.warn("!!! RELOAD REQUESTED")
|
||||
onOpenLink: Qt.openUrlExternally(link)
|
||||
onOpenLinkWithConfirmation: Qt.openUrlExternally(link)
|
||||
onKeycardFactoryResetRequested: console.warn("!!! FACTORY RESET")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: emptyPage
|
||||
KeycardEmptyPage {
|
||||
onCreateProfileWithEmptyKeycardRequested: console.warn("!!! CREATE NEW PROFILE")
|
||||
onReloadKeycardRequested: console.warn("!!! RELOAD REQUESTED")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notEmptyPage
|
||||
KeycardNotEmptyPage {
|
||||
onReloadKeycardRequested: console.warn("!!! RELOAD REQUESTED")
|
||||
onLoginWithThisKeycardRequested: console.warn("!!! LOGIN REQUESTED")
|
||||
onKeycardFactoryResetRequested: console.warn("!!! FACTORY RESET")
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
CheckBox {
|
||||
id: ctrlDisplayPromo
|
||||
text: "Promo banner"
|
||||
checked: true
|
||||
visible: ctrlKeycardState.currentValue === Onboarding.KeycardState.InsertKeycard
|
||||
}
|
||||
ToolButton {
|
||||
text: "<"
|
||||
onClicked: ctrlKeycardState.decrementCurrentIndex()
|
||||
}
|
||||
ComboBox {
|
||||
id: ctrlKeycardState
|
||||
|
||||
focusPolicy: Qt.NoFocus
|
||||
Layout.preferredWidth: 250
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
model: [
|
||||
{ value: Onboarding.KeycardState.NoPCSCService, text: "NoPCSCService" },
|
||||
{ value: Onboarding.KeycardState.PluginReader, text: "PluginReader" },
|
||||
{ value: Onboarding.KeycardState.InsertKeycard, text: "InsertKeycard" },
|
||||
{ value: Onboarding.KeycardState.ReadingKeycard, text: "ReadingKeycard" },
|
||||
{ value: Onboarding.KeycardState.WrongKeycard, text: "WrongKeycard" },
|
||||
{ value: Onboarding.KeycardState.NotKeycard, text: "NotKeycard" },
|
||||
{ value: Onboarding.KeycardState.MaxPairingSlotsReached, text: "MaxPairingSlotsReached" },
|
||||
{ value: Onboarding.KeycardState.Locked, text: "Locked" },
|
||||
{ value: Onboarding.KeycardState.NotEmpty, text: "NotEmpty" },
|
||||
{ value: Onboarding.KeycardState.Empty, text: "Empty" }
|
||||
]
|
||||
}
|
||||
ToolButton {
|
||||
text: ">"
|
||||
onClicked: ctrlKeycardState.incrementCurrentIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Onboarding
|
||||
// status: good
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=507-34558&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=507-34583&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=507-34608&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=595-57486&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=595-57709&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=972-44743&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=972-44633&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=972-44611&node-type=frame&m=dev
|
266
storybook/pages/OnboardingLayoutPage.qml
Normal file
@ -0,0 +1,266 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQml 2.15
|
||||
|
||||
import StatusQ 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
import Models 1.0
|
||||
import Storybook 1.0
|
||||
|
||||
import utils 1.0
|
||||
|
||||
import AppLayouts.Onboarding2 1.0
|
||||
import AppLayouts.Onboarding2.stores 1.0
|
||||
import AppLayouts.Onboarding.enums 1.0
|
||||
|
||||
import shared.panels 1.0
|
||||
import shared.stores 1.0 as SharedStores
|
||||
|
||||
SplitView {
|
||||
id: root
|
||||
orientation: Qt.Vertical
|
||||
|
||||
Logs { id: logs }
|
||||
|
||||
QtObject {
|
||||
id: mockDriver
|
||||
readonly property string mnemonic: "dog dog dog dog dog dog dog dog dog dog dog dog"
|
||||
readonly property var seedWords: ["apple", "banana", "cat", "cow", "catalog", "catch", "category", "cattle", "dog", "elephant", "fish", "grape"]
|
||||
|
||||
// TODO simulation
|
||||
function restart() {
|
||||
// add keypair state
|
||||
// sync state
|
||||
}
|
||||
}
|
||||
|
||||
OnboardingLayout {
|
||||
id: onboarding
|
||||
SplitView.fillWidth: true
|
||||
SplitView.fillHeight: true
|
||||
networkChecksEnabled: true
|
||||
onboardingStore: OnboardingStore {
|
||||
readonly property int keycardState: ctrlKeycardState.currentValue // enum Onboarding.KeycardState
|
||||
property int keycardRemainingPinAttempts: 5
|
||||
|
||||
function setPin(pin: string) { // -> bool
|
||||
logs.logEvent("OnboardingStore.setPin", ["pin"], arguments)
|
||||
const valid = pin === ctrlPin.text
|
||||
if (!valid)
|
||||
keycardRemainingPinAttempts--
|
||||
return valid
|
||||
}
|
||||
|
||||
property int addKeyPairState // enum Onboarding.AddKeyPairState
|
||||
function startKeypairTransfer() { // -> void
|
||||
logs.logEvent("OnboardingStore.startKeypairTransfer")
|
||||
addKeyPairState = Onboarding.AddKeyPairState.InProgress
|
||||
}
|
||||
|
||||
// password
|
||||
function getPasswordStrengthScore(password: string) { // -> int
|
||||
logs.logEvent("OnboardingStore.getPasswordStrengthScore", ["password"], arguments)
|
||||
return Math.min(password.length-1, 4)
|
||||
}
|
||||
|
||||
// seedphrase/mnemonic
|
||||
function validMnemonic(mnemonic: string) { // -> bool
|
||||
logs.logEvent("OnboardingStore.validMnemonic", ["mnemonic"], arguments)
|
||||
return mnemonic === mockDriver.mnemonic
|
||||
}
|
||||
function getMnemonic() { // -> string
|
||||
logs.logEvent("OnboardingStore.getMnemonic()")
|
||||
return mockDriver.seedWords.join(" ")
|
||||
}
|
||||
function mnemonicWasShown() { // -> void
|
||||
logs.logEvent("OnboardingStore.mnemonicWasShown()")
|
||||
}
|
||||
function removeMnemonic() { // -> void
|
||||
logs.logEvent("OnboardingStore.removeMnemonic()")
|
||||
}
|
||||
|
||||
readonly property int syncState: Onboarding.SyncState.InProgress // enum Onboarding.SyncState
|
||||
function validateLocalPairingConnectionString(connectionString: string) { // -> bool
|
||||
logs.logEvent("OnboardingStore.validateLocalPairingConnectionString", ["connectionString"], arguments)
|
||||
return !Number.isNaN(parseInt(connectionString))
|
||||
}
|
||||
function inputConnectionStringForBootstrapping(connectionString: string) { // -> void
|
||||
logs.logEvent("OnboardingStore.inputConnectionStringForBootstrapping", ["connectionString"], arguments)
|
||||
}
|
||||
}
|
||||
|
||||
metricsStore: SharedStores.MetricsStore {
|
||||
readonly property var d: QtObject {
|
||||
id: d
|
||||
property bool isCentralizedMetricsEnabled
|
||||
}
|
||||
|
||||
function toggleCentralizedMetrics(enabled) {
|
||||
d.isCentralizedMetricsEnabled = enabled
|
||||
}
|
||||
|
||||
function addCentralizedMetricIfEnabled(eventName, eventValue = null) {}
|
||||
|
||||
readonly property bool isCentralizedMetricsEnabled : d.isCentralizedMetricsEnabled
|
||||
}
|
||||
|
||||
splashScreenDurationMs: 3000
|
||||
biometricsAvailable: ctrlBiometrics.checked
|
||||
|
||||
QtObject {
|
||||
id: localAppSettings
|
||||
property bool metricsPopupSeen
|
||||
}
|
||||
|
||||
onFinished: (primaryFlow, secondaryFlow, data) => {
|
||||
console.warn("!!! ONBOARDING FINISHED; primary flow:", primaryFlow, "; secondary:", secondaryFlow, "; data:", JSON.stringify(data))
|
||||
logs.logEvent("onFinished", ["primaryFlow", "secondaryFlow", "data"], arguments)
|
||||
|
||||
console.warn("!!! SIMULATION: SHOWING SPLASH")
|
||||
stack.clear()
|
||||
stack.push(splashScreen, { runningProgressAnimation: true })
|
||||
ctrlKeycardState.currentIndex = 0
|
||||
}
|
||||
onKeycardFactoryResetRequested: {
|
||||
logs.logEvent("onKeycardFactoryResetRequested")
|
||||
console.warn("!!! FACTORY RESET; RESTARTING FLOW")
|
||||
restartFlow()
|
||||
ctrlKeycardState.currentIndex = 0
|
||||
}
|
||||
onKeycardReloaded: {
|
||||
logs.logEvent("onKeycardReloaded")
|
||||
console.warn("!!! RELOAD KEYCARD")
|
||||
ctrlKeycardState.currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: splashScreen
|
||||
DidYouKnowSplashScreen {
|
||||
readonly property string pageClassName: "Splash"
|
||||
property bool runningProgressAnimation
|
||||
NumberAnimation on progress {
|
||||
from: 0.0
|
||||
to: 1
|
||||
duration: onboarding.splashScreenDurationMs
|
||||
running: runningProgressAnimation
|
||||
onStopped: {
|
||||
console.warn("!!! SPLASH SCREEN DONE")
|
||||
console.warn("!!! RESTARTING FLOW")
|
||||
onboarding.restartFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Global
|
||||
function onOpenLink(link: string) {
|
||||
console.warn("Opening link in an external web browser:", link)
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
function onOpenLinkWithConfirmation(link: string, domain: string) {
|
||||
console.warn("Opening link in an external web browser:", link, domain)
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
|
||||
LogsAndControlsPanel {
|
||||
id: logsAndControlsPanel
|
||||
|
||||
SplitView.minimumHeight: 150
|
||||
SplitView.preferredHeight: 150
|
||||
|
||||
logsView.logText: logs.logText
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: "Current page: %1".arg(onboarding.stack.currentItem ? onboarding.stack.currentItem.pageClassName : "")
|
||||
}
|
||||
Label {
|
||||
text: `Current flow: ${onboarding.primaryFlow} -> ${onboarding.secondaryFlow}`
|
||||
}
|
||||
Label {
|
||||
text: "Stack depth: %1".arg(onboarding.stack.depth)
|
||||
}
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
Button {
|
||||
text: "Restart"
|
||||
focusPolicy: Qt.NoFocus
|
||||
onClicked: onboarding.restartFlow()
|
||||
}
|
||||
Button {
|
||||
text: "Copy password"
|
||||
focusPolicy: Qt.NoFocus
|
||||
onClicked: ClipboardUtils.setText("0123456789")
|
||||
}
|
||||
Button {
|
||||
text: "Copy seedphrase"
|
||||
focusPolicy: Qt.NoFocus
|
||||
onClicked: ClipboardUtils.setText(mockDriver.mnemonic)
|
||||
}
|
||||
Button {
|
||||
text: "Copy PIN (\"%1\")".arg(ctrlPin.text)
|
||||
focusPolicy: Qt.NoFocus
|
||||
enabled: ctrlPin.acceptableInput
|
||||
onClicked: ClipboardUtils.setText(ctrlPin.text)
|
||||
}
|
||||
Switch {
|
||||
id: ctrlBiometrics
|
||||
text: "Biometrics?"
|
||||
checked: true
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Label {
|
||||
text: "Keycard PIN:"
|
||||
}
|
||||
TextField {
|
||||
id: ctrlPin
|
||||
text: "111111"
|
||||
inputMask: "999999"
|
||||
}
|
||||
Label {
|
||||
text: "State:"
|
||||
}
|
||||
ComboBox {
|
||||
Layout.preferredWidth: 300
|
||||
id: ctrlKeycardState
|
||||
focusPolicy: Qt.NoFocus
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
model: [
|
||||
{ value: Onboarding.KeycardState.NoPCSCService, text: "NoPCSCService" },
|
||||
{ value: Onboarding.KeycardState.PluginReader, text: "PluginReader" },
|
||||
{ value: Onboarding.KeycardState.InsertKeycard, text: "InsertKeycard" },
|
||||
{ value: Onboarding.KeycardState.ReadingKeycard, text: "ReadingKeycard" },
|
||||
{ value: Onboarding.KeycardState.WrongKeycard, text: "WrongKeycard" },
|
||||
{ value: Onboarding.KeycardState.NotKeycard, text: "NotKeycard" },
|
||||
{ value: Onboarding.KeycardState.MaxPairingSlotsReached, text: "MaxPairingSlotsReached" },
|
||||
{ value: Onboarding.KeycardState.Locked, text: "Locked" },
|
||||
{ value: Onboarding.KeycardState.NotEmpty, text: "NotEmpty" },
|
||||
{ value: Onboarding.KeycardState.Empty, text: "Empty" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Onboarding
|
||||
// status: good
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1-25&node-type=canvas&m=dev
|
41
storybook/pages/StatusPinInputPage.qml
Normal file
@ -0,0 +1,41 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Controls.Validators 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 16
|
||||
StatusBaseText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "ENTER NUMERIC PIN, EXPECTED LENGTH: %1".arg(pinInput.pinLen)
|
||||
}
|
||||
StatusPinInput {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
id: pinInput
|
||||
validator: StatusIntValidator { bottom: 0; top: 999999 }
|
||||
Component.onCompleted: {
|
||||
statesInitialization()
|
||||
forceFocus()
|
||||
}
|
||||
}
|
||||
StatusBaseText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "ENTERED PIN: %1".arg(pinInput.pinInput || "[empty]")
|
||||
}
|
||||
StatusBaseText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "VALID: %1".arg(pinInput.valid ? "true" : "false")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Controls
|
||||
// status: good
|
43
storybook/pages/SyncProgressPagePage.qml
Normal file
@ -0,0 +1,43 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import StatusQ.Core.Backpressure 0.1
|
||||
|
||||
import AppLayouts.Onboarding2.pages 1.0
|
||||
import AppLayouts.Onboarding.enums 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
SyncProgressPage {
|
||||
id: progressPage
|
||||
anchors.fill: parent
|
||||
syncState: Onboarding.SyncState.InProgress
|
||||
timeoutInterval: 5000
|
||||
onRestartSyncRequested: {
|
||||
console.warn("!!! RESTART SYNC REQUESTED")
|
||||
syncState = Onboarding.SyncState.InProgress
|
||||
Backpressure.debounce(root, 2000, function() {
|
||||
console.warn("!!! SIMULATION: SUCCESS")
|
||||
syncState = Onboarding.SyncState.Success
|
||||
})()
|
||||
}
|
||||
onLoginToAppRequested: console.warn("!!! LOGIN TO APP REQUESTED")
|
||||
onLoginWithSeedphraseRequested: console.warn("!!! LOGIN WITH SEEDPHRASE REQUESTED")
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: ctrlState
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
width: 300
|
||||
model: ["Onboarding.SyncState.InProgress", "Onboarding.SyncState.Success", "Onboarding.SyncState.Failed"]
|
||||
onCurrentIndexChanged: progressPage.syncState = currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
// category: Onboarding
|
||||
// status: good
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=221-23716&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=224-20891&node-type=frame&m=dev
|
||||
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=221-23788&node-type=frame&m=dev
|
@ -7,6 +7,7 @@ import Storybook 1.0
|
||||
import mainui 1.0
|
||||
import shared.views 1.0
|
||||
import shared.stores 1.0 as SharedStores
|
||||
import shared.popups 1.0
|
||||
|
||||
import AppLayouts.stores 1.0 as AppLayoutStores
|
||||
|
||||
@ -33,9 +34,19 @@ SplitView {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
validateConnectionString: (stringValue) => !Number.isNaN(parseInt(stringValue))
|
||||
|
||||
onDisplayInstructions: logs.logEvent("SyncingEnterCode::displayInstructions")
|
||||
onDisplayInstructions: {
|
||||
logs.logEvent("SyncingEnterCode::displayInstructions")
|
||||
instructionsPopup.createObject(root).open()
|
||||
}
|
||||
onProceed: (connectionString) => logs.logEvent("SyncingEnterCode::proceed", ["connectionString"], arguments)
|
||||
}
|
||||
|
||||
Component {
|
||||
id: instructionsPopup
|
||||
GetSyncCodeInstructionsPopup {
|
||||
destroyOnClose: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogsAndControlsPanel {
|
||||
|
885
storybook/qmlTests/tests/tst_OnboardingLayout.qml
Normal file
@ -0,0 +1,885 @@
|
||||
import QtQuick 2.15
|
||||
import QtTest 1.15
|
||||
|
||||
import StatusQ 0.1 // ClipboardUtils
|
||||
|
||||
import AppLayouts.Onboarding2 1.0
|
||||
import AppLayouts.Onboarding2.pages 1.0
|
||||
import AppLayouts.Onboarding2.stores 1.0
|
||||
import AppLayouts.Onboarding.enums 1.0
|
||||
|
||||
import shared.stores 1.0 as SharedStores
|
||||
|
||||
import utils 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
width: 1200
|
||||
height: 700
|
||||
|
||||
QtObject {
|
||||
id: mockDriver
|
||||
property int keycardState // enum Onboarding.KeycardState
|
||||
property bool biometricsAvailable
|
||||
property string existingPin
|
||||
|
||||
readonly property string mnemonic: "dog dog dog dog dog dog dog dog dog dog dog dog"
|
||||
readonly property var seedWords: ["apple", "banana", "cat", "cow", "catalog", "catch", "category", "cattle", "dog", "elephant", "fish", "grape"]
|
||||
readonly property string dummyNewPassword: "0123456789"
|
||||
}
|
||||
|
||||
Component {
|
||||
id: componentUnderTest
|
||||
|
||||
OnboardingLayout {
|
||||
anchors.fill: parent
|
||||
networkChecksEnabled: false
|
||||
onboardingStore: OnboardingStore {
|
||||
readonly property int keycardState: mockDriver.keycardState // enum Onboarding.KeycardState
|
||||
property int keycardRemainingPinAttempts: 5
|
||||
|
||||
function setPin(pin: string) {
|
||||
const valid = pin === mockDriver.existingPin
|
||||
if (!valid)
|
||||
keycardRemainingPinAttempts--
|
||||
return valid
|
||||
}
|
||||
|
||||
readonly property int addKeyPairState: Onboarding.AddKeyPairState.InProgress // enum Onboarding.AddKeyPairState
|
||||
function startKeypairTransfer() {}
|
||||
|
||||
// password
|
||||
function getPasswordStrengthScore(password: string) {
|
||||
return Math.min(password.length-1, 4)
|
||||
}
|
||||
|
||||
// seedphrase/mnemonic
|
||||
function validMnemonic(mnemonic: string) {
|
||||
return mnemonic === mockDriver.mnemonic
|
||||
}
|
||||
function getMnemonic() {
|
||||
return mockDriver.seedWords.join(" ")
|
||||
}
|
||||
function mnemonicWasShown() {}
|
||||
function removeMnemonic() {}
|
||||
|
||||
readonly property int syncState: Onboarding.SyncState.InProgress // enum Onboarding.SyncState
|
||||
function validateLocalPairingConnectionString(connectionString: string) {
|
||||
return !Number.isNaN(parseInt(connectionString))
|
||||
}
|
||||
function inputConnectionStringForBootstrapping(connectionString: string) {}
|
||||
}
|
||||
metricsStore: SharedStores.MetricsStore {
|
||||
readonly property var d: QtObject {
|
||||
id: d
|
||||
property bool isCentralizedMetricsEnabled
|
||||
}
|
||||
|
||||
function toggleCentralizedMetrics(enabled) {
|
||||
d.isCentralizedMetricsEnabled = enabled
|
||||
}
|
||||
|
||||
function addCentralizedMetricIfEnabled(eventName, eventValue = null) {}
|
||||
|
||||
readonly property bool isCentralizedMetricsEnabled : d.isCentralizedMetricsEnabled
|
||||
}
|
||||
|
||||
splashScreenDurationMs: 3000
|
||||
biometricsAvailable: mockDriver.biometricsAvailable
|
||||
|
||||
QtObject {
|
||||
id: localAppSettings
|
||||
property bool metricsPopupSeen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: dynamicSpy
|
||||
|
||||
function setup(t, s) {
|
||||
clear()
|
||||
target = t
|
||||
signalName = s
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
target = null
|
||||
signalName = ""
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: finishedSpy
|
||||
target: controlUnderTest
|
||||
signalName: "finished"
|
||||
}
|
||||
|
||||
property OnboardingLayout controlUnderTest: null
|
||||
|
||||
TestCase {
|
||||
name: "OnboardingLayout"
|
||||
when: windowShown
|
||||
|
||||
function init() {
|
||||
controlUnderTest = createTemporaryObject(componentUnderTest, root)
|
||||
|
||||
// disable animated transitions to speed-up tests
|
||||
const stack = findChild(controlUnderTest, "stack")
|
||||
stack.pushEnter = null
|
||||
stack.pushExit = null
|
||||
stack.popEnter = null
|
||||
stack.popExit = null
|
||||
stack.replaceEnter = null
|
||||
stack.replaceExit = null
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
mockDriver.keycardState = -1
|
||||
mockDriver.biometricsAvailable = false
|
||||
mockDriver.existingPin = ""
|
||||
dynamicSpy.cleanup()
|
||||
finishedSpy.clear()
|
||||
}
|
||||
|
||||
function keyClickSequence(keys) {
|
||||
for (let k of keys) {
|
||||
keyClick(k)
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentPage(stack, pageClassName) {
|
||||
if (!stack || !pageClassName)
|
||||
fail("getCurrentPage: expected param 'stack' or 'pageClassName' empty")
|
||||
verify(!!stack)
|
||||
tryCompare(stack, "busy", false) // wait for page transitions to stop
|
||||
tryCompare(stack.currentItem, "pageClassName", pageClassName)
|
||||
return stack.currentItem
|
||||
}
|
||||
|
||||
// common variant data for all flow related TDD tests
|
||||
function init_data() {
|
||||
return [ { tag: "shareUsageData+bioEnabled", shareBtnName: "btnShare", shareResult: true, biometrics: true, bioEnabled: true },
|
||||
{ tag: "dontShareUsageData+bioEnabled", shareBtnName: "btnDontShare", shareResult: false, biometrics: true, bioEnabled: true },
|
||||
|
||||
{ tag: "shareUsageData+bioDisabled", shareBtnName: "btnShare", shareResult: true, biometrics: true, bioEnabled: false },
|
||||
{ tag: "dontShareUsageData+bioDisabled", shareBtnName: "btnDontShare", shareResult: false, biometrics: true, bioEnabled: false },
|
||||
|
||||
{ tag: "shareUsageData-bio", shareBtnName: "btnShare", shareResult: true, biometrics: false },
|
||||
{ tag: "dontShareUsageData-bio", shareBtnName: "btnDontShare", shareResult: false, biometrics: false },
|
||||
]
|
||||
}
|
||||
|
||||
function test_basicGeometry() {
|
||||
verify(!!controlUnderTest)
|
||||
verify(controlUnderTest.width > 0)
|
||||
verify(controlUnderTest.height > 0)
|
||||
}
|
||||
|
||||
// FLOW: Create Profile -> Start fresh (create profile with new password)
|
||||
function test_flow_createProfile_withPassword(data) {
|
||||
verify(!!controlUnderTest)
|
||||
controlUnderTest.biometricsAvailable = data.biometrics
|
||||
|
||||
const stack = findChild(controlUnderTest, "stack")
|
||||
verify(!!stack)
|
||||
|
||||
// PAGE 1: Welcome
|
||||
let page = getCurrentPage(stack, "WelcomePage")
|
||||
|
||||
const linksText = findChild(controlUnderTest, "approvalLinks")
|
||||
verify(!!linksText)
|
||||
|
||||
dynamicSpy.setup(page, "termsOfUseRequested")
|
||||
mouseClick(linksText, linksText.width/2 - 20, linksText.height - 8)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
keyClick(Qt.Key_Escape) // close the popup
|
||||
|
||||
dynamicSpy.setup(page, "privacyPolicyRequested")
|
||||
mouseClick(linksText, linksText.width/2 + 20, linksText.height - 8)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
keyClick(Qt.Key_Escape) // close the popup
|
||||
|
||||
const btnCreateProfile = findChild(controlUnderTest, "btnCreateProfile")
|
||||
verify(!!btnCreateProfile)
|
||||
mouseClick(btnCreateProfile)
|
||||
|
||||
// PAGE 2: Help us improve
|
||||
page = getCurrentPage(stack, "HelpUsImproveStatusPage")
|
||||
|
||||
let infoButton = findChild(controlUnderTest, "infoButton")
|
||||
verify(!!infoButton)
|
||||
mouseClick(infoButton)
|
||||
const helpUsImproveDetailsPopup = findChild(controlUnderTest, "helpUsImproveDetailsPopup")
|
||||
verify(!!helpUsImproveDetailsPopup)
|
||||
compare(helpUsImproveDetailsPopup.opened, true)
|
||||
keyClick(Qt.Key_Escape) // close the popup
|
||||
|
||||
const shareButton = findChild(controlUnderTest, data.shareBtnName)
|
||||
dynamicSpy.setup(page, "shareUsageDataRequested")
|
||||
mouseClick(shareButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.shareResult)
|
||||
|
||||
// PAGE 3: Create profile
|
||||
page = getCurrentPage(stack, "CreateProfilePage")
|
||||
|
||||
const btnCreateWithPassword = findChild(controlUnderTest, "btnCreateWithPassword")
|
||||
verify(!!btnCreateWithPassword)
|
||||
mouseClick(btnCreateWithPassword)
|
||||
|
||||
// PAGE 4: Create password
|
||||
page = getCurrentPage(stack, "CreatePasswordPage")
|
||||
|
||||
infoButton = findChild(controlUnderTest, "infoButton")
|
||||
verify(!!infoButton)
|
||||
mouseClick(infoButton)
|
||||
const passwordDetailsPopup = findChild(controlUnderTest, "passwordDetailsPopup")
|
||||
verify(!!passwordDetailsPopup)
|
||||
compare(passwordDetailsPopup.opened, true)
|
||||
keyClick(Qt.Key_Escape) // close the popup
|
||||
|
||||
const btnConfirmPassword = findChild(controlUnderTest, "btnConfirmPassword")
|
||||
verify(!!btnConfirmPassword)
|
||||
compare(btnConfirmPassword.enabled, false)
|
||||
|
||||
const passwordViewNewPassword = findChild(controlUnderTest, "passwordViewNewPassword")
|
||||
verify(!!passwordViewNewPassword)
|
||||
mouseClick(passwordViewNewPassword)
|
||||
compare(passwordViewNewPassword.activeFocus, true)
|
||||
compare(passwordViewNewPassword.text, "")
|
||||
|
||||
keyClickSequence(mockDriver.dummyNewPassword)
|
||||
compare(passwordViewNewPassword.text, mockDriver.dummyNewPassword)
|
||||
compare(btnConfirmPassword.enabled, false)
|
||||
|
||||
const passwordViewNewPasswordConfirm = findChild(controlUnderTest, "passwordViewNewPasswordConfirm")
|
||||
verify(!!passwordViewNewPasswordConfirm)
|
||||
mouseClick(passwordViewNewPasswordConfirm)
|
||||
compare(passwordViewNewPasswordConfirm.activeFocus, true)
|
||||
compare(passwordViewNewPasswordConfirm.text, "")
|
||||
|
||||
keyClickSequence(mockDriver.dummyNewPassword)
|
||||
compare(passwordViewNewPassword.text, mockDriver.dummyNewPassword)
|
||||
compare(btnConfirmPassword.enabled, true)
|
||||
|
||||
mouseClick(btnConfirmPassword)
|
||||
|
||||
// PAGE 5: Enable Biometrics
|
||||
if (data.biometrics) {
|
||||
page = getCurrentPage(stack, "EnableBiometricsPage")
|
||||
|
||||
const enableBioButton = findChild(controlUnderTest, data.bioEnabled ? "btnEnableBiometrics" : "btnDontEnableBiometrics")
|
||||
dynamicSpy.setup(page, "enableBiometricsRequested")
|
||||
mouseClick(enableBioButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.bioEnabled)
|
||||
}
|
||||
|
||||
// FINISH
|
||||
tryCompare(finishedSpy, "count", 1)
|
||||
compare(finishedSpy.signalArguments[0][0], Onboarding.PrimaryFlow.CreateProfile)
|
||||
compare(finishedSpy.signalArguments[0][1], Onboarding.SecondaryFlow.CreateProfileWithPassword)
|
||||
}
|
||||
|
||||
// FLOW: Create Profile -> Use a recovery phrase (create profile with seedphrase)
|
||||
function test_flow_createProfile_withSeedphrase(data) {
|
||||
verify(!!controlUnderTest)
|
||||
controlUnderTest.biometricsAvailable = data.biometrics
|
||||
|
||||
const stack = findChild(controlUnderTest, "stack")
|
||||
verify(!!stack)
|
||||
|
||||
// PAGE 1: Welcome
|
||||
let page = getCurrentPage(stack, "WelcomePage")
|
||||
|
||||
const btnCreateProfile = findChild(controlUnderTest, "btnCreateProfile")
|
||||
verify(!!btnCreateProfile)
|
||||
mouseClick(btnCreateProfile)
|
||||
|
||||
// PAGE 2: Help us improve
|
||||
page = getCurrentPage(stack, "HelpUsImproveStatusPage")
|
||||
|
||||
const shareButton = findChild(controlUnderTest, data.shareBtnName)
|
||||
dynamicSpy.setup(page, "shareUsageDataRequested")
|
||||
mouseClick(shareButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.shareResult)
|
||||
|
||||
// PAGE 3: Create profile
|
||||
page = getCurrentPage(stack, "CreateProfilePage")
|
||||
|
||||
const btnCreateWithSeedPhrase = findChild(controlUnderTest, "btnCreateWithSeedPhrase")
|
||||
verify(!!btnCreateWithSeedPhrase)
|
||||
mouseClick(btnCreateWithSeedPhrase)
|
||||
|
||||
// PAGE 4: Create profile using a recovery phrase
|
||||
page = getCurrentPage(stack, "SeedphrasePage")
|
||||
|
||||
const btnContinue = findChild(page, "btnContinue")
|
||||
verify(!!btnContinue)
|
||||
compare(btnContinue.enabled, false)
|
||||
|
||||
const firstInput = findChild(page, "enterSeedPhraseInputField1")
|
||||
verify(!!firstInput)
|
||||
tryCompare(firstInput, "activeFocus", true)
|
||||
ClipboardUtils.setText(mockDriver.mnemonic)
|
||||
keySequence(StandardKey.Paste)
|
||||
compare(btnContinue.enabled, true)
|
||||
mouseClick(btnContinue)
|
||||
|
||||
// PAGE 5: Create password
|
||||
page = getCurrentPage(stack, "CreatePasswordPage")
|
||||
|
||||
const btnConfirmPassword = findChild(controlUnderTest, "btnConfirmPassword")
|
||||
verify(!!btnConfirmPassword)
|
||||
compare(btnConfirmPassword.enabled, false)
|
||||
|
||||
const passwordViewNewPassword = findChild(controlUnderTest, "passwordViewNewPassword")
|
||||
verify(!!passwordViewNewPassword)
|
||||
mouseClick(passwordViewNewPassword)
|
||||
compare(passwordViewNewPassword.activeFocus, true)
|
||||
compare(passwordViewNewPassword.text, "")
|
||||
|
||||
keyClickSequence(mockDriver.dummyNewPassword)
|
||||
compare(passwordViewNewPassword.text, mockDriver.dummyNewPassword)
|
||||
compare(btnConfirmPassword.enabled, false)
|
||||
|
||||
const passwordViewNewPasswordConfirm = findChild(controlUnderTest, "passwordViewNewPasswordConfirm")
|
||||
verify(!!passwordViewNewPasswordConfirm)
|
||||
mouseClick(passwordViewNewPasswordConfirm)
|
||||
compare(passwordViewNewPasswordConfirm.activeFocus, true)
|
||||
compare(passwordViewNewPasswordConfirm.text, "")
|
||||
|
||||
keyClickSequence(mockDriver.dummyNewPassword)
|
||||
compare(passwordViewNewPassword.text, mockDriver.dummyNewPassword)
|
||||
compare(btnConfirmPassword.enabled, true)
|
||||
|
||||
mouseClick(btnConfirmPassword)
|
||||
|
||||
// PAGE 6: Enable Biometrics
|
||||
if (data.biometrics) {
|
||||
page = getCurrentPage(stack, "EnableBiometricsPage")
|
||||
|
||||
const enableBioButton = findChild(controlUnderTest, data.bioEnabled ? "btnEnableBiometrics" : "btnDontEnableBiometrics")
|
||||
dynamicSpy.setup(page, "enableBiometricsRequested")
|
||||
mouseClick(enableBioButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.bioEnabled)
|
||||
}
|
||||
|
||||
// FINISH
|
||||
tryCompare(finishedSpy, "count", 1)
|
||||
compare(finishedSpy.signalArguments[0][0], Onboarding.PrimaryFlow.CreateProfile)
|
||||
compare(finishedSpy.signalArguments[0][1], Onboarding.SecondaryFlow.CreateProfileWithSeedphrase)
|
||||
}
|
||||
|
||||
function test_flow_createProfile_withKeycardAndNewSeedphrase_data() {
|
||||
const commonData = init_data()
|
||||
const flowData = []
|
||||
for (let dataRow of commonData) {
|
||||
let newRowEmptyPin = Object.create(dataRow)
|
||||
Object.assign(newRowEmptyPin, { tag: dataRow.tag + "+emptyPin", pin: "" })
|
||||
flowData.push(newRowEmptyPin)
|
||||
}
|
||||
|
||||
return flowData
|
||||
}
|
||||
|
||||
// FLOW: Create Profile -> Use an empty Keycard -> Use a new recovery phrase (create profile with keycard + new seedphrase)
|
||||
function test_flow_createProfile_withKeycardAndNewSeedphrase(data) {
|
||||
verify(!!controlUnderTest)
|
||||
controlUnderTest.biometricsAvailable = data.biometrics
|
||||
mockDriver.existingPin = data.pin
|
||||
|
||||
const stack = findChild(controlUnderTest, "stack")
|
||||
verify(!!stack)
|
||||
|
||||
// PAGE 1: Welcome
|
||||
let page = getCurrentPage(stack, "WelcomePage")
|
||||
const btnCreateProfile = findChild(controlUnderTest, "btnCreateProfile")
|
||||
verify(!!btnCreateProfile)
|
||||
mouseClick(btnCreateProfile)
|
||||
|
||||
// PAGE 2: Help us improve
|
||||
page = getCurrentPage(stack, "HelpUsImproveStatusPage")
|
||||
const shareButton = findChild(controlUnderTest, data.shareBtnName)
|
||||
dynamicSpy.setup(page, "shareUsageDataRequested")
|
||||
mouseClick(shareButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.shareResult)
|
||||
|
||||
// PAGE 3: Create profile
|
||||
page = getCurrentPage(stack, "CreateProfilePage")
|
||||
const btnCreateWithEmptyKeycard = findChild(controlUnderTest, "btnCreateWithEmptyKeycard")
|
||||
verify(!!btnCreateWithEmptyKeycard)
|
||||
mouseClick(btnCreateWithEmptyKeycard)
|
||||
|
||||
// PAGE 4: Keycard intro
|
||||
page = getCurrentPage(stack, "KeycardIntroPage")
|
||||
dynamicSpy.setup(page, "emptyKeycardDetected")
|
||||
mockDriver.keycardState = Onboarding.KeycardState.Empty // SIMULATION // TODO test other states here as well
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
tryCompare(page, "state", "empty")
|
||||
|
||||
// PAGE 5: Create profile on empty Keycard -> Use a new recovery phrase
|
||||
page = getCurrentPage(stack, "CreateKeycardProfilePage")
|
||||
const btnCreateWithEmptySeedphrase = findChild(page, "btnCreateWithEmptySeedphrase")
|
||||
verify(!!btnCreateWithEmptySeedphrase)
|
||||
mouseClick(btnCreateWithEmptySeedphrase)
|
||||
|
||||
// PAGE 6: Backup your recovery phrase (intro)
|
||||
page = getCurrentPage(stack, "BackupSeedphraseIntro")
|
||||
const btnBackupSeedphrase = findChild(page, "btnBackupSeedphrase")
|
||||
verify(!!btnBackupSeedphrase)
|
||||
mouseClick(btnBackupSeedphrase)
|
||||
|
||||
// PAGE 7: Backup your recovery phrase (ack checkboxes)
|
||||
page = getCurrentPage(stack, "BackupSeedphraseAcks")
|
||||
let btnContinue = findChild(page, "btnContinue")
|
||||
verify(!!btnContinue)
|
||||
compare(btnContinue.enabled, false)
|
||||
for (let ack of ["ack1", "ack2", "ack3", "ack4"]) {
|
||||
const cb = findChild(page, ack)
|
||||
verify(!!cb)
|
||||
mouseClick(cb)
|
||||
}
|
||||
tryCompare(btnContinue, "enabled", true)
|
||||
mouseClick(btnContinue)
|
||||
|
||||
// PAGE 8: Backup your recovery phrase (seedphrase reveal) - step 1
|
||||
page = getCurrentPage(stack, "BackupSeedphraseReveal")
|
||||
const seedGrid = findChild(page, "seedGrid")
|
||||
verify(!!seedGrid)
|
||||
tryCompare(seedGrid.layer, "enabled", true)
|
||||
const btnConfirm = findChild(page, "btnConfirm")
|
||||
verify(!!btnConfirm)
|
||||
compare(btnConfirm.enabled, false)
|
||||
const btnReveal = findChild(page, "btnReveal")
|
||||
verify(!!btnReveal)
|
||||
mouseClick(btnReveal)
|
||||
tryCompare(seedGrid.layer, "enabled", false)
|
||||
compare(btnConfirm.enabled, true)
|
||||
mouseClick(btnConfirm)
|
||||
|
||||
// PAGE 9: Backup your recovery phrase (seedphrase verification) - step 2
|
||||
page = getCurrentPage(stack, "BackupSeedphraseVerify")
|
||||
btnContinue = findChild(page, "btnContinue")
|
||||
verify(!!btnContinue)
|
||||
compare(btnContinue.enabled, false)
|
||||
const seedWords = page.seedWordsToVerify.map((entry) => entry.seedWord)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const seedInput = findChild(page, "seedInput_%1".arg(i))
|
||||
verify(!!seedInput)
|
||||
mouseClick(seedInput)
|
||||
keyClickSequence(seedWords[i])
|
||||
keyClick(Qt.Key_Tab)
|
||||
}
|
||||
compare(btnContinue.enabled, true)
|
||||
mouseClick(btnContinue)
|
||||
|
||||
// PAGE 10: Backup your recovery phrase (outro) - step 3
|
||||
page = getCurrentPage(stack, "BackupSeedphraseOutro")
|
||||
btnContinue = findChild(page, "btnContinue")
|
||||
verify(!!btnContinue)
|
||||
compare(btnContinue.enabled, false)
|
||||
const cbAck = findChild(page, "cbAck")
|
||||
verify(!!cbAck)
|
||||
compare(cbAck.checked, false)
|
||||
mouseClick(cbAck)
|
||||
compare(cbAck.checked, true)
|
||||
compare(btnContinue.enabled, true)
|
||||
mouseClick(btnContinue)
|
||||
|
||||
// PAGE 11a: Enter Keycard PIN
|
||||
if (!!data.pin) {
|
||||
page = getCurrentPage(stack, "KeycardEnterPinPage")
|
||||
dynamicSpy.setup(page, "keycardPinEntered")
|
||||
keyClickSequence(data.pin)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.pin)
|
||||
}
|
||||
// PAGE 11b: Create new Keycard PIN
|
||||
else {
|
||||
const newPin = "123321"
|
||||
page = getCurrentPage(stack, "KeycardCreatePinPage")
|
||||
tryCompare(page, "state", "creating")
|
||||
dynamicSpy.setup(page, "keycardPinCreated")
|
||||
keyClickSequence(newPin)
|
||||
tryCompare(page, "state", "repeating")
|
||||
keyClickSequence(newPin)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], newPin)
|
||||
}
|
||||
|
||||
// PAGE 12: Adding key pair to Keycard
|
||||
page = getCurrentPage(stack, "KeycardAddKeyPairPage")
|
||||
tryCompare(page, "addKeyPairState", Onboarding.AddKeyPairState.InProgress)
|
||||
page.addKeyPairState = Onboarding.AddKeyPairState.Success // SIMULATION
|
||||
btnContinue = findChild(page, "btnContinue")
|
||||
verify(!!btnContinue)
|
||||
compare(btnContinue.enabled, true)
|
||||
mouseClick(btnContinue)
|
||||
|
||||
// PAGE 13: Enable Biometrics
|
||||
if (data.biometrics) {
|
||||
page = getCurrentPage(stack, "EnableBiometricsPage")
|
||||
|
||||
const enableBioButton = findChild(controlUnderTest, data.bioEnabled ? "btnEnableBiometrics" : "btnDontEnableBiometrics")
|
||||
dynamicSpy.setup(page, "enableBiometricsRequested")
|
||||
mouseClick(enableBioButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.bioEnabled)
|
||||
}
|
||||
|
||||
// FINISH
|
||||
tryCompare(finishedSpy, "count", 1)
|
||||
compare(finishedSpy.signalArguments[0][0], Onboarding.PrimaryFlow.CreateProfile)
|
||||
compare(finishedSpy.signalArguments[0][1], Onboarding.SecondaryFlow.CreateProfileWithKeycardNewSeedphrase)
|
||||
}
|
||||
|
||||
function test_flow_createProfile_withKeycardAndExistingSeedphrase_data() {
|
||||
return test_flow_createProfile_withKeycardAndNewSeedphrase_data()
|
||||
}
|
||||
|
||||
// FLOW: Create Profile -> Use an empty Keycard -> Use an existing recovery phrase (create profile with keycard + existing seedphrase)
|
||||
function test_flow_createProfile_withKeycardAndExistingSeedphrase(data) {
|
||||
verify(!!controlUnderTest)
|
||||
controlUnderTest.biometricsAvailable = data.biometrics
|
||||
mockDriver.existingPin = data.pin
|
||||
|
||||
const stack = findChild(controlUnderTest, "stack")
|
||||
verify(!!stack)
|
||||
|
||||
// PAGE 1: Welcome
|
||||
let page = getCurrentPage(stack, "WelcomePage")
|
||||
const btnCreateProfile = findChild(controlUnderTest, "btnCreateProfile")
|
||||
verify(!!btnCreateProfile)
|
||||
mouseClick(btnCreateProfile)
|
||||
|
||||
// PAGE 2: Help us improve
|
||||
page = getCurrentPage(stack, "HelpUsImproveStatusPage")
|
||||
const shareButton = findChild(controlUnderTest, data.shareBtnName)
|
||||
dynamicSpy.setup(page, "shareUsageDataRequested")
|
||||
mouseClick(shareButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.shareResult)
|
||||
|
||||
// PAGE 3: Create profile
|
||||
page = getCurrentPage(stack, "CreateProfilePage")
|
||||
const btnCreateWithEmptyKeycard = findChild(controlUnderTest, "btnCreateWithEmptyKeycard")
|
||||
verify(!!btnCreateWithEmptyKeycard)
|
||||
mouseClick(btnCreateWithEmptyKeycard)
|
||||
|
||||
// PAGE 4: Keycard intro
|
||||
page = getCurrentPage(stack, "KeycardIntroPage")
|
||||
dynamicSpy.setup(page, "emptyKeycardDetected")
|
||||
mockDriver.keycardState = Onboarding.KeycardState.Empty // SIMULATION // TODO test other states here as well
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
tryCompare(page, "state", "empty")
|
||||
|
||||
// PAGE 5: Create profile on empty Keycard -> Use an existing recovery phrase
|
||||
page = getCurrentPage(stack, "CreateKeycardProfilePage")
|
||||
const btnCreateWithExistingSeedphrase = findChild(page, "btnCreateWithExistingSeedphrase")
|
||||
verify(!!btnCreateWithExistingSeedphrase)
|
||||
mouseClick(btnCreateWithExistingSeedphrase)
|
||||
|
||||
// PAGE 6: Create profile on empty Keycard using a recovery phrase
|
||||
page = getCurrentPage(stack, "SeedphrasePage")
|
||||
const btnContinue = findChild(page, "btnContinue")
|
||||
verify(!!btnContinue)
|
||||
compare(btnContinue.enabled, false)
|
||||
const firstInput = findChild(page, "enterSeedPhraseInputField1")
|
||||
verify(!!firstInput)
|
||||
tryCompare(firstInput, "activeFocus", true)
|
||||
ClipboardUtils.setText(mockDriver.mnemonic)
|
||||
keySequence(StandardKey.Paste)
|
||||
compare(btnContinue.enabled, true)
|
||||
mouseClick(btnContinue)
|
||||
|
||||
// PAGE 7a: Enter Keycard PIN
|
||||
if (!!data.pin) {
|
||||
page = getCurrentPage(stack, "KeycardEnterPinPage")
|
||||
dynamicSpy.setup(page, "keycardPinEntered")
|
||||
keyClickSequence(data.pin)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.pin)
|
||||
}
|
||||
// PAGE 7b: Create new Keycard PIN
|
||||
else {
|
||||
const newPin = "123321"
|
||||
page = getCurrentPage(stack, "KeycardCreatePinPage")
|
||||
tryCompare(page, "state", "creating")
|
||||
dynamicSpy.setup(page, "keycardPinCreated")
|
||||
keyClickSequence(newPin)
|
||||
tryCompare(page, "state", "repeating")
|
||||
keyClickSequence(newPin)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], newPin)
|
||||
}
|
||||
|
||||
// PAGE 8: Adding key pair to Keycard
|
||||
page = getCurrentPage(stack, "KeycardAddKeyPairPage")
|
||||
tryCompare(page, "addKeyPairState", Onboarding.AddKeyPairState.InProgress)
|
||||
page.addKeyPairState = Onboarding.AddKeyPairState.Success // SIMULATION
|
||||
const btnContinue2 = findChild(page, "btnContinue")
|
||||
verify(!!btnContinue2)
|
||||
compare(btnContinue2.enabled, true)
|
||||
mouseClick(btnContinue2)
|
||||
|
||||
// PAGE 9: Enable Biometrics
|
||||
if (controlUnderTest.biometricsAvailable) {
|
||||
page = getCurrentPage(stack, "EnableBiometricsPage")
|
||||
|
||||
const enableBioButton = findChild(controlUnderTest, data.bioEnabled ? "btnEnableBiometrics" : "btnDontEnableBiometrics")
|
||||
dynamicSpy.setup(page, "enableBiometricsRequested")
|
||||
mouseClick(enableBioButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.bioEnabled)
|
||||
}
|
||||
|
||||
// FINISH
|
||||
tryCompare(finishedSpy, "count", 1)
|
||||
compare(finishedSpy.signalArguments[0][0], Onboarding.PrimaryFlow.CreateProfile)
|
||||
compare(finishedSpy.signalArguments[0][1], Onboarding.SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase)
|
||||
}
|
||||
|
||||
// FLOW: Log in -> Log in with recovery phrase
|
||||
function test_flow_login_withSeedphrase(data) {
|
||||
verify(!!controlUnderTest)
|
||||
controlUnderTest.biometricsAvailable = data.biometrics
|
||||
|
||||
const stack = findChild(controlUnderTest, "stack")
|
||||
verify(!!stack)
|
||||
|
||||
// PAGE 1: Welcome
|
||||
let page = getCurrentPage(stack, "WelcomePage")
|
||||
const btnLogin = findChild(controlUnderTest, "btnLogin")
|
||||
verify(!!btnLogin)
|
||||
mouseClick(btnLogin)
|
||||
|
||||
// PAGE 2: Help us improve
|
||||
page = getCurrentPage(stack, "HelpUsImproveStatusPage")
|
||||
const shareButton = findChild(controlUnderTest, data.shareBtnName)
|
||||
dynamicSpy.setup(page, "shareUsageDataRequested")
|
||||
mouseClick(shareButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.shareResult)
|
||||
|
||||
// PAGE 3: Log in -> Enter recovery phrase
|
||||
page = getCurrentPage(stack, "LoginPage")
|
||||
const btnWithSeedphrase = findChild(page, "btnWithSeedphrase")
|
||||
verify(!!btnWithSeedphrase)
|
||||
mouseClick(btnWithSeedphrase)
|
||||
|
||||
// PAGE 4: Sign in with your Status recovery phrase
|
||||
page = getCurrentPage(stack, "SeedphrasePage")
|
||||
|
||||
const btnContinue = findChild(page, "btnContinue")
|
||||
verify(!!btnContinue)
|
||||
compare(btnContinue.enabled, false)
|
||||
|
||||
const firstInput = findChild(page, "enterSeedPhraseInputField1")
|
||||
verify(!!firstInput)
|
||||
tryCompare(firstInput, "activeFocus", true)
|
||||
ClipboardUtils.setText(mockDriver.mnemonic)
|
||||
keySequence(StandardKey.Paste)
|
||||
compare(btnContinue.enabled, true)
|
||||
mouseClick(btnContinue)
|
||||
|
||||
// PAGE 5: Create password
|
||||
page = getCurrentPage(stack, "CreatePasswordPage")
|
||||
|
||||
const btnConfirmPassword = findChild(controlUnderTest, "btnConfirmPassword")
|
||||
verify(!!btnConfirmPassword)
|
||||
compare(btnConfirmPassword.enabled, false)
|
||||
|
||||
const passwordViewNewPassword = findChild(controlUnderTest, "passwordViewNewPassword")
|
||||
verify(!!passwordViewNewPassword)
|
||||
mouseClick(passwordViewNewPassword)
|
||||
compare(passwordViewNewPassword.activeFocus, true)
|
||||
compare(passwordViewNewPassword.text, "")
|
||||
|
||||
keyClickSequence(mockDriver.dummyNewPassword)
|
||||
compare(passwordViewNewPassword.text, mockDriver.dummyNewPassword)
|
||||
compare(btnConfirmPassword.enabled, false)
|
||||
|
||||
const passwordViewNewPasswordConfirm = findChild(controlUnderTest, "passwordViewNewPasswordConfirm")
|
||||
verify(!!passwordViewNewPasswordConfirm)
|
||||
mouseClick(passwordViewNewPasswordConfirm)
|
||||
compare(passwordViewNewPasswordConfirm.activeFocus, true)
|
||||
compare(passwordViewNewPasswordConfirm.text, "")
|
||||
|
||||
keyClickSequence(mockDriver.dummyNewPassword)
|
||||
compare(passwordViewNewPassword.text, mockDriver.dummyNewPassword)
|
||||
compare(btnConfirmPassword.enabled, true)
|
||||
|
||||
mouseClick(btnConfirmPassword)
|
||||
|
||||
// PAGE 6: Enable Biometrics
|
||||
if (data.biometrics) {
|
||||
page = getCurrentPage(stack, "EnableBiometricsPage")
|
||||
|
||||
const enableBioButton = findChild(controlUnderTest, data.bioEnabled ? "btnEnableBiometrics" : "btnDontEnableBiometrics")
|
||||
dynamicSpy.setup(page, "enableBiometricsRequested")
|
||||
mouseClick(enableBioButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.bioEnabled)
|
||||
}
|
||||
|
||||
// FINISH
|
||||
tryCompare(finishedSpy, "count", 1)
|
||||
compare(finishedSpy.signalArguments[0][0], Onboarding.PrimaryFlow.Login)
|
||||
compare(finishedSpy.signalArguments[0][1], Onboarding.SecondaryFlow.LoginWithSeedphrase)
|
||||
}
|
||||
|
||||
// FLOW: Log in -> Log in by syncing
|
||||
function test_flow_login_bySyncing(data) {
|
||||
verify(!!controlUnderTest)
|
||||
controlUnderTest.biometricsAvailable = data.biometrics
|
||||
|
||||
const stack = findChild(controlUnderTest, "stack")
|
||||
verify(!!stack)
|
||||
|
||||
// PAGE 1: Welcome
|
||||
let page = getCurrentPage(stack, "WelcomePage")
|
||||
const btnLogin = findChild(controlUnderTest, "btnLogin")
|
||||
verify(!!btnLogin)
|
||||
mouseClick(btnLogin)
|
||||
|
||||
// PAGE 2: Help us improve
|
||||
page = getCurrentPage(stack, "HelpUsImproveStatusPage")
|
||||
const shareButton = findChild(controlUnderTest, data.shareBtnName)
|
||||
dynamicSpy.setup(page, "shareUsageDataRequested")
|
||||
mouseClick(shareButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.shareResult)
|
||||
|
||||
// PAGE 3: Log in
|
||||
page = getCurrentPage(stack, "LoginPage")
|
||||
const btnBySyncing = findChild(page, "btnBySyncing")
|
||||
verify(!!btnBySyncing)
|
||||
mouseClick(btnBySyncing)
|
||||
|
||||
const loginWithSyncAckPopup = findChild(page, "loginWithSyncAckPopup")
|
||||
verify(!!loginWithSyncAckPopup)
|
||||
|
||||
let btnContinue = findChild(loginWithSyncAckPopup, "btnContinue")
|
||||
verify(!!btnContinue)
|
||||
compare(btnContinue.enabled, false)
|
||||
for (let ack of ["ack1", "ack2", "ack3"]) {
|
||||
const cb = findChild(loginWithSyncAckPopup, ack)
|
||||
verify(!!cb)
|
||||
mouseClick(cb)
|
||||
}
|
||||
tryCompare(btnContinue, "enabled", true)
|
||||
mouseClick(btnContinue)
|
||||
|
||||
// PAGE 4: Log in by syncing
|
||||
page = getCurrentPage(stack, "LoginBySyncingPage")
|
||||
|
||||
const enterCodeTabBtn = findChild(page, "secondTab_StatusSwitchTabButton")
|
||||
verify(!!enterCodeTabBtn)
|
||||
mouseClick(enterCodeTabBtn)
|
||||
|
||||
btnContinue = findChild(page, "continue_StatusButton")
|
||||
verify(!!btnContinue)
|
||||
tryCompare(btnContinue, "enabled", false)
|
||||
|
||||
const syncCodeInput = findChild(page, "syncCodeInput")
|
||||
verify(!!syncCodeInput)
|
||||
mouseClick(syncCodeInput)
|
||||
compare(syncCodeInput.input.edit.activeFocus, true)
|
||||
keyClickSequence("1234")
|
||||
tryCompare(btnContinue, "enabled", true)
|
||||
mouseClick(btnContinue)
|
||||
|
||||
// PAGE 5: Profile sync in progress
|
||||
page = getCurrentPage(stack, "SyncProgressPage")
|
||||
tryCompare(page, "syncState", Onboarding.SyncState.InProgress)
|
||||
page.syncState = Onboarding.SyncState.Success // SIMULATION
|
||||
const btnLogin2 = findChild(page, "btnLogin") // TODO test other flows/buttons here as well
|
||||
verify(!!btnLogin2)
|
||||
compare(btnLogin2.enabled, true)
|
||||
mouseClick(btnLogin2)
|
||||
|
||||
// PAGE 6: Enable Biometrics
|
||||
if (data.biometrics) {
|
||||
page = getCurrentPage(stack, "EnableBiometricsPage")
|
||||
|
||||
const enableBioButton = findChild(controlUnderTest, data.bioEnabled ? "btnEnableBiometrics" : "btnDontEnableBiometrics")
|
||||
dynamicSpy.setup(page, "enableBiometricsRequested")
|
||||
mouseClick(enableBioButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.bioEnabled)
|
||||
}
|
||||
|
||||
// FINISH
|
||||
tryCompare(finishedSpy, "count", 1)
|
||||
compare(finishedSpy.signalArguments[0][0], Onboarding.PrimaryFlow.Login)
|
||||
compare(finishedSpy.signalArguments[0][1], Onboarding.SecondaryFlow.LoginWithSyncing)
|
||||
}
|
||||
|
||||
// FLOW: Log in -> Log in with Keycard
|
||||
function test_flow_login_withKeycard(data) {
|
||||
verify(!!controlUnderTest)
|
||||
controlUnderTest.biometricsAvailable = data.biometrics
|
||||
mockDriver.existingPin = "123456"
|
||||
|
||||
const stack = findChild(controlUnderTest, "stack")
|
||||
verify(!!stack)
|
||||
|
||||
// PAGE 1: Welcome
|
||||
let page = getCurrentPage(stack, "WelcomePage")
|
||||
const btnLogin = findChild(controlUnderTest, "btnLogin")
|
||||
verify(!!btnLogin)
|
||||
mouseClick(btnLogin)
|
||||
|
||||
// PAGE 2: Help us improve
|
||||
page = getCurrentPage(stack, "HelpUsImproveStatusPage")
|
||||
const shareButton = findChild(controlUnderTest, data.shareBtnName)
|
||||
dynamicSpy.setup(page, "shareUsageDataRequested")
|
||||
mouseClick(shareButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.shareResult)
|
||||
|
||||
// PAGE 3: Log in -> Login with Keycard
|
||||
page = getCurrentPage(stack, "LoginPage")
|
||||
const btnWithKeycard = findChild(page, "btnWithKeycard")
|
||||
verify(!!btnWithKeycard)
|
||||
mouseClick(btnWithKeycard)
|
||||
|
||||
// PAGE 4: Keycard intro
|
||||
page = getCurrentPage(stack, "KeycardIntroPage")
|
||||
dynamicSpy.setup(page, "notEmptyKeycardDetected")
|
||||
mockDriver.keycardState = Onboarding.KeycardState.NotEmpty // SIMULATION // TODO test other states here as well
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
tryCompare(page, "state", "notEmpty")
|
||||
|
||||
// PAGE 5: Enter Keycard PIN
|
||||
page = getCurrentPage(stack, "KeycardEnterPinPage")
|
||||
dynamicSpy.setup(page, "keycardPinEntered")
|
||||
keyClickSequence(mockDriver.existingPin)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], mockDriver.existingPin)
|
||||
|
||||
// PAGE 6: Enable Biometrics
|
||||
if (data.biometrics) {
|
||||
page = getCurrentPage(stack, "EnableBiometricsPage")
|
||||
|
||||
const enableBioButton = findChild(controlUnderTest, data.bioEnabled ? "btnEnableBiometrics" : "btnDontEnableBiometrics")
|
||||
dynamicSpy.setup(page, "enableBiometricsRequested")
|
||||
mouseClick(enableBioButton)
|
||||
tryCompare(dynamicSpy, "count", 1)
|
||||
compare(dynamicSpy.signalArguments[0][0], data.bioEnabled)
|
||||
}
|
||||
|
||||
// FINISH
|
||||
tryCompare(finishedSpy, "count", 1)
|
||||
compare(finishedSpy.signalArguments[0][0], Onboarding.PrimaryFlow.Login)
|
||||
compare(finishedSpy.signalArguments[0][1], Onboarding.SecondaryFlow.LoginWithKeycard)
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import QtQml 2.14
|
||||
|
||||
QtObject {
|
||||
function decomposeLink(link) {
|
||||
const fileRegex = /www\.figma\.com\/file\/([a-zA-Z0-9]+)/
|
||||
const fileRegex = /www\.figma\.com\/design\/([a-zA-Z0-9]+)/
|
||||
const fileMatch = link.match(fileRegex)
|
||||
|
||||
const nodeIdRegex = /node-id=([0-9A-Za-z%-]+)/
|
||||
|
@ -0,0 +1,3 @@
|
||||
import QtQml 2.15
|
||||
|
||||
QtObject {}
|
1
storybook/stubs/AppLayouts/Onboarding2/stores/qmldir
Normal file
@ -0,0 +1 @@
|
||||
OnboardingStore 1.0 OnboardingStore.qml
|
@ -1,28 +1,3 @@
|
||||
import QtQuick 2.15
|
||||
import QtQml 2.15
|
||||
|
||||
QtObject {
|
||||
property QtObject privacyModule: QtObject {
|
||||
signal passwordChanged(success: bool, errorMsg: string)
|
||||
signal storeToKeychainError(errorDescription: string)
|
||||
signal storeToKeychainSuccess()
|
||||
}
|
||||
|
||||
function tryStoreToKeyChain(errorDescription) {
|
||||
if (generateMacKeyChainStoreError.checked) {
|
||||
privacyModule.storeToKeychainError(errorDescription)
|
||||
} else {
|
||||
passwordView.localAccountSettings.storeToKeychainValue = Constants.keychain.storedValue.store
|
||||
privacyModule.storeToKeychainSuccess()
|
||||
privacyModule.passwordChanged(true, "")
|
||||
}
|
||||
}
|
||||
|
||||
function tryRemoveFromKeyChain() {
|
||||
if (generateMacKeyChainStoreError.checked) {
|
||||
privacyModule.storeToKeychainError("Error removing from keychain")
|
||||
} else {
|
||||
passwordView.localAccountSettings.storeToKeychainValue = Constants.keychain.storedValue.notNow
|
||||
privacyModule.storeToKeychainSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
QtObject {}
|
||||
|
@ -5,7 +5,7 @@ ListModel {
|
||||
|
||||
Component.onCompleted: {
|
||||
var englishWords = [
|
||||
"apple", "banana", "cat", "cow", "catalog", "catch", "category", "cattle", "dog", "elephant", "fish", "grape", "horse", "ice cream", "jellyfish",
|
||||
"age", "agent", "apple", "banana", "cat", "cow", "catalog", "catch", "category", "cattle", "dog", "elephant", "fish", "grape", "horse", "icecream", "jellyfish",
|
||||
"kiwi", "lemon", "mango", "nut", "orange", "pear", "quail", "rabbit", "strawberry", "turtle",
|
||||
"umbrella", "violet", "watermelon", "xylophone", "yogurt", "zebra"
|
||||
// Add more English words here...
|
||||
|
3
storybook/stubs/shared/stores/MetricsStore.qml
Normal file
@ -0,0 +1,3 @@
|
||||
import QtQml 2.15
|
||||
|
||||
QtObject {}
|
@ -9,3 +9,4 @@ ProfileStore 1.0 ProfileStore.qml
|
||||
RootStore 1.0 RootStore.qml
|
||||
UtilsStore 1.0 UtilsStore.qml
|
||||
BrowserConnectStore 1.0 BrowserConnectStore.qml
|
||||
MetricsStore 1.0 MetricsStore.qml
|
||||
|
@ -8,8 +8,8 @@ class Keycard(Enum):
|
||||
KEYCARD_INCORRECT_PUK = '222222222222'
|
||||
KEYCARD_NAME = 'Test Keycard'
|
||||
ACCOUNT_NAME = 'Test Account'
|
||||
KEYCARD_POPUP_HEADER_CREATE_SEED = 'Create a new Keycard account with a new seed phrase'
|
||||
KEYCARD_POPUP_HEADER_IMPORT_SEED = 'Import or restore a Keycard via a seed phrase'
|
||||
KEYCARD_POPUP_HEADER_CREATE_SEED = 'Create a new Keycard account with a new recovery phrase'
|
||||
KEYCARD_POPUP_HEADER_IMPORT_SEED = 'Import or restore a Keycard via a recovery phrase'
|
||||
KEYCARD_POPUP_HEADER_SET_UP_EXISTING = 'Set up a new Keycard with an existing account'
|
||||
KEYCARD_INSTRUCTIONS_PLUG_IN = 'Plug in Keycard reader...'
|
||||
KEYCARD_INSTRUCTIONS_INSERT_KEYCARD = 'Insert Keycard...'
|
||||
|
@ -18,18 +18,18 @@ class OnboardingScreensHeaders(Enum):
|
||||
class KeysExistText(Enum):
|
||||
KEYS_EXIST_TITLE = 'Keys for this account already exist'
|
||||
KEYS_EXIST_TEXT = (
|
||||
"Keys for this account already exist and can't be added again. If you've lost your password, passcode or Keycard, uninstall the app, reinstall and access your keys by entering your seed phrase. In case of Keycard try recovering using PUK or reinstall the app and try login with the Keycard option.")
|
||||
"Keys for this account already exist and can't be added again. If you've lost your password, passcode or Keycard, uninstall the app, reinstall and access your keys by entering your recovery phrase. In case of Keycard try recovering using PUK or reinstall the app and try login with the Keycard option.")
|
||||
|
||||
|
||||
password_strength_elements = namedtuple('Password_Strength_Elements',
|
||||
['strength_indicator', 'strength_color', 'strength_messages'])
|
||||
very_weak_lower_elements = password_strength_elements('Very weak', '#ff2d55', ['• Lower case'])
|
||||
very_weak_upper_elements = password_strength_elements('Very weak', '#ff2d55', ['• Upper case'])
|
||||
very_weak_numbers_elements = password_strength_elements('Very weak', '#ff2d55', ['• Numbers'])
|
||||
very_weak_symbols_elements = password_strength_elements('Very weak', '#ff2d55', ['• Symbols'])
|
||||
weak_elements = password_strength_elements('Weak', '#fe8f59', ['• Numbers', '• Symbols'])
|
||||
so_so_elements = password_strength_elements('So-so', '#ffca0f', ['• Lower case', '• Numbers', '• Symbols'])
|
||||
very_weak_lower_elements = password_strength_elements('Very weak', '#ff2d55', ['✓ Lower case'])
|
||||
very_weak_upper_elements = password_strength_elements('Very weak', '#ff2d55', ['✓ Upper case'])
|
||||
very_weak_numbers_elements = password_strength_elements('Very weak', '#ff2d55', ['✓ Numbers'])
|
||||
very_weak_symbols_elements = password_strength_elements('Very weak', '#ff2d55', ['✓ Symbols'])
|
||||
weak_elements = password_strength_elements('Weak', '#fe8f59', ['✓ Numbers', '✓ Symbols'])
|
||||
okay_elements = password_strength_elements('Okay', '#ffca0f', ['✓ Lower case', '✓ Numbers', '✓ Symbols'])
|
||||
good_elements = password_strength_elements('Good', '#9ea85d',
|
||||
['• Lower case', '• Upper case', '• Numbers', '• Symbols'])
|
||||
great_elements = password_strength_elements('Great', '#4ebc60',
|
||||
['• Lower case', '• Upper case', '• Numbers', '• Symbols'])
|
||||
['✓ Lower case', '✓ Upper case', '✓ Numbers', '✓ Symbols'])
|
||||
strong_elements = password_strength_elements('Very strong', '#4ebc60',
|
||||
['✓ Lower case', '✓ Upper case', '✓ Numbers', '✓ Symbols'])
|
||||
|
@ -87,7 +87,7 @@ class WalletRenameKeypair(Enum):
|
||||
|
||||
|
||||
class WalletSeedPhrase(Enum):
|
||||
WALLET_SEED_PHRASE_ALREADY_ADDED = 'The entered seed phrase is already added'
|
||||
WALLET_SEED_PHRASE_ALREADY_ADDED = 'The entered recovery phrase is already added'
|
||||
|
||||
|
||||
class WalletAccountPopup(Enum):
|
||||
|
@ -44,10 +44,11 @@ mainWindow_Import_StatusButton = {"checkable": False, "container": mainWindow_Se
|
||||
# SyncCode View
|
||||
mainWindow_SyncCodeView = {"container": statusDesktop_mainWindow, "type": "SyncCodeView", "unnamed": 1, "visible": True}
|
||||
mainWindow_switchTabBar_StatusSwitchTabBar_2 = {"container": statusDesktop_mainWindow, "id": "switchTabBar", "type": "StatusSwitchTabBar", "unnamed": 1, "visible": True}
|
||||
switchTabBar_Enter_sync_code_StatusSwitchTabButton = {"checkable": True, "container": mainWindow_switchTabBar_StatusSwitchTabBar_2, "text": "Enter sync code", "type": "StatusSwitchTabButton", "unnamed": 1, "visible": True}
|
||||
switchTabBar_Enter_sync_code_StatusSwitchTabButton = {"checkable": True, "container": mainWindow_switchTabBar_StatusSwitchTabBar_2, "objectName": "secondTab_StatusSwitchTabButton", "type": "StatusSwitchTabButton", "visible": True}
|
||||
mainWindow_statusBaseInput_StatusBaseInput = {"container": statusDesktop_mainWindow, "id": "statusBaseInput", "type": "StatusBaseInput", "unnamed": 1, "visible": True}
|
||||
mainWindow_Paste_StatusButton = {"container": statusDesktop_mainWindow, "objectName": "syncCodePasteButton", "type": "StatusButton", "visible": True}
|
||||
mainWindow_syncingEnterCode_SyncingEnterCode = {"container": statusDesktop_mainWindow, "objectName": "syncingEnterCode", "type": "SyncingEnterCode", "visible": True}
|
||||
mainWindow_nameInput_syncingEnterCode_Continue = {"checkable": False, "container": statusDesktop_mainWindow, "objectName": "continue_StatusButton", "type": "StatusButton", "visible": True}
|
||||
|
||||
# SyncDevice View
|
||||
mainWindow_SyncingDeviceView_found = {"container": statusDesktop_mainWindow, "type": "SyncingDeviceView", "unnamed": 1, "visible": True}
|
||||
@ -89,7 +90,7 @@ mainWindow_passwordViewNewPassword = {"container": mainWindow_CreatePasswordView
|
||||
mainWindow_passwordViewNewPasswordConfirm = {"container": mainWindow_CreatePasswordView, "objectName": "passwordViewNewPasswordConfirm", "type": "StatusPasswordInput", "visible": True}
|
||||
mainWindow_Create_password_StatusButton = {"checkable": False, "container": mainWindow_CreatePasswordView, "objectName": "onboardingCreatePasswordButton", "type": "StatusButton", "visible": True}
|
||||
mainWindow_view_PasswordView = {"container": statusDesktop_mainWindow, "id": "view", "type": "PasswordView", "unnamed": 1, "visible": True}
|
||||
mainWindow_RowLayout = {"container": statusDesktop_mainWindow, "type": "RowLayout", "unnamed": 1, "visible": True}
|
||||
mainWindow_RowLayout = {"container": statusDesktop_mainWindow, "type": "PassIncludesIndicator", "unnamed": 1, "visible": True}
|
||||
mainWindow_strengthInditactor_StatusPasswordStrengthIndicator = {"container": statusDesktop_mainWindow, "id": "strengthInditactor", "type": "StatusPasswordStrengthIndicator", "unnamed": 1, "visible": True}
|
||||
mainWindow_show_icon_StatusIcon = {"container": statusDesktop_mainWindow, "objectName": "show-icon", "type": "StatusIcon", "visible": True}
|
||||
mainWindow_hide_icon_StatusIcon = {"container": statusDesktop_mainWindow, "objectName": "hide-icon", "type": "StatusIcon", "visible": True}
|
||||
|
@ -148,6 +148,7 @@ class SyncCodeView(OnboardingView):
|
||||
self._enter_sync_code_button = Button(onboarding_names.switchTabBar_Enter_sync_code_StatusSwitchTabButton)
|
||||
self._paste_sync_code_button = Button(onboarding_names.mainWindow_Paste_StatusButton)
|
||||
self._syncing_enter_code_item = QObject(onboarding_names.mainWindow_syncingEnterCode_SyncingEnterCode)
|
||||
self.continue_button = Button(onboarding_names.mainWindow_nameInput_syncingEnterCode_Continue)
|
||||
|
||||
@allure.step('Open enter sync code form')
|
||||
def open_enter_sync_code_form(self):
|
||||
@ -481,15 +482,10 @@ class CreatePasswordView(OnboardingView):
|
||||
def green_indicator_messages(self) -> typing.List[str]:
|
||||
messages = []
|
||||
color = ColorCodes.GREEN.value
|
||||
for child in walk_children(self._indicator_panel_object.object):
|
||||
if getattr(child, 'id', '') == 'lowerCaseTxt' and child.color['name'] == color:
|
||||
messages.append(str(child.text))
|
||||
elif getattr(child, 'id', '') == 'upperCaseTxt' and child.color['name'] == color:
|
||||
messages.append(str(child.text))
|
||||
elif getattr(child, 'id', '') == 'numbersTxt' and child.color['name'] == color:
|
||||
messages.append(str(child.text))
|
||||
elif getattr(child, 'id', '') == 'symbolsTxt' and child.color['name'] == color:
|
||||
messages.append(str(child.text))
|
||||
|
||||
for item in driver.findAllObjects(self._indicator_panel_object.real_name):
|
||||
if str(item.color.name) == color:
|
||||
messages.append(str(item.text))
|
||||
return messages
|
||||
|
||||
@property
|
||||
|
@ -56,6 +56,7 @@ def test_sync_device_during_onboarding(multiple_instances):
|
||||
sync_start = sync_view.open_enter_sync_code_form()
|
||||
pyperclip.copy(sync_code)
|
||||
sync_start.click_paste_button()
|
||||
sync_start.continue_button.click()
|
||||
sync_device_found = SyncDeviceFoundView()
|
||||
assert driver.waitFor(
|
||||
lambda: 'Device found!' in sync_device_found.device_found_notifications, 15000)
|
||||
|
@ -35,7 +35,8 @@ def test_wrong_sync_code(sync_screen, wrong_sync_code):
|
||||
with step('Paste wrong sync code and check that error message appears'):
|
||||
pyperclip.copy(wrong_sync_code)
|
||||
sync_view.click_paste_button()
|
||||
assert SyncingSettings.SYNC_CODE_IS_WRONG_TEXT.value == sync_view.sync_code_error_message, f'Wrong sync code message did not appear'
|
||||
assert str(SyncingSettings.SYNC_CODE_IS_WRONG_TEXT.value == sync_view.sync_code_error_message), \
|
||||
f'Wrong sync code message did not appear'
|
||||
|
||||
|
||||
@allure.testcase('https://ethstatus.testrail.net/index.php?/cases/view/703591', 'Generate sync code. Negative')
|
||||
|
@ -6,7 +6,7 @@ from helpers.OnboardingHelper import open_generate_new_keys_view
|
||||
from . import marks
|
||||
|
||||
from constants.onboarding import very_weak_lower_elements, very_weak_upper_elements, \
|
||||
very_weak_numbers_elements, very_weak_symbols_elements, weak_elements, so_so_elements, good_elements, great_elements
|
||||
very_weak_numbers_elements, very_weak_symbols_elements, weak_elements, okay_elements, good_elements, strong_elements
|
||||
|
||||
pytestmark = marks
|
||||
|
||||
@ -20,9 +20,9 @@ def test_check_password_strength_and_login(main_window, user_account):
|
||||
('1234567890', very_weak_numbers_elements),
|
||||
('+_!!!!!!!!', very_weak_symbols_elements),
|
||||
('+1_3!48888', weak_elements),
|
||||
('+1_3!48a11', so_so_elements),
|
||||
('+1_3!48a11', okay_elements),
|
||||
('+1_3!48aT1', good_elements),
|
||||
('+1_3!48aTq', great_elements)]
|
||||
('+1_3!48aTq', strong_elements)]
|
||||
expected_password = ""
|
||||
|
||||
keys_screen = open_generate_new_keys_view()
|
||||
@ -38,7 +38,7 @@ def test_check_password_strength_and_login(main_window, user_account):
|
||||
expected_password = input_text
|
||||
create_password_view.set_password_in_first_field(input_text)
|
||||
assert create_password_view.strength_indicator_color == expected_indicator[1]
|
||||
assert create_password_view.strength_indicator_text == expected_indicator[0]
|
||||
assert str(create_password_view.strength_indicator_text) == expected_indicator[0]
|
||||
assert sorted(create_password_view.green_indicator_messages) == sorted(expected_indicator[2])
|
||||
assert not create_password_view.is_create_password_button_enabled
|
||||
|
||||
|
@ -165,6 +165,9 @@ add_library(StatusQ SHARED
|
||||
src/wallet/managetokensmodel.h
|
||||
src/wallet/tokendata.cpp
|
||||
src/wallet/tokendata.h
|
||||
|
||||
# onboarding
|
||||
src/onboarding/enums.h
|
||||
)
|
||||
|
||||
target_compile_features(StatusQ PRIVATE cxx_std_17)
|
||||
|
@ -1,41 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QQmlParserStatus>
|
||||
#include <QTimer>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
class QNetworkAccessManager;
|
||||
|
||||
/// Checks if the internet connection is available, when active.
|
||||
/// It checks the connection every 30 seconds as long as the \c active property is \c true.
|
||||
class NetworkChecker : public QObject
|
||||
/// It checks the connection every 30 seconds as long as the \c active property is \c true (by default it is)
|
||||
class NetworkChecker : public QObject, public QQmlParserStatus
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged)
|
||||
Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged)
|
||||
Q_INTERFACES(QQmlParserStatus)
|
||||
|
||||
Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged FINAL)
|
||||
Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged FINAL)
|
||||
Q_PROPERTY(bool checking READ checking NOTIFY checkingChanged FINAL)
|
||||
|
||||
public:
|
||||
explicit NetworkChecker(QObject* parent = nullptr);
|
||||
explicit NetworkChecker(QObject *parent = nullptr);
|
||||
bool isOnline() const;
|
||||
|
||||
bool isActive() const;
|
||||
void setActive(bool active);
|
||||
|
||||
Q_INVOKABLE void checkNetwork();
|
||||
|
||||
protected:
|
||||
void classBegin() override;
|
||||
void componentComplete() override;
|
||||
|
||||
signals:
|
||||
void isOnlineChanged(bool online);
|
||||
void activeChanged(bool active);
|
||||
void checkingChanged();
|
||||
|
||||
private:
|
||||
QNetworkAccessManager manager;
|
||||
QTimer timer;
|
||||
bool online = false;
|
||||
bool active = true;
|
||||
constexpr static std::chrono::milliseconds checkInterval = 30s;
|
||||
|
||||
void checkNetwork();
|
||||
void onFinished(QNetworkReply* reply);
|
||||
void onFinished(QNetworkReply *reply);
|
||||
void updateRegularCheck(bool active);
|
||||
};
|
||||
|
||||
bool m_checking{false};
|
||||
bool checking() const;
|
||||
void setChecking(bool checking);
|
||||
};
|
||||
|
@ -1,9 +1,11 @@
|
||||
import QtQuick 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
import "private"
|
||||
|
||||
/*!
|
||||
\qmltype StatusDotsLoadingIndicator
|
||||
\inherits Control
|
||||
@ -39,7 +41,7 @@ Control {
|
||||
|
||||
/*!
|
||||
\qmlproperty string StatusDotsLoadingIndicator::duration
|
||||
This property holds the duration of the animation.
|
||||
This property holds the duration of the animation in milliseconds
|
||||
*/
|
||||
property int duration: 1500
|
||||
|
||||
@ -59,33 +61,11 @@ Control {
|
||||
|
||||
spacing: 2
|
||||
|
||||
component DotItem: Rectangle{
|
||||
id: dotItem
|
||||
|
||||
property double maxOpacity
|
||||
|
||||
width: root.dotsDiameter
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: root.dotsColor
|
||||
|
||||
SequentialAnimation {
|
||||
id: blinkingAnimation
|
||||
|
||||
loops: Animation.Infinite
|
||||
running: visible
|
||||
NumberAnimation { target: dotItem; property: "opacity"; to: 0; duration: root.duration;}
|
||||
NumberAnimation { target: dotItem; property: "opacity"; to: dotItem.maxOpacity; duration: root.duration;}
|
||||
}
|
||||
|
||||
Component.onCompleted: blinkingAnimation.start()
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: root.spacing
|
||||
|
||||
DotItem { id: firstDot; maxOpacity: d.opacity1}
|
||||
DotItem { id: secondDot; maxOpacity: d.opacity2}
|
||||
DotItem { id: thirdDot; maxOpacity: d.opacity3}
|
||||
LoadingDotItem { dotsDiameter: root.dotsDiameter; duration: root.duration; dotsColor: root.dotsColor; maxOpacity: d.opacity1 }
|
||||
LoadingDotItem { dotsDiameter: root.dotsDiameter; duration: root.duration; dotsColor: root.dotsColor; maxOpacity: d.opacity2 }
|
||||
LoadingDotItem { dotsDiameter: root.dotsDiameter; duration: root.duration; dotsColor: root.dotsColor; maxOpacity: d.opacity3 }
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import QtQuick 2.13
|
||||
import QtQuick.Window 2.15
|
||||
import QtQuick 2.15
|
||||
|
||||
/*!
|
||||
\qmltype StatusImage
|
||||
|
@ -18,6 +18,8 @@ Loader {
|
||||
property StatusAssetSettings asset: StatusAssetSettings {
|
||||
width: 40
|
||||
height: 40
|
||||
bgWidth: width
|
||||
bgHeight: height
|
||||
bgRadius: bgWidth / 2
|
||||
}
|
||||
|
||||
@ -53,6 +55,7 @@ Loader {
|
||||
objectName: "statusRoundImage"
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
radius: asset.bgRadius
|
||||
image.source: root.asset.isImage ? root.asset.name : ""
|
||||
showLoadingIndicator: true
|
||||
border.width: root.asset.imgIsIdenticon ? 1 : 0
|
||||
|
28
ui/StatusQ/src/StatusQ/Components/private/LoadingDotItem.qml
Normal file
@ -0,0 +1,28 @@
|
||||
import QtQuick 2.15
|
||||
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property double dotsDiameter
|
||||
property int duration
|
||||
property double maxOpacity
|
||||
property color dotsColor
|
||||
|
||||
width: root.dotsDiameter
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: root.dotsColor
|
||||
|
||||
SequentialAnimation {
|
||||
id: blinkingAnimation
|
||||
|
||||
loops: Animation.Infinite
|
||||
running: visible
|
||||
NumberAnimation { target: root; property: "opacity"; to: 0; duration: root.duration }
|
||||
NumberAnimation { target: root; property: "opacity"; to: root.maxOpacity; duration: root.duration }
|
||||
}
|
||||
|
||||
Component.onCompleted: blinkingAnimation.start()
|
||||
}
|
@ -8,12 +8,10 @@ StatusAnimatedImage 0.1 StatusAnimatedImage.qml
|
||||
StatusBadge 0.1 StatusBadge.qml
|
||||
StatusBetaTag 0.1 StatusBetaTag.qml
|
||||
StatusCard 0.1 StatusCard.qml
|
||||
StatusChart 0.1 StatusChart.qml
|
||||
StatusChartPanel 0.1 StatusChartPanel.qml
|
||||
StatusChatInfoToolBar 0.1 StatusChatInfoToolBar.qml
|
||||
StatusChatList 0.1 StatusChatList.qml
|
||||
StatusChatListAndCategories 0.1 StatusChatListAndCategories.qml
|
||||
StatusChatListCategory 0.1 StatusChatListCategory.qml
|
||||
StatusChatListCategoryItem 0.1 StatusChatListCategoryItem.qml
|
||||
StatusChatListItem 0.1 StatusChatListItem.qml
|
||||
StatusColorSpace 0.0 StatusColorSpace.qml
|
||||
@ -23,7 +21,6 @@ StatusContactRequestsIndicatorListItem 0.1 StatusContactRequestsIndicatorListIte
|
||||
StatusContactVerificationIcons 0.1 StatusContactVerificationIcons.qml
|
||||
StatusCursorDelegate 0.1 StatusCursorDelegate.qml
|
||||
StatusDateGroupLabel 0.1 StatusDateGroupLabel.qml
|
||||
StatusDateInput 0.1 StatusDateInput.qml
|
||||
StatusDatePicker 0.1 StatusDatePicker.qml
|
||||
StatusDescriptionListItem 0.1 StatusDescriptionListItem.qml
|
||||
StatusDotsLoadingIndicator 0.1 StatusDotsLoadingIndicator.qml
|
||||
|
@ -11,6 +11,7 @@ ItemDelegate {
|
||||
property bool centerTextHorizontally: false
|
||||
property int radius: 0
|
||||
property int cursorShape: Qt.PointingHandCursor
|
||||
property color highlightColor: Theme.palette.statusMenu.hoverBackgroundColor
|
||||
|
||||
padding: 8
|
||||
spacing: 8
|
||||
@ -19,7 +20,7 @@ ItemDelegate {
|
||||
icon.height: 16
|
||||
|
||||
font.family: Theme.baseFont.name
|
||||
font.pixelSize: 15
|
||||
font.pixelSize: Theme.primaryTextFontSize
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: root.spacing
|
||||
@ -40,7 +41,7 @@ ItemDelegate {
|
||||
text: root.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
color: root.enabled ? Theme.palette.directColor1 : Theme.palette.baseColor1
|
||||
color: root.highlighted ? Theme.palette.white : root.enabled ? Theme.palette.directColor1 : Theme.palette.baseColor1
|
||||
|
||||
Binding on horizontalAlignment {
|
||||
when: root.centerTextHorizontally
|
||||
@ -50,16 +51,11 @@ ItemDelegate {
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: root.highlighted
|
||||
? Theme.palette.statusMenu.hoverBackgroundColor
|
||||
: "transparent"
|
||||
|
||||
color: root.highlighted ? root.highlightColor : "transparent"
|
||||
radius: root.radius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: root.cursorShape
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
HoverHandler {
|
||||
cursorShape: root.cursorShape
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ StatusProgressBar {
|
||||
|
||||
Default value: "So-so"
|
||||
*/
|
||||
property string labelSoso: qsTr("So-so")
|
||||
property string labelSoso: qsTr("Okay")
|
||||
/*!
|
||||
\qmlproperty string StatusPasswordStrengthIndicator::labelGood
|
||||
This property holds the text shown when the strength is StatusPasswordStrengthIndicator.Strength.Good.
|
||||
@ -88,7 +88,7 @@ StatusProgressBar {
|
||||
|
||||
Default value: "Great"
|
||||
*/
|
||||
property string labelGreat: qsTr("Great")
|
||||
property string labelGreat: qsTr("Very strong")
|
||||
|
||||
enum Strength {
|
||||
None, // 0
|
||||
|
@ -1,4 +1,5 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick 2.15
|
||||
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Controls.Validators 0.1
|
||||
|
||||
@ -42,7 +43,7 @@ Item {
|
||||
property alias pinInput: inputText.text
|
||||
|
||||
/*!
|
||||
\qmlproperty Validator StatusPinInput::validator
|
||||
\qmlproperty StatusValidator StatusPinInput::validator
|
||||
This property allows you to set a validator on the StatusPinInput. When a validator is set the StatusPinInput will only accept
|
||||
input which leaves the pinInput property in an acceptable state.
|
||||
|
||||
@ -59,6 +60,13 @@ Item {
|
||||
*/
|
||||
property alias validator: d.statusValidator
|
||||
|
||||
/*!
|
||||
\qmlproperty bool StatusPinInput::pinInput
|
||||
This property holds whether the entered PIN is valid; PIN is considered valid when it passes the internal validator
|
||||
and its length matches that of @p pinLen
|
||||
*/
|
||||
readonly property bool valid: inputText.acceptableInput && inputText.length === pinLen
|
||||
|
||||
/*!
|
||||
\qmlproperty int StatusPinInput::pinLen
|
||||
This property allows you to set a specific pin input length. The default value is 6.
|
||||
@ -169,6 +177,23 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
\qmlmethod StatusPinInput::clearPin()
|
||||
|
||||
Sets the pin input to an empty string, setting state of each digit to "EMPTY", and stops the blinking animation
|
||||
|
||||
Doesn't change the current `pinLen`.
|
||||
*/
|
||||
function clearPin() {
|
||||
inputText.text = ""
|
||||
d.currentPinIndex = 0
|
||||
d.deactivateBlink()
|
||||
for (var i = 0; i < root.pinLen; i++) {
|
||||
const currItem = repeater.itemAt(i)
|
||||
currItem.innerState = "EMPTY"
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: childrenRect.width
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import QtQuick 2.12
|
||||
import QtGraphicalEffects 1.13
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Popups 0.1
|
||||
@ -75,6 +74,11 @@ Item {
|
||||
input text.
|
||||
*/
|
||||
property ListModel filteredList: ListModel { }
|
||||
|
||||
property bool isError
|
||||
|
||||
readonly property bool suggestionsOpened: suggListContainer.opened
|
||||
|
||||
/*!
|
||||
\qmlsignal doneInsertingWord
|
||||
This signal is emitted when the user selects a word from the suggestions list
|
||||
@ -117,11 +121,14 @@ Item {
|
||||
Component {
|
||||
id: seedInputLeftComponent
|
||||
StatusBaseText {
|
||||
leftPadding: 4
|
||||
rightPadding: 6
|
||||
leftPadding: text.length == 1 ? 10 : 6
|
||||
rightPadding: 4
|
||||
text: root.leftComponentText
|
||||
color: seedWordInput.input.edit.activeFocus ?
|
||||
Theme.palette.primaryColor1 : Theme.palette.baseColor1
|
||||
font.family: Theme.monoFont.name
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
color: root.isError ? Theme.palette.dangerColor1
|
||||
: seedWordInput.input.edit.activeFocus ? Theme.palette.primaryColor1
|
||||
: Theme.palette.baseColor1
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +138,12 @@ Item {
|
||||
implicitWidth: parent.width
|
||||
input.leftComponent: seedInputLeftComponent
|
||||
input.acceptReturn: true
|
||||
|
||||
Binding on input.background.border.color {
|
||||
value: Theme.palette.dangerColor1
|
||||
when: root.isError && seedWordInput.input.edit.activeFocus
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
filteredList.clear();
|
||||
let textToCheck = text.trim().toLowerCase()
|
||||
@ -197,7 +210,7 @@ Item {
|
||||
id: suggListContainer
|
||||
contentWidth: seedSuggestionsList.width
|
||||
contentHeight: ((seedSuggestionsList.count <= 5) ? seedSuggestionsList.count : 5) *34
|
||||
x: 16
|
||||
x: 0
|
||||
y: seedWordInput.height + 4
|
||||
topPadding: 8
|
||||
bottomPadding: 8
|
||||
|
@ -1,4 +1,4 @@
|
||||
import QtQuick 2.14
|
||||
import QtQuick 2.15
|
||||
|
||||
import StatusQ.Controls 0.1
|
||||
|
||||
|
@ -86,5 +86,8 @@ QtObject {
|
||||
|
||||
'lightDesktopBlue10': '#ECEFFB',
|
||||
'darkDesktopBlue10': '#273251',
|
||||
|
||||
// new/mobile colors
|
||||
'neutral-95': '#0D1625'
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ ThemePalette {
|
||||
miscColor11: getColor('brown2')
|
||||
miscColor12: getColor('green5')
|
||||
|
||||
dropShadow: Qt.rgba(0, 34/255, 51/255, 0.03)
|
||||
dropShadow2: getColor('blue7', 0.02)
|
||||
|
||||
statusFloatingButtonHighlight: getColor('blueHijab')
|
||||
|
@ -14,7 +14,7 @@ QtObject {
|
||||
property color blue: getColor('blue')
|
||||
property color darkBlue: getColor('blue2')
|
||||
|
||||
property color dropShadow: getColor('black', 0.12)
|
||||
property color dropShadow
|
||||
property color dropShadow2
|
||||
property color backdropColor: getColor('black', 0.4)
|
||||
|
||||
|
@ -279,4 +279,25 @@ QtObject {
|
||||
function stripHttpsAndwwwFromUrl(text) {
|
||||
return text.replace(/http(s)?(:)?(\/\/)?|(\/\/)?(www\.)?(\/)/gim, '')
|
||||
}
|
||||
|
||||
/**
|
||||
- given a contiguous array of non repeating numbers from [0..totalCount-1]
|
||||
- @return an array of @p n random numbers, sorted in ascending order
|
||||
Example:
|
||||
const arr = [0, 1, 2, 3, 4, 5]
|
||||
const indexes = nSamples(3, 6) // pick 3 random numbers from an array of 6 elements [0..5]
|
||||
console.log(indexes) -> Array[0, 4, 5] // example output
|
||||
*/
|
||||
function nSamples(n, totalCount) {
|
||||
if (n > totalCount) {
|
||||
console.error("'n' must be less than or equal to 'totalCount'")
|
||||
return
|
||||
}
|
||||
|
||||
let set = new Set()
|
||||
while (set.size < n) {
|
||||
set.add(~~(Math.random() * totalCount))
|
||||
}
|
||||
return [...set].sort((a, b) => a - b)
|
||||
}
|
||||
}
|
||||
|
24
ui/StatusQ/src/StatusQ/Popups/StatusSimpleTextPopup.qml
Normal file
@ -0,0 +1,24 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Popups.Dialog 0.1
|
||||
|
||||
StatusDialog {
|
||||
width: 600
|
||||
padding: 0
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
property alias content: contentText
|
||||
|
||||
StatusScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
contentWidth: availableWidth
|
||||
StatusBaseText {
|
||||
id: contentText
|
||||
width: scrollView.availableWidth
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
@ -14,5 +14,6 @@ StatusModalDivider 0.1 StatusModalDivider.qml
|
||||
StatusSearchLocationMenu 0.1 StatusSearchLocationMenu.qml
|
||||
StatusSearchPopup 0.1 StatusSearchPopup.qml
|
||||
StatusSearchPopupMenuItem 0.1 StatusSearchPopupMenuItem.qml
|
||||
StatusSimpleTextPopup 0.1 StatusSimpleTextPopup.qml
|
||||
StatusStackModal 0.1 StatusStackModal.qml
|
||||
StatusSuccessAction 0.1 StatusSuccessAction.qml
|
||||
|
@ -8340,6 +8340,35 @@
|
||||
<file>assets/png/onboarding/profile_fetching_in_progress.png</file>
|
||||
<file>assets/png/onboarding/seed-phrase.png</file>
|
||||
<file>assets/png/onboarding/welcome.png</file>
|
||||
<file>assets/png/onboarding/status_totebag_artwork_1.png</file>
|
||||
<file>assets/png/onboarding/status_generate_keys.png</file>
|
||||
<file>assets/png/onboarding/status_generate_keycard.png</file>
|
||||
<file>assets/png/onboarding/create_profile_seed.png</file>
|
||||
<file>assets/png/onboarding/create_profile_keycard.png</file>
|
||||
<file>assets/png/onboarding/login_syncing.png</file>
|
||||
<file>assets/png/onboarding/status_chat.png</file>
|
||||
<file>assets/png/onboarding/status_login_seedphrase.png</file>
|
||||
<file>assets/png/onboarding/status_key.png</file>
|
||||
<file>assets/png/onboarding/status_keycard.png</file>
|
||||
<file>assets/png/onboarding/status_keycard_multiple.png</file>
|
||||
<file>assets/png/onboarding/status_keycard_adding_keypair.png</file>
|
||||
<file>assets/png/onboarding/status_keycard_adding_keypair_success.png</file>
|
||||
<file>assets/png/onboarding/status_keycard_adding_keypair_failed.png</file>
|
||||
<file>assets/png/onboarding/status_seedphrase.png</file>
|
||||
<file>assets/png/onboarding/status_sync.png</file>
|
||||
<file>assets/png/onboarding/status_sync_progress.png</file>
|
||||
<file>assets/png/onboarding/status_sync_success.png</file>
|
||||
<file>assets/png/onboarding/status_sync_failed.png</file>
|
||||
<file>assets/png/onboarding/enable_biometrics.png</file>
|
||||
<file>assets/png/onboarding/carousel/crypto.png</file>
|
||||
<file>assets/png/onboarding/carousel/chat.png</file>
|
||||
<file>assets/png/onboarding/carousel/keycard.png</file>
|
||||
<file>assets/png/onboarding/keycard/empty.png</file>
|
||||
<file>assets/png/onboarding/keycard/insert.png</file>
|
||||
<file>assets/png/onboarding/keycard/invalid.png</file>
|
||||
<file>assets/png/onboarding/keycard/reading.png</file>
|
||||
<file>assets/png/onboarding/keycard/error.png</file>
|
||||
<file>assets/png/onboarding/keycard/success.png</file>
|
||||
<file>assets/png/onRampProviders/latamex.png</file>
|
||||
<file>assets/png/onRampProviders/mercuryo.png</file>
|
||||
<file>assets/png/onRampProviders/moonPay.png</file>
|
||||
@ -8978,6 +9007,7 @@
|
||||
<file>assets/png/status-logo-dev-round-rect.png</file>
|
||||
<file>assets/png/status-logo-icon.png</file>
|
||||
<file>assets/png/status-logo-round-rect.png</file>
|
||||
<file>assets/png/status-preparing.png</file>
|
||||
<file>assets/png/unfurling-image.png</file>
|
||||
<file>assets/img/icons/arrow-next.svg</file>
|
||||
<file>assets/img/icons/arrow-previous.svg</file>
|
||||
|
BIN
ui/StatusQ/src/assets/png/onboarding/carousel/chat.png
Normal file
After Width: | Height: | Size: 515 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/carousel/crypto.png
Normal file
After Width: | Height: | Size: 682 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/carousel/keycard.png
Normal file
After Width: | Height: | Size: 475 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/create_profile_keycard.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/create_profile_seed.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/enable_biometrics.png
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/keycard/empty.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/keycard/error.png
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/keycard/insert.png
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/keycard/invalid.png
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/keycard/reading.png
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/keycard/success.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/login_syncing.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_chat.png
Normal file
After Width: | Height: | Size: 257 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_generate_keycard.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_generate_keys.png
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_key.png
Normal file
After Width: | Height: | Size: 149 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_keycard.png
Normal file
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 114 KiB |
After Width: | Height: | Size: 115 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_keycard_multiple.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_login_seedphrase.png
Normal file
After Width: | Height: | Size: 320 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_seedphrase.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_sync.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_sync_failed.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_sync_progress.png
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
ui/StatusQ/src/assets/png/onboarding/status_sync_success.png
Normal file
After Width: | Height: | Size: 129 KiB |
After Width: | Height: | Size: 431 KiB |
Before Width: | Height: | Size: 778 KiB After Width: | Height: | Size: 776 KiB |
BIN
ui/StatusQ/src/assets/png/status-preparing.png
Normal file
After Width: | Height: | Size: 364 KiB |
@ -1,12 +1,17 @@
|
||||
#include "StatusQ/networkchecker.h"
|
||||
|
||||
NetworkChecker::NetworkChecker(QObject* parent)
|
||||
namespace {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
constexpr static auto checkInterval = 30s;
|
||||
}
|
||||
|
||||
NetworkChecker::NetworkChecker(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
manager.setTransferTimeout();
|
||||
connect(&manager, &QNetworkAccessManager::finished, this, &NetworkChecker::onFinished);
|
||||
connect(&timer, &QTimer::timeout, this, &NetworkChecker::checkNetwork);
|
||||
|
||||
updateRegularCheck(active);
|
||||
}
|
||||
|
||||
bool NetworkChecker::isOnline() const
|
||||
@ -18,16 +23,26 @@ void NetworkChecker::checkNetwork()
|
||||
{
|
||||
QNetworkRequest request(QUrl(QStringLiteral("http://fedoraproject.org/static/hotspot.txt")));
|
||||
manager.get(request);
|
||||
setChecking(true);
|
||||
}
|
||||
|
||||
void NetworkChecker::onFinished(QNetworkReply* reply)
|
||||
void NetworkChecker::classBegin()
|
||||
{
|
||||
bool wasOnline = online;
|
||||
// empty on purpose
|
||||
}
|
||||
|
||||
void NetworkChecker::componentComplete() {
|
||||
updateRegularCheck(active);
|
||||
}
|
||||
|
||||
void NetworkChecker::onFinished(QNetworkReply *reply)
|
||||
{
|
||||
setChecking(false);
|
||||
const auto wasOnline = online;
|
||||
online = (reply->error() == QNetworkReply::NoError);
|
||||
reply->deleteLater();
|
||||
|
||||
if(wasOnline != online)
|
||||
{
|
||||
if (wasOnline != online) {
|
||||
emit isOnlineChanged(online);
|
||||
}
|
||||
}
|
||||
@ -39,7 +54,8 @@ bool NetworkChecker::isActive() const
|
||||
|
||||
void NetworkChecker::setActive(bool active)
|
||||
{
|
||||
if(active == this->active) return;
|
||||
if (active == this->active)
|
||||
return;
|
||||
|
||||
this->active = active;
|
||||
emit activeChanged(active);
|
||||
@ -49,13 +65,24 @@ void NetworkChecker::setActive(bool active)
|
||||
|
||||
void NetworkChecker::updateRegularCheck(bool active)
|
||||
{
|
||||
if(active)
|
||||
{
|
||||
if (active) {
|
||||
checkNetwork();
|
||||
timer.start(checkInterval);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
timer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool NetworkChecker::checking() const
|
||||
{
|
||||
return m_checking;
|
||||
}
|
||||
|
||||
void NetworkChecker::setChecking(bool checking)
|
||||
{
|
||||
if (m_checking == checking)
|
||||
return;
|
||||
|
||||
m_checking = checking;
|
||||
emit checkingChanged();
|
||||
}
|
||||
|
61
ui/StatusQ/src/onboarding/enums.h
Normal file
@ -0,0 +1,61 @@
|
||||
#include <QObject>
|
||||
|
||||
class OnboardingEnums
|
||||
{
|
||||
Q_GADGET
|
||||
Q_CLASSINFO("RegisterEnumClassesUnscoped", "false")
|
||||
public:
|
||||
enum class PrimaryFlow {
|
||||
Unknown,
|
||||
CreateProfile,
|
||||
Login
|
||||
};
|
||||
|
||||
enum class SecondaryFlow {
|
||||
Unknown,
|
||||
|
||||
CreateProfileWithPassword,
|
||||
CreateProfileWithSeedphrase,
|
||||
CreateProfileWithKeycard,
|
||||
CreateProfileWithKeycardNewSeedphrase,
|
||||
CreateProfileWithKeycardExistingSeedphrase,
|
||||
|
||||
LoginWithSeedphrase,
|
||||
LoginWithSyncing,
|
||||
LoginWithKeycard
|
||||
};
|
||||
|
||||
enum class KeycardState {
|
||||
NoPCSCService,
|
||||
PluginReader,
|
||||
InsertKeycard,
|
||||
ReadingKeycard,
|
||||
// error states
|
||||
WrongKeycard,
|
||||
NotKeycard,
|
||||
MaxPairingSlotsReached,
|
||||
Locked,
|
||||
// exit states
|
||||
NotEmpty,
|
||||
Empty
|
||||
};
|
||||
|
||||
enum class AddKeyPairState {
|
||||
InProgress,
|
||||
Success,
|
||||
Failed
|
||||
};
|
||||
|
||||
enum class SyncState {
|
||||
InProgress,
|
||||
Success,
|
||||
Failed
|
||||
};
|
||||
|
||||
private:
|
||||
Q_ENUM(PrimaryFlow)
|
||||
Q_ENUM(SecondaryFlow)
|
||||
Q_ENUM(KeycardState)
|
||||
Q_ENUM(AddKeyPairState)
|
||||
Q_ENUM(SyncState)
|
||||
};
|
@ -66,6 +66,7 @@
|
||||
<file>StatusQ/Components/StatusVideo.qml</file>
|
||||
<file>StatusQ/Components/StatusWizardStepper.qml</file>
|
||||
<file>StatusQ/Components/WebEngineLoader.qml</file>
|
||||
<file>StatusQ/Components/private/LoadingDotItem.qml</file>
|
||||
<file>StatusQ/Components/private/StatusComboboxBackground.qml</file>
|
||||
<file>StatusQ/Components/private/StatusComboboxIndicator.qml</file>
|
||||
<file>StatusQ/Components/private/chart/ChartCanvas.qml</file>
|
||||
@ -246,6 +247,7 @@
|
||||
<file>StatusQ/Popups/StatusSearchLocationMenu.qml</file>
|
||||
<file>StatusQ/Popups/StatusSearchPopup.qml</file>
|
||||
<file>StatusQ/Popups/StatusSearchPopupMenuItem.qml</file>
|
||||
<file>StatusQ/Popups/StatusSimpleTextPopup.qml</file>
|
||||
<file>StatusQ/Popups/StatusStackModal.qml</file>
|
||||
<file>StatusQ/Popups/StatusSuccessAction.qml</file>
|
||||
<file>StatusQ/Popups/qmldir</file>
|
||||
|
@ -35,6 +35,8 @@
|
||||
#include "wallet/managetokenscontroller.h"
|
||||
#include "wallet/managetokensmodel.h"
|
||||
|
||||
#include "onboarding/enums.h"
|
||||
|
||||
void registerStatusQTypes() {
|
||||
qmlRegisterType<StatusWindow>("StatusQ", 0, 1, "StatusWindow");
|
||||
qmlRegisterType<StatusSyntaxHighlighter>("StatusQ", 0, 1, "StatusSyntaxHighlighter");
|
||||
@ -102,6 +104,10 @@ void registerStatusQTypes() {
|
||||
return new PermissionUtilsInternal;
|
||||
});
|
||||
|
||||
// onboarding
|
||||
qmlRegisterUncreatableType<OnboardingEnums>("AppLayouts.Onboarding.enums", 1, 0,
|
||||
"Onboarding", "This is an enum type, cannot be created directly.");
|
||||
|
||||
QZXing::registerQMLTypes();
|
||||
qqsfpm::registerTypes();
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ OnboardingBasePage {
|
||||
if (error === Constants.existingAccountError) {
|
||||
msgDialog.title = qsTr("Keys for this account already exist")
|
||||
msgDialog.text = qsTr("Keys for this account already exist and can't be added again. If you've lost \
|
||||
your password, passcode or Keycard, uninstall the app, reinstall and access your keys by entering your seed phrase.")
|
||||
your password, passcode or Keycard, uninstall the app, reinstall and access your keys by entering your recovery phrase.")
|
||||
} else {
|
||||
msgDialog.title = qsTr("Login failed")
|
||||
msgDialog.text = qsTr("Login failed. Please re-enter your password and try again.")
|
||||
@ -167,7 +167,7 @@ your password, passcode or Keycard, uninstall the app, reinstall and access your
|
||||
if (error === Constants.existingAccountError) {
|
||||
msgDialog.title = qsTr("Keys for this account already exist")
|
||||
msgDialog.text = qsTr("Keys for this account already exist and can't be added again. If you've lost \
|
||||
your password, passcode or Keycard, uninstall the app, reinstall and access your keys by entering your seed phrase. In \
|
||||
your password, passcode or Keycard, uninstall the app, reinstall and access your keys by entering your recovery phrase. In \
|
||||
case of Keycard try recovering using PUK or reinstall the app and try login with the Keycard option.")
|
||||
} else {
|
||||
msgDialog.title = qsTr("Error importing seed")
|
||||
@ -179,7 +179,7 @@ case of Keycard try recovering using PUK or reinstall the app and try login with
|
||||
msgDialog.text = qsTr("Really sorry about this inconvenience.\n\
|
||||
Most likely that your account is damaged while converting to a regular Status account.\n\
|
||||
First try to login after app restart, if that doesn't work, you can alway recover your account\n\
|
||||
following the \"Add existing Status user\" flow, using your seed phrase.")
|
||||
following the \"Add existing Status user\" flow, using your recovery phrase.")
|
||||
}
|
||||
|
||||
msgDialog.open()
|
||||
@ -202,7 +202,6 @@ following the \"Add existing Status user\" flow, using your seed phrase.")
|
||||
|
||||
StatusBaseText {
|
||||
anchors.fill: parent
|
||||
font.pixelSize: 15
|
||||
color: Theme.palette.directColor1
|
||||
text: msgDialog.text
|
||||
wrapMode: Text.WordWrap
|
||||
@ -210,7 +209,7 @@ following the \"Add existing Status user\" flow, using your seed phrase.")
|
||||
|
||||
standardButtons: Dialog.Ok
|
||||
onAccepted: {
|
||||
if (msgDialog.errType == Constants.startupErrorType.convertToRegularAccError) {
|
||||
if (msgDialog.errType === Constants.startupErrorType.convertToRegularAccError) {
|
||||
Qt.quit();
|
||||
}
|
||||
console.log("TODO: restart flow...")
|
||||
|
@ -12,7 +12,7 @@ import shared.popups 1.0
|
||||
// TODO: replace with StatusModal
|
||||
ModalPopup {
|
||||
id: popup
|
||||
title: qsTr("Enter seed phrase")
|
||||
title: qsTr("Enter recovery phrase")
|
||||
height: 200
|
||||
signal openModalClicked()
|
||||
|
||||
|
@ -349,7 +349,7 @@ Item {
|
||||
}
|
||||
PropertyChanges {
|
||||
target: button
|
||||
text: qsTr("Unlock using seed phrase")
|
||||
text: qsTr("Unlock using recovery phrase")
|
||||
type: StatusBaseButton.Type.Normal
|
||||
}
|
||||
PropertyChanges {
|
||||
@ -420,19 +420,19 @@ Item {
|
||||
}
|
||||
PropertyChanges {
|
||||
target: title
|
||||
text: qsTr("Seed phrase doesn’t match any user")
|
||||
text: qsTr("Recovery phrase doesn’t match any user")
|
||||
color: Theme.palette.directColor1
|
||||
font.pixelSize: Constants.keycard.general.fontSize1
|
||||
}
|
||||
PropertyChanges {
|
||||
target: info
|
||||
text: qsTr("The seed phrase you enter needs to match the seed phrase of an existing user on this device")
|
||||
text: qsTr("The recovery phrase you enter needs to match the recovery phrase of an existing user on this device")
|
||||
color: Theme.palette.directColor1
|
||||
font.pixelSize: Constants.keycard.general.fontSize2
|
||||
}
|
||||
PropertyChanges {
|
||||
target: button
|
||||
text: qsTr("Try entering seed phrase again")
|
||||
text: qsTr("Try entering recovery phrase again")
|
||||
}
|
||||
PropertyChanges {
|
||||
target: link
|
||||
|
@ -386,7 +386,7 @@ Item {
|
||||
}
|
||||
PropertyChanges {
|
||||
target: button3
|
||||
text: qsTr("Enter a seed phrase")
|
||||
text: qsTr("Enter a recovery phrase")
|
||||
}
|
||||
},
|
||||
State {
|
||||
@ -421,7 +421,7 @@ Item {
|
||||
}
|
||||
PropertyChanges {
|
||||
target: button3
|
||||
text: qsTr("Import a seed phrase")
|
||||
text: qsTr("Import a recovery phrase")
|
||||
}
|
||||
},
|
||||
State {
|
||||
@ -429,7 +429,7 @@ Item {
|
||||
when: root.startupStore.currentStartupState.stateType === Constants.startupState.userProfileImportSeedPhrase
|
||||
PropertyChanges {
|
||||
target: txtTitle
|
||||
text: qsTr("Import a seed phrase")
|
||||
text: qsTr("Import a recovery phrase")
|
||||
}
|
||||
PropertyChanges {
|
||||
target: keysImg
|
||||
@ -439,12 +439,12 @@ Item {
|
||||
}
|
||||
PropertyChanges {
|
||||
target: txtDesc
|
||||
text: qsTr("Seed phrases are used to back up and restore your keys.\nOnly use this option if you already have a seed phrase.")
|
||||
text: qsTr("Recovery phrases are used to back up and restore your keys.\nOnly use this option if you already have a recovery phrase.")
|
||||
height: Constants.onboarding.loginInfoHeight2
|
||||
}
|
||||
PropertyChanges {
|
||||
target: button1
|
||||
text: qsTr("Import a seed phrase")
|
||||
text: qsTr("Import a recovery phrase")
|
||||
}
|
||||
PropertyChanges {
|
||||
target: betaTagButton1
|
||||
@ -452,7 +452,7 @@ Item {
|
||||
}
|
||||
PropertyChanges {
|
||||
target: button2
|
||||
text: qsTr("Import a seed phrase into a new Keycard")
|
||||
text: qsTr("Import a recovery phrase into a new Keycard")
|
||||
}
|
||||
PropertyChanges {
|
||||
target: button3
|
||||
@ -547,7 +547,7 @@ Item {
|
||||
}
|
||||
PropertyChanges {
|
||||
target: button1
|
||||
text: qsTr("Create replacement Keycard with seed phrase")
|
||||
text: qsTr("Create replacement Keycard with recovery phrase")
|
||||
}
|
||||
PropertyChanges {
|
||||
target: betaTagButton1
|
||||
|
@ -29,10 +29,10 @@ Item {
|
||||
onWrongSeedPhraseChanged: {
|
||||
if (wrongSeedPhrase) {
|
||||
if (root.startupStore.startupModuleInst.flowType === Constants.startupFlow.firstRunOldUserImportSeedPhrase) {
|
||||
seedPhraseView.setWrongSeedPhraseMessage(qsTr("Profile key pair for the inserted seed phrase is already set up"))
|
||||
seedPhraseView.setWrongSeedPhraseMessage(qsTr("Profile key pair for the inserted recovery phrase is already set up"))
|
||||
return
|
||||
}
|
||||
seedPhraseView.setWrongSeedPhraseMessage(qsTr("Seed phrase doesn’t match the profile of an existing Keycard user on this device"))
|
||||
seedPhraseView.setWrongSeedPhraseMessage(qsTr("Recovery phrase doesn’t match the profile of an existing Keycard user on this device"))
|
||||
}
|
||||
else {
|
||||
seedPhraseView.setWrongSeedPhraseMessage("")
|
||||
@ -52,7 +52,7 @@ Item {
|
||||
font.weight: Font.Bold
|
||||
color: Theme.palette.directColor1
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Enter seed phrase")
|
||||
text: qsTr("Enter recovery phrase")
|
||||
}
|
||||
|
||||
EnterSeedPhrase {
|
||||
|
@ -50,7 +50,7 @@ Item {
|
||||
font.pixelSize: Constants.keycard.general.fontSize1
|
||||
font.weight: Font.Bold
|
||||
color: Theme.palette.directColor1
|
||||
text: qsTr("Write down your seed phrase")
|
||||
text: qsTr("Write down your recovery phrase")
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
|
@ -75,7 +75,7 @@ Item {
|
||||
font.pixelSize: Constants.keycard.general.fontSize1
|
||||
font.weight: Font.Bold
|
||||
color: Theme.palette.directColor1
|
||||
text: qsTr("Enter seed phrase words")
|
||||
text: qsTr("Enter recovery phrase words")
|
||||
}
|
||||
|
||||
Item {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import QtQuick 2.13
|
||||
import QtQuick 2.15
|
||||
|
||||
import shared.popups 1.0
|
||||
import shared.views 1.0
|
||||
|
591
ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml
Normal file
@ -0,0 +1,591 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import Qt.labs.settings 1.1
|
||||
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Core.Utils 0.1 as SQUtils
|
||||
import StatusQ.Core.Backpressure 0.1
|
||||
import StatusQ.Popups 0.1
|
||||
|
||||
import AppLayouts.Onboarding2.pages 1.0
|
||||
import AppLayouts.Onboarding2.stores 1.0
|
||||
import AppLayouts.Onboarding.enums 1.0
|
||||
|
||||
import shared.stores 1.0 as SharedStores
|
||||
|
||||
import utils 1.0
|
||||
|
||||
Page {
|
||||
id: root
|
||||
|
||||
required property OnboardingStore onboardingStore
|
||||
|
||||
// TODO backend: externalize the metrics handling too?
|
||||
required property SharedStores.MetricsStore metricsStore
|
||||
|
||||
property int splashScreenDurationMs: 30000
|
||||
property bool biometricsAvailable: Qt.platform.os === Constants.mac
|
||||
required property bool networkChecksEnabled
|
||||
|
||||
readonly property alias stack: stack
|
||||
readonly property alias primaryFlow: d.primaryFlow // Onboarding.PrimaryFlow enum
|
||||
readonly property alias secondaryFlow: d.secondaryFlow // Onboarding.SecondaryFlow enum
|
||||
|
||||
signal finished(int primaryFlow, int secondaryFlow, var data)
|
||||
signal keycardFactoryResetRequested() // TODO integrate/switch to an external flow, needed?
|
||||
signal keycardReloaded()
|
||||
|
||||
function restartFlow() {
|
||||
stack.clear()
|
||||
stack.push(welcomePage)
|
||||
d.resetState()
|
||||
d.settings.reset()
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
// logic
|
||||
property int primaryFlow: Onboarding.PrimaryFlow.Unknown
|
||||
property int secondaryFlow: Onboarding.SecondaryFlow.Unknown
|
||||
readonly property int currentKeycardState: root.onboardingStore.keycardState
|
||||
readonly property var seedWords: root.onboardingStore.getMnemonic().split(" ")
|
||||
readonly property int numWordsToVerify: 4
|
||||
|
||||
// UI
|
||||
readonly property int opacityDuration: 50
|
||||
readonly property int swipeDuration: 400
|
||||
|
||||
// state collected
|
||||
property string password
|
||||
property string keycardPin
|
||||
property bool enableBiometrics
|
||||
property string seedphrase
|
||||
|
||||
function resetState() {
|
||||
d.primaryFlow = Onboarding.PrimaryFlow.Unknown
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.Unknown
|
||||
d.password = ""
|
||||
d.keycardPin = ""
|
||||
d.enableBiometrics = false
|
||||
d.seedphrase = ""
|
||||
}
|
||||
|
||||
readonly property Settings settings: Settings {
|
||||
property bool keycardPromoShown // whether we've seen the keycard promo banner on KeycardIntroPage
|
||||
|
||||
function reset() {
|
||||
keycardPromoShown = false
|
||||
}
|
||||
}
|
||||
|
||||
function pushOrSkipBiometricsPage() {
|
||||
if (root.biometricsAvailable) {
|
||||
dbg.debugFlow("ENTERING BIOMETRICS PAGE")
|
||||
stack.replace(null, enableBiometricsPage)
|
||||
} else {
|
||||
dbg.debugFlow("SKIPPING BIOMETRICS PAGE")
|
||||
d.finishFlow()
|
||||
}
|
||||
}
|
||||
|
||||
function finishFlow() {
|
||||
dbg.debugFlow(`ONBOARDING FINISHED; ${d.primaryFlow} -> ${d.secondaryFlow}`)
|
||||
root.finished(d.primaryFlow, d.secondaryFlow,
|
||||
{"password": d.password, "keycardPin": d.keycardPin,
|
||||
"seedphrase": d.seedphrase, "enableBiometrics": d.enableBiometrics})
|
||||
}
|
||||
}
|
||||
|
||||
LoggingCategory {
|
||||
id: dbg
|
||||
name: "app.status.onboarding"
|
||||
|
||||
function debugFlow(message) {
|
||||
const currentPageName = stack.currentItem ? stack.currentItem.pageClassName : "<empty stack>"
|
||||
console.info(dbg, "!!!", currentPageName, "->", message)
|
||||
}
|
||||
}
|
||||
|
||||
// page stack
|
||||
StackView {
|
||||
id: stack
|
||||
objectName: "stack"
|
||||
anchors.fill: parent
|
||||
initialItem: welcomePage
|
||||
|
||||
pushEnter: Transition {
|
||||
ParallelAnimation {
|
||||
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: d.opacityDuration; easing.type: Easing.InQuint }
|
||||
NumberAnimation { property: "x"; from: (stack.mirrored ? -0.3 : 0.3) * stack.width; to: 0; duration: d.swipeDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
pushExit: Transition {
|
||||
NumberAnimation { property: "opacity"; from: 1; to: 0; duration: d.opacityDuration; easing.type: Easing.OutQuint }
|
||||
}
|
||||
popEnter: Transition {
|
||||
ParallelAnimation {
|
||||
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: d.opacityDuration; easing.type: Easing.InQuint }
|
||||
NumberAnimation { property: "x"; from: (stack.mirrored ? -0.3 : 0.3) * -stack.width; to: 0; duration: d.swipeDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
popExit: pushExit
|
||||
replaceEnter: pushEnter
|
||||
replaceExit: pushExit
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.BackButton
|
||||
enabled: stack.depth > 1 && !stack.busy
|
||||
cursorShape: undefined // fall thru
|
||||
onClicked: stack.pop()
|
||||
}
|
||||
|
||||
StatusBackButton {
|
||||
width: 44
|
||||
height: 44
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padding
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Theme.padding
|
||||
opacity: stack.depth > 1 && !stack.busy ? 1 : 0
|
||||
visible: opacity > 0
|
||||
Behavior on opacity { NumberAnimation { duration: 100 } }
|
||||
onClicked: stack.pop()
|
||||
}
|
||||
|
||||
// main signal handler
|
||||
Connections {
|
||||
id: mainHandler
|
||||
target: stack.currentItem
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
// common popups
|
||||
function onPrivacyPolicyRequested() {
|
||||
dbg.debugFlow("AUX: PRIVACY POLICY")
|
||||
privacyPolicyPopup.createObject(root).open()
|
||||
}
|
||||
function onTermsOfUseRequested() {
|
||||
dbg.debugFlow("AUX: TERMS OF USE")
|
||||
termsOfUsePopup.createObject(root).open()
|
||||
}
|
||||
function onOpenLink(link: string) {
|
||||
dbg.debugFlow(`OPEN LINK: ${link}`)
|
||||
Global.openLink(link)
|
||||
}
|
||||
function onOpenLinkWithConfirmation(link: string, domain: string) {
|
||||
dbg.debugFlow(`OPEN LINK WITH CONFIRM: ${link}`)
|
||||
Global.openLinkWithConfirmation(link, domain)
|
||||
}
|
||||
|
||||
// welcome page
|
||||
function onCreateProfileRequested() {
|
||||
dbg.debugFlow("PRIMARY: CREATE PROFILE")
|
||||
d.primaryFlow = Onboarding.PrimaryFlow.CreateProfile
|
||||
stack.push(helpUsImproveStatusPage)
|
||||
}
|
||||
function onLoginRequested() {
|
||||
dbg.debugFlow("PRIMARY: LOG IN")
|
||||
d.primaryFlow = Onboarding.PrimaryFlow.Login
|
||||
stack.push(helpUsImproveStatusPage)
|
||||
}
|
||||
|
||||
// help us improve page
|
||||
function onShareUsageDataRequested(enabled: bool) {
|
||||
dbg.debugFlow(`SHARE USAGE DATA: ${enabled}`)
|
||||
metricsStore.toggleCentralizedMetrics(enabled)
|
||||
Global.addCentralizedMetricIfEnabled("usage_data_shared", {placement: Constants.metricsEnablePlacement.onboarding})
|
||||
localAppSettings.metricsPopupSeen = true
|
||||
|
||||
if (d.primaryFlow === Onboarding.PrimaryFlow.CreateProfile)
|
||||
stack.push(createProfilePage)
|
||||
else if (d.primaryFlow === Onboarding.PrimaryFlow.Login)
|
||||
stack.push(loginPage)
|
||||
}
|
||||
|
||||
// create profile page
|
||||
function onCreateProfileWithPasswordRequested() {
|
||||
dbg.debugFlow("SECONDARY: CREATE PROFILE WITH PASSWORD")
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.CreateProfileWithPassword
|
||||
stack.push(createPasswordPage)
|
||||
}
|
||||
function onCreateProfileWithSeedphraseRequested() {
|
||||
dbg.debugFlow("SECONDARY: CREATE PROFILE WITH SEEDPHRASE")
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.CreateProfileWithSeedphrase
|
||||
stack.push(seedphrasePage, { title: qsTr("Create profile using a recovery phrase")})
|
||||
}
|
||||
function onCreateProfileWithEmptyKeycardRequested() {
|
||||
dbg.debugFlow("SECONDARY: CREATE PROFILE WITH KEYCARD")
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.CreateProfileWithKeycard
|
||||
stack.push(keycardIntroPage)
|
||||
}
|
||||
|
||||
// login page
|
||||
function onLoginWithSeedphraseRequested() {
|
||||
dbg.debugFlow("SECONDARY: LOGIN WITH SEEDPHRASE")
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.LoginWithSeedphrase
|
||||
stack.push(seedphrasePage, { title: qsTr("Log in with your Status recovery phrase")})
|
||||
}
|
||||
function onLoginWithSyncingRequested() {
|
||||
dbg.debugFlow("SECONDARY: LOGIN WITH SYNCING")
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.LoginWithSyncing
|
||||
stack.push(loginBySyncPage)
|
||||
}
|
||||
function onLoginWithKeycardRequested() {
|
||||
dbg.debugFlow("SECONDARY: LOGIN WITH KEYCARD")
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.LoginWithKeycard
|
||||
stack.push(keycardIntroPage)
|
||||
}
|
||||
|
||||
// create password page
|
||||
function onSetPasswordRequested(password: string) {
|
||||
dbg.debugFlow("SET PASSWORD REQUESTED")
|
||||
d.password = password
|
||||
d.pushOrSkipBiometricsPage()
|
||||
}
|
||||
|
||||
// seedphrase page
|
||||
function onSeedphraseSubmitted(seedphrase: string) {
|
||||
dbg.debugFlow(`SEEDPHRASE SUBMITTED: ${seedphrase}`)
|
||||
d.seedphrase = seedphrase
|
||||
if (d.secondaryFlow === Onboarding.SecondaryFlow.CreateProfileWithSeedphrase || d.secondaryFlow === Onboarding.SecondaryFlow.LoginWithSeedphrase) {
|
||||
dbg.debugFlow("AFTER SEEDPHRASE -> PASSWORD PAGE")
|
||||
stack.push(createPasswordPage)
|
||||
} else if (d.secondaryFlow === Onboarding.SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase) {
|
||||
dbg.debugFlow("AFTER SEEDPHRASE -> KEYCARD PIN PAGE")
|
||||
stack.push(keycardCreatePinPage)
|
||||
}
|
||||
}
|
||||
|
||||
// keycard pages
|
||||
function onReloadKeycardRequested() {
|
||||
dbg.debugFlow("RELOAD KEYCARD REQUESTED")
|
||||
root.keycardReloaded()
|
||||
stack.replace(keycardIntroPage)
|
||||
}
|
||||
function onKeycardFactoryResetRequested() {
|
||||
dbg.debugFlow("KEYCARD FACTORY RESET REQUESTED")
|
||||
// TODO start keycard factory reset in a popup here
|
||||
// cf. KeycardStore.runFactoryResetPopup()
|
||||
root.keycardFactoryResetRequested()
|
||||
}
|
||||
function onLoginWithThisKeycardRequested() {
|
||||
dbg.debugFlow("LOGIN WITH THIS KEYCARD REQUESTED")
|
||||
d.primaryFlow = Onboarding.PrimaryFlow.Login
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.LoginWithKeycard
|
||||
stack.push(keycardEnterPinPage)
|
||||
}
|
||||
function onEmptyKeycardDetected() {
|
||||
dbg.debugFlow("EMPTY KEYCARD DETECTED")
|
||||
if (d.secondaryFlow === Onboarding.SecondaryFlow.LoginWithKeycard)
|
||||
stack.replace(keycardEmptyPage) // NB: replacing the loginPage
|
||||
else
|
||||
stack.replace(createKeycardProfilePage) // NB: replacing the keycardIntroPage
|
||||
}
|
||||
function onNotEmptyKeycardDetected() {
|
||||
dbg.debugFlow("NOT EMPTY KEYCARD DETECTED")
|
||||
if (d.secondaryFlow === Onboarding.SecondaryFlow.LoginWithKeycard)
|
||||
stack.replace(keycardEnterPinPage)
|
||||
else
|
||||
stack.replace(keycardNotEmptyPage)
|
||||
}
|
||||
|
||||
function onCreateKeycardProfileWithNewSeedphrase() {
|
||||
dbg.debugFlow("CREATE KEYCARD PROFILE WITH NEW SEEDPHRASE")
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.CreateProfileWithKeycardNewSeedphrase
|
||||
stack.push(backupSeedIntroPage)
|
||||
}
|
||||
function onCreateKeycardProfileWithExistingSeedphrase() {
|
||||
dbg.debugFlow("CREATE KEYCARD PROFILE WITH EXISTING SEEDPHRASE")
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase
|
||||
stack.push(seedphrasePage, { title: qsTr("Create profile on empty Keycard using a recovery phrase")})
|
||||
}
|
||||
|
||||
function onKeycardPinCreated(pin: string) {
|
||||
dbg.debugFlow(`KEYCARD PIN CREATED: ${pin}`)
|
||||
d.keycardPin = pin
|
||||
root.onboardingStore.setPin(pin)
|
||||
|
||||
if (d.secondaryFlow === Onboarding.SecondaryFlow.CreateProfileWithKeycardNewSeedphrase ||
|
||||
d.secondaryFlow === Onboarding.SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase) {
|
||||
dbg.debugFlow("ENTERING KEYPAIR TRANSFER PAGE")
|
||||
stack.clear()
|
||||
root.onboardingStore.startKeypairTransfer()
|
||||
stack.push(addKeypairPage)
|
||||
} else {
|
||||
Backpressure.debounce(root, 2000, function() {
|
||||
d.pushOrSkipBiometricsPage()
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
function onKeycardPinEntered(pin: string) {
|
||||
dbg.debugFlow(`KEYCARD PIN ENTERED: ${pin}`)
|
||||
d.keycardPin = pin
|
||||
root.onboardingStore.setPin(pin)
|
||||
|
||||
if (d.secondaryFlow === Onboarding.SecondaryFlow.CreateProfileWithKeycardNewSeedphrase ||
|
||||
d.secondaryFlow === Onboarding.SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase) {
|
||||
dbg.debugFlow("ENTERING KEYPAIR TRANSFER PAGE")
|
||||
stack.clear()
|
||||
root.onboardingStore.startKeypairTransfer()
|
||||
stack.push(addKeypairPage)
|
||||
} else {
|
||||
d.pushOrSkipBiometricsPage()
|
||||
}
|
||||
}
|
||||
|
||||
// backup seedphrase pages
|
||||
function onBackupSeedphraseRequested() {
|
||||
dbg.debugFlow("BACKUP SEED REQUESTED")
|
||||
stack.push(backupSeedAcksPage)
|
||||
}
|
||||
|
||||
function onBackupSeedphraseContinue() {
|
||||
dbg.debugFlow("BACKUP SEED CONTINUE")
|
||||
stack.push(backupSeedRevealPage)
|
||||
}
|
||||
|
||||
function onBackupSeedphraseConfirmed() {
|
||||
dbg.debugFlow("BACKUP SEED CONFIRMED")
|
||||
root.onboardingStore.mnemonicWasShown()
|
||||
stack.push(backupSeedVerifyPage)
|
||||
}
|
||||
|
||||
function onBackupSeedphraseVerified() {
|
||||
dbg.debugFlow("BACKUP SEED VERIFIED")
|
||||
stack.push(backupSeedOutroPage)
|
||||
}
|
||||
|
||||
function onBackupSeedphraseRemovalConfirmed() {
|
||||
dbg.debugFlow("BACKUP SEED REMOVAL CONFIRMED")
|
||||
root.onboardingStore.removeMnemonic()
|
||||
stack.replace(keycardCreatePinPage)
|
||||
}
|
||||
|
||||
// login with sync pages
|
||||
function onSyncProceedWithConnectionString(connectionString) {
|
||||
dbg.debugFlow(`SYNC PROCEED WITH CONNECTION STRING: ${connectionString}`)
|
||||
root.onboardingStore.inputConnectionStringForBootstrapping(connectionString)
|
||||
stack.replace(syncProgressPage)
|
||||
}
|
||||
|
||||
function onRestartSyncRequested() {
|
||||
dbg.debugFlow("RESTART SYNC REQUESTED")
|
||||
stack.replace(loginBySyncPage)
|
||||
}
|
||||
|
||||
function onLoginToAppRequested() {
|
||||
dbg.debugFlow("LOGIN TO APP REQUESTED")
|
||||
d.pushOrSkipBiometricsPage()
|
||||
}
|
||||
|
||||
// keypair transfer page
|
||||
function onKeypairAddContinueRequested() {
|
||||
dbg.debugFlow("KEYPAIR TRANSFER COMPLETED")
|
||||
d.pushOrSkipBiometricsPage()
|
||||
}
|
||||
function onKeypairAddTryAgainRequested() {
|
||||
dbg.debugFlow("RESTART KEYPAIR TRANSFER REQUESTED")
|
||||
root.onboardingStore.startKeypairTransfer()
|
||||
stack.clear()
|
||||
stack.push(addKeypairPage)
|
||||
}
|
||||
function onCreateProfilePageRequested() {
|
||||
dbg.debugFlow("KEYPAIR TRANSFER -> CREATE PROFILE")
|
||||
stack.replace([welcomePage, createProfilePage])
|
||||
}
|
||||
|
||||
// enable biometrics page
|
||||
function onEnableBiometricsRequested(enabled: bool) {
|
||||
dbg.debugFlow(`ENABLE BIOMETRICS: ${enabled}`)
|
||||
d.enableBiometrics = enabled
|
||||
d.finishFlow()
|
||||
}
|
||||
}
|
||||
|
||||
// pages
|
||||
Component {
|
||||
id: welcomePage
|
||||
WelcomePage {
|
||||
StackView.onActivated: d.resetState()
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: helpUsImproveStatusPage
|
||||
HelpUsImproveStatusPage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: createProfilePage
|
||||
CreateProfilePage {
|
||||
StackView.onActivated: {
|
||||
// reset when we get back here
|
||||
d.primaryFlow = Onboarding.PrimaryFlow.CreateProfile
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: createPasswordPage
|
||||
CreatePasswordPage {
|
||||
passwordStrengthScoreFunction: root.onboardingStore.getPasswordStrengthScore
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: enableBiometricsPage
|
||||
EnableBiometricsPage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: seedphrasePage
|
||||
SeedphrasePage {
|
||||
isSeedPhraseValid: root.onboardingStore.validMnemonic
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: createKeycardProfilePage
|
||||
CreateKeycardProfilePage {
|
||||
StackView.onActivated: {
|
||||
d.primaryFlow = Onboarding.PrimaryFlow.CreateProfile
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.CreateProfileWithKeycard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: keycardIntroPage
|
||||
KeycardIntroPage {
|
||||
keycardState: d.currentKeycardState
|
||||
displayPromoBanner: !d.settings.keycardPromoShown
|
||||
StackView.onActivated: {
|
||||
// NB just to make sure we don't miss the signal when we (re)load the page in the final state already
|
||||
if (keycardState === Onboarding.KeycardState.Empty)
|
||||
emptyKeycardDetected()
|
||||
else if (keycardState === Onboarding.KeycardState.NotEmpty)
|
||||
notEmptyKeycardDetected()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: keycardEmptyPage
|
||||
KeycardEmptyPage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: keycardNotEmptyPage
|
||||
KeycardNotEmptyPage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: keycardCreatePinPage
|
||||
KeycardCreatePinPage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: keycardEnterPinPage
|
||||
KeycardEnterPinPage {
|
||||
tryToSetPinFunction: root.onboardingStore.setPin
|
||||
remainingAttempts: root.onboardingStore.keycardRemainingPinAttempts
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: backupSeedIntroPage
|
||||
BackupSeedphraseIntro {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: backupSeedAcksPage
|
||||
BackupSeedphraseAcks {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: backupSeedRevealPage
|
||||
BackupSeedphraseReveal {
|
||||
seedWords: d.seedWords
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: backupSeedVerifyPage
|
||||
BackupSeedphraseVerify {
|
||||
seedWordsToVerify: {
|
||||
let result = []
|
||||
const randomIndexes = SQUtils.Utils.nSamples(d.numWordsToVerify, d.seedWords.length)
|
||||
return randomIndexes.map(i => ({ seedWordNumber: i+1, seedWord: d.seedWords[i] }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: backupSeedOutroPage
|
||||
BackupSeedphraseOutro {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: loginPage
|
||||
LoginPage {
|
||||
networkChecksEnabled: root.networkChecksEnabled
|
||||
StackView.onActivated: {
|
||||
// reset when we get back here
|
||||
d.primaryFlow = Onboarding.PrimaryFlow.Login
|
||||
d.secondaryFlow = Onboarding.SecondaryFlow.Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: loginBySyncPage
|
||||
LoginBySyncingPage {
|
||||
validateConnectionString: root.onboardingStore.validateLocalPairingConnectionString
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: syncProgressPage
|
||||
SyncProgressPage {
|
||||
syncState: root.onboardingStore.syncState
|
||||
timeoutInterval: root.splashScreenDurationMs
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: addKeypairPage
|
||||
KeycardAddKeyPairPage {
|
||||
addKeyPairState: root.onboardingStore.addKeyPairState
|
||||
timeoutInterval: root.splashScreenDurationMs
|
||||
}
|
||||
}
|
||||
|
||||
// common popups
|
||||
Component {
|
||||
id: privacyPolicyPopup
|
||||
StatusSimpleTextPopup {
|
||||
title: qsTr("Status Software Privacy Policy")
|
||||
content {
|
||||
textFormat: Text.MarkdownText
|
||||
text: SQUtils.StringUtils.readTextFile(Qt.resolvedUrl("../../../imports/assets/docs/privacy.mdwn"))
|
||||
}
|
||||
destroyOnClose: true
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: termsOfUsePopup
|
||||
StatusSimpleTextPopup {
|
||||
title: qsTr("Status Software Terms of Use")
|
||||
content {
|
||||
textFormat: Text.MarkdownText
|
||||
text: SQUtils.StringUtils.readTextFile(Qt.resolvedUrl("../../../imports/assets/docs/terms-of-use.mdwn"))
|
||||
}
|
||||
destroyOnClose: true
|
||||
}
|
||||
}
|
||||
}
|
109
ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml
Normal file
@ -0,0 +1,109 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
Control {
|
||||
id: root
|
||||
|
||||
// [{primary:string, secondary:string, image:string}]
|
||||
required property var newsModel
|
||||
|
||||
background: Rectangle {
|
||||
color: StatusColors.colors["neutral-95"]
|
||||
radius: 20
|
||||
}
|
||||
|
||||
verticalPadding: Theme.xlPadding
|
||||
horizontalPadding: Theme.xlPadding * 2
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: newsPage
|
||||
readonly property string primaryText: root.newsModel.get(pageIndicator.currentIndex).primary
|
||||
readonly property string secondaryText: root.newsModel.get(pageIndicator.currentIndex).secondary
|
||||
|
||||
spacing: Theme.halfPadding
|
||||
|
||||
Image {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 460
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumHeight: 582
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
source: Theme.png(root.newsModel.get(pageIndicator.currentIndex).image)
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
Layout.fillWidth: true
|
||||
text: newsPage.primaryText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.palette.white
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
Layout.fillWidth: true
|
||||
text: newsPage.secondaryText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Theme.additionalTextSize
|
||||
color: Theme.palette.white
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
PageIndicator {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
|
||||
Layout.topMargin: Theme.smallPadding
|
||||
Layout.maximumWidth: parent.width
|
||||
id: pageIndicator
|
||||
interactive: true
|
||||
count: root.newsModel.count
|
||||
currentIndex: -1
|
||||
Component.onCompleted: currentIndex = 0 // start switching pages
|
||||
|
||||
function switchToNextOrFirstPage() {
|
||||
currentIndex = (currentIndex + 1) % count
|
||||
}
|
||||
|
||||
delegate: Control {
|
||||
id: pageIndicatorDelegate
|
||||
implicitWidth: 44
|
||||
implicitHeight: 8
|
||||
|
||||
readonly property bool isCurrentPage: index === pageIndicator.currentIndex
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.rgba(1, 1, 1, 0.1)
|
||||
radius: 4
|
||||
HoverHandler {
|
||||
cursorShape: hovered ? Qt.PointingHandCursor : undefined
|
||||
}
|
||||
}
|
||||
contentItem: Item {
|
||||
Rectangle {
|
||||
NumberAnimation on width {
|
||||
from: 0
|
||||
to: pageIndicatorDelegate.availableWidth
|
||||
duration: 3000
|
||||
running: pageIndicatorDelegate.isCurrentPage
|
||||
onStopped: {
|
||||
if (pageIndicatorDelegate.isCurrentPage)
|
||||
pageIndicator.switchToNextOrFirstPage()
|
||||
}
|
||||
}
|
||||
|
||||
height: parent.height
|
||||
color: pageIndicatorDelegate.isCurrentPage ? Theme.palette.white : "transparent"
|
||||
radius: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
StatusTextField {
|
||||
id: root
|
||||
|
||||
required property bool valid
|
||||
required property var seedSuggestions // [{seedWord:string}, ...]
|
||||
|
||||
placeholderText: qsTr("Enter word")
|
||||
|
||||
leftPadding: Theme.padding
|
||||
rightPadding: Theme.padding + rightIcon.width + spacing
|
||||
topPadding: Theme.smallPadding
|
||||
bottomPadding: Theme.smallPadding
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radius
|
||||
color: d.isEmpty ? Theme.palette.baseColor2 : root.valid ? Theme.palette.successColor2 : Theme.palette.dangerColor3
|
||||
border.width: 1
|
||||
border.color: {
|
||||
if (d.isEmpty)
|
||||
return Theme.palette.primaryColor1
|
||||
if (root.valid)
|
||||
return Theme.palette.successColor3
|
||||
return Theme.palette.dangerColor2
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
readonly property int delegateHeight: 33
|
||||
readonly property bool isEmpty: root.text === ""
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Tab:
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter: {
|
||||
if (root.text === "") {
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
if (filteredModel.count > 0) {
|
||||
event.accepted = true
|
||||
root.text = filteredModel.get(suggestionsList.currentIndex).seedWord
|
||||
root.accepted()
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
case Qt.Key_Space: {
|
||||
event.accepted = !event.text.match(/^[a-zA-Z]$/)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Keys.forwardTo: [suggestionsList]
|
||||
|
||||
StatusDropdown {
|
||||
x: 0
|
||||
y: parent.height + 4
|
||||
width: parent.width
|
||||
contentHeight: ((suggestionsList.count <= 5) ? suggestionsList.count : 5) * d.delegateHeight // max 5 delegates
|
||||
visible: filteredModel.count > 0 && root.cursorVisible && !d.isEmpty && !root.valid
|
||||
verticalPadding: Theme.halfPadding
|
||||
horizontalPadding: 0
|
||||
contentItem: StatusListView {
|
||||
id: suggestionsList
|
||||
currentIndex: 0
|
||||
model: SortFilterProxyModel {
|
||||
id: filteredModel
|
||||
sourceModel: root.seedSuggestions
|
||||
filters: RegExpFilter {
|
||||
pattern: `^${root.text}`
|
||||
caseSensitivity: Qt.CaseInsensitive
|
||||
}
|
||||
sorters: StringSorter {
|
||||
roleName: "seedWord"
|
||||
}
|
||||
}
|
||||
delegate: StatusItemDelegate {
|
||||
width: ListView.view.width
|
||||
height: d.delegateHeight
|
||||
text: model.seedWord
|
||||
font.pixelSize: Theme.additionalTextSize
|
||||
highlightColor: Theme.palette.primaryColor1
|
||||
highlighted: hovered || index === suggestionsList.currentIndex
|
||||
onClicked: {
|
||||
root.text = text
|
||||
root.accepted()
|
||||
}
|
||||
}
|
||||
onCountChanged: currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
StatusIcon {
|
||||
id: rightIcon
|
||||
width: 20
|
||||
height: 20
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.padding
|
||||
visible: !d.isEmpty
|
||||
icon: root.valid ? "checkmark-circle" : root.activeFocus ? "clear" : "warning"
|
||||
color: root.valid ? Theme.palette.successColor1 :
|
||||
root.activeFocus ? Theme.palette.directColor9 : Theme.palette.dangerColor1
|
||||
|
||||
HoverHandler {
|
||||
id: hhandler
|
||||
cursorShape: hovered ? Qt.PointingHandCursor : undefined
|
||||
}
|
||||
TapHandler {
|
||||
enabled: rightIcon.icon === "clear"
|
||||
onSingleTapped: root.clear()
|
||||
}
|
||||
StatusToolTip {
|
||||
text: root.valid ? qsTr("Correct word") : root.activeFocus ? qsTr("Clear") : qsTr("Wrong word")
|
||||
visible: hhandler.hovered && rightIcon.visible
|
||||
}
|
||||
}
|
||||
}
|
42
ui/app/AppLayouts/Onboarding2/components/StepIndicator.qml
Normal file
@ -0,0 +1,42 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property int currentStep
|
||||
required property int totalSteps
|
||||
required property string caption
|
||||
|
||||
StatusBaseText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Step %1 of %2").arg(root.currentStep).arg(root.totalSteps)
|
||||
font.pixelSize: Theme.additionalTextSize
|
||||
color: Theme.palette.baseColor1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: root.totalSteps
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 4
|
||||
radius: 2
|
||||
color: index <= root.currentStep - 1 ? Theme.palette.primaryColor1 : Theme.palette.baseColor2
|
||||
}
|
||||
}
|
||||
}
|
||||
StatusBaseText {
|
||||
Layout.fillWidth: true
|
||||
text: root.caption
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
3
ui/app/AppLayouts/Onboarding2/components/qmldir
Normal file
@ -0,0 +1,3 @@
|
||||
NewsCarousel 1.0 NewsCarousel.qml
|
||||
SeedphraseVerifyInput 1.0 SeedphraseVerifyInput.qml
|
||||
StepIndicator 1.0 StepIndicator.qml
|
23
ui/app/AppLayouts/Onboarding2/controls/BulletPoint.qml
Normal file
@ -0,0 +1,23 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
RowLayout {
|
||||
property string text
|
||||
property bool checked
|
||||
|
||||
spacing: 6
|
||||
StatusIcon {
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
icon: parent.checked ? "check-circle" : "close-circle"
|
||||
color: parent.checked ? Theme.palette.successColor1 : Theme.palette.dangerColor1
|
||||
}
|
||||
StatusBaseText {
|
||||
Layout.fillWidth: true
|
||||
text: parent.text
|
||||
font.pixelSize: Theme.additionalTextSize
|
||||
}
|
||||
}
|
65
ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml
Normal file
@ -0,0 +1,65 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
AbstractButton {
|
||||
id: root
|
||||
|
||||
property string subTitle
|
||||
|
||||
padding: Theme.padding
|
||||
spacing: Theme.padding
|
||||
|
||||
icon.width: 32
|
||||
icon.height: 32
|
||||
|
||||
background: Rectangle {
|
||||
color: root.hovered ? Theme.palette.backgroundHover : "transparent"
|
||||
HoverHandler {
|
||||
cursorShape: root.hovered ? Qt.PointingHandCursor : undefined
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: root.spacing
|
||||
|
||||
StatusImage {
|
||||
Layout.preferredWidth: root.icon.width
|
||||
Layout.preferredHeight: root.icon.height
|
||||
source: root.icon.source
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 1
|
||||
StatusBaseText {
|
||||
Layout.fillWidth: true
|
||||
text: root.text
|
||||
font.pixelSize: Theme.additionalTextSize
|
||||
font.weight: Font.Medium
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18
|
||||
}
|
||||
StatusBaseText {
|
||||
Layout.fillWidth: true
|
||||
text: root.subTitle
|
||||
font.pixelSize: Theme.additionalTextSize
|
||||
color: Theme.palette.baseColor1
|
||||
visible: !!text
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18
|
||||
}
|
||||
}
|
||||
|
||||
StatusIcon {
|
||||
Layout.preferredWidth: 16
|
||||
Layout.preferredHeight: 16
|
||||
icon: "tiny/chevron-right"
|
||||
color: Theme.palette.baseColor1
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import QtQuick 2.15
|
||||
import QtQml 2.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
StatusButton {
|
||||
id: root
|
||||
|
||||
implicitWidth: 320
|
||||
|
||||
// inside a Column (or another Positioner), make all but the first button outline
|
||||
Binding on normalColor {
|
||||
value: "transparent"
|
||||
when: !root.Positioner.isFirstItem
|
||||
restoreMode: Binding.RestoreBindingOrValue
|
||||
}
|
||||
Binding on borderWidth {
|
||||
value: 1
|
||||
when: !root.Positioner.isFirstItem
|
||||
restoreMode: Binding.RestoreBindingOrValue
|
||||
}
|
||||
Binding on borderColor {
|
||||
value: Theme.palette.baseColor2
|
||||
when: !root.Positioner.isFirstItem
|
||||
restoreMode: Binding.RestoreBindingOrValue
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtGraphicalEffects 1.15
|
||||
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
Frame {
|
||||
id: root
|
||||
|
||||
padding: 0
|
||||
|
||||
background: Rectangle {
|
||||
id: background
|
||||
border.width: 1
|
||||
border.color: Theme.palette.baseColor2
|
||||
radius: 12
|
||||
color: Theme.palette.background
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: root.width
|
||||
height: root.height
|
||||
radius: background.radius
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
32
ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml
Normal file
@ -0,0 +1,32 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtGraphicalEffects 1.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
Frame {
|
||||
id: root
|
||||
|
||||
property bool dropShadow: true
|
||||
property alias cornerRadius: background.radius
|
||||
|
||||
padding: Theme.bigPadding
|
||||
|
||||
background: Rectangle {
|
||||
id: background
|
||||
border.width: 1
|
||||
border.color: Theme.palette.baseColor2
|
||||
radius: 20
|
||||
color: Theme.palette.background
|
||||
}
|
||||
|
||||
layer.enabled: root.dropShadow
|
||||
layer.effect: DropShadow {
|
||||
verticalOffset: 4
|
||||
radius: 7
|
||||
samples: 15
|
||||
cached: true
|
||||
color: Theme.palette.dropShadow
|
||||
}
|
||||
}
|