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
This commit is contained in:
Lukáš Tinkl 2024-11-06 00:39:08 +01:00 committed by Lukáš Tinkl
parent 97a40fb18f
commit 3705249e40
175 changed files with 6160 additions and 504 deletions

View File

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

View 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

View File

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

View File

@ -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/KubaDesktop?node-id=25878%3A518438&t=C7xTpNib38t7s7XU-4

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

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

View 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)
}
}
}

View File

@ -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%-]+)/

View File

@ -0,0 +1,3 @@
import QtQml 2.15
QtObject {}

View File

@ -0,0 +1 @@
OnboardingStore 1.0 OnboardingStore.qml

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import QtQml 2.15
QtObject {}

View File

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

View File

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

View File

@ -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'])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import QtQuick 2.13
import QtQuick.Window 2.15
import QtQuick 2.15
/*!
\qmltype StatusImage

View File

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

View 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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import QtQuick 2.14
import QtQuick 2.15
import StatusQ.Controls 0.1

View File

@ -86,5 +86,8 @@ QtObject {
'lightDesktopBlue10': '#ECEFFB',
'darkDesktopBlue10': '#273251',
// new/mobile colors
'neutral-95': '#0D1625'
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 778 KiB

After

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

View File

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

View 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)
};

View File

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

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

View File

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

View File

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

View File

@ -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 doesnt match any user")
text: qsTr("Recovery phrase doesnt 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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import QtQuick 2.13
import QtQuick 2.15
import shared.popups 1.0
import shared.views 1.0

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

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

View File

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

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

View File

@ -0,0 +1,3 @@
NewsCarousel 1.0 NewsCarousel.qml
SeedphraseVerifyInput 1.0 SeedphraseVerifyInput.qml
StepIndicator 1.0 StepIndicator.qml

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

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

View File

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

View File

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

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

Some files were not shown because too many files have changed in this diff Show More