status-desktop/storybook/pages/OnboardingLayoutPage.qml
Lukáš Tinkl 638676ed0b feat(Onboarding) Implement new Login screen
- implement the new UI and frontend logic of the Login screen
- integrate it (as a separate page) into the OnboardingLayout
- add SB pages
- add an integration QML test
- add some TODOs and FIXMEs for the existing and new external flows,
which will be covered separately in followup PRs

Fixes #17057
2025-01-23 17:41:43 +01:00

526 lines
18 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
import StatusQ 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import AppLayouts.Onboarding.enums 1.0
import AppLayouts.Onboarding2 1.0
import AppLayouts.Onboarding2.pages 1.0
import AppLayouts.Onboarding2.stores 1.0
import shared.panels 1.0
import utils 1.0
import Storybook 1.0
import Models 1.0
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"]
readonly property string pin: "111111"
readonly property string password: "somepassword"
// TODO simulation
function restart() {
// add keypair state
// sync state
}
}
LoginAccountsModel {
id: loginAccountsModel
}
ListModel {
id: emptyModel
}
OnboardingLayout {
id: onboarding
SplitView.fillWidth: true
SplitView.fillHeight: true
onboardingStore: OnboardingStore {
id: store
property int keycardState: Onboarding.KeycardState.NoPCSCService
property int addKeyPairState: Onboarding.AddKeyPairState.InProgress
property int syncState: Onboarding.SyncState.InProgress
property int keycardRemainingPinAttempts: ctrlUnlockWithPuk.checked ? 1 : 5
function setPin(pin: string) { // -> bool
logs.logEvent("OnboardingStore.setPin", ["pin"], arguments)
ctrlLoginResult.result = "🯄"
const valid = pin === mockDriver.pin
if (!valid)
keycardRemainingPinAttempts--
if (keycardRemainingPinAttempts <= 0) { // SIMULATION: "lock" the keycard
keycardState = Onboarding.KeycardState.Locked
keycardRemainingPinAttempts = ctrlUnlockWithPuk.checked ? 1 : 5
}
return valid
}
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()")
}
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)
}
// password signals
signal accountLoginError(string error, bool wrongPassword)
// biometrics signals
signal obtainingPasswordSuccess(string password)
signal obtainingPasswordError(string errorDescription, string errorType /* Constants.keychain.errorType.* */, bool wrongFingerprint)
}
loginAccountsModel: ctrlLoginScreen.checked ? loginAccountsModel : emptyModel
biometricsAvailable: ctrlBiometrics.checked
isBiometricsLogin: localAccountSettings.storeToKeychainValue === Constants.keychain.storedValue.store
onBiometricsRequested: biometricsPopup.open()
onFinished: (flow, data) => {
console.warn("!!! ONBOARDING FINISHED; flow:", flow, "; data:", JSON.stringify(data))
logs.logEvent("onFinished", ["flow", "data"], arguments)
console.warn("!!! SIMULATION: SHOWING SPLASH")
stack.clear()
stack.push(splashScreen, { runningProgressAnimation: true })
store.keycardState = Onboarding.KeycardState.NoPCSCService
}
onLoginRequested: (keyUid, method, data) => {
logs.logEvent("onLoginRequested", ["keyUid", "method", "data"], arguments)
// SIMULATION: emit an error in case of wrong password
if (method === Onboarding.LoginMethod.Password && data.password !== mockDriver.password) {
onboardingStore.accountLoginError("The impossible has happened", Math.random() < 0.5)
ctrlLoginResult.result = "<font color='red'>⛔</font>"
} else {
ctrlLoginResult.result = "<font color='green'>✔</font>"
}
}
onReloadKeycardRequested: store.keycardState = Onboarding.KeycardState.NoPCSCService
// mocks
QtObject {
id: localAccountSettings
readonly property string storeToKeychainValue: ctrlTouchIdUser.checked ? Constants.keychain.storedValue.store : ""
}
Button {
text: "Paste password"
focusPolicy: Qt.NoFocus
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 10
visible: onboarding.stack.currentItem instanceof CreatePasswordPage ||
(onboarding.stack.currentItem instanceof LoginScreen && !onboarding.stack.currentItem.selectedProfileIsKeycard)
onClicked: {
const currentItem = onboarding.stack.currentItem
const loginPassInput = StorybookUtils.findChild(
currentItem,
"loginPasswordInput")
if (!!loginPassInput) {
ClipboardUtils.setText(mockDriver.password)
loginPassInput.paste()
}
const input1 = StorybookUtils.findChild(
currentItem,
"passwordViewNewPassword")
const input2 = StorybookUtils.findChild(
currentItem,
"passwordViewNewPasswordConfirm")
if (!input1 || !input2)
return
input1.text = mockDriver.password
input2.text = mockDriver.password
}
}
Button {
text: "Paste seed phrase"
focusPolicy: Qt.NoFocus
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 10
visible: onboarding.stack.currentItem instanceof SeedphrasePage
onClicked: {
for (let i = 1;; i++) {
const input = StorybookUtils.findChild(
onboarding.stack.currentItem,
`enterSeedPhraseInputField${i}`)
if (input === null)
break
input.text = "dog"
}
}
}
Button {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 10
visible: onboarding.stack.currentItem instanceof KeycardEnterPinPage ||
onboarding.stack.currentItem instanceof KeycardCreatePinPage ||
(onboarding.stack.currentItem instanceof LoginScreen && onboarding.stack.currentItem.selectedProfileIsKeycard)
text: "Copy valid PIN (\"%1\")".arg(mockDriver.pin)
focusPolicy: Qt.NoFocus
onClicked: ClipboardUtils.setText(mockDriver.pin)
}
Button {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 10
visible: onboarding.stack.currentItem instanceof BackupSeedphraseVerify
text: "Paste seed phrase verification"
focusPolicy: Qt.NoFocus
onClicked: {
for (let i = 0;; i++) {
const input = StorybookUtils.findChild(
onboarding.stack.currentItem,
`seedInput_${i}`)
if (input === null)
break
const index = input.seedWordIndex
input.text = mockDriver.seedWords[index]
}
}
}
Button {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 10
visible: onboarding.stack.currentItem instanceof BackupSeedphraseAcks
text: "Paste seed phrase verification"
focusPolicy: Qt.NoFocus
onClicked: {
for (let i = 1;; i++) {
const checkBox = StorybookUtils.findChild(
onboarding.stack.currentItem,
`ack${i}`)
if (checkBox === null)
break
checkBox.checked = true
}
}
}
}
BiometricsPopup {
id: biometricsPopup
visible: onboarding.stack.currentItem instanceof LoginScreen && ctrlBiometrics.checked && ctrlTouchIdUser.checked
x: root.Window.width - width
password: mockDriver.password
pin: mockDriver.pin
onAccountLoginError: (error, wrongPassword) => store.accountLoginError(error, wrongPassword)
onObtainingPasswordSuccess: (password) => store.obtainingPasswordSuccess(password)
onObtainingPasswordError: (errorDescription, errorType, wrongFingerprint) => store.obtainingPasswordError(errorDescription, errorType, wrongFingerprint)
}
Component {
id: splashScreen
DidYouKnowSplashScreen {
property bool runningProgressAnimation
NumberAnimation on progress {
from: 0.0
to: 1
duration: 3000
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: 250
SplitView.preferredHeight: 250
logsView.logText: logs.logText
ColumnLayout {
anchors.fill: parent
spacing: 10
TextField {
Layout.fillWidth: true
text: {
const stack = onboarding.stack
let content = `Stack (${stack.depth}):`
for (let i = 0; i < stack.depth; i++)
content += " " + InspectionUtils.baseName(
stack.get(i, StackView.ForceLoad))
return content
}
background: null
readOnly: true
selectByMouse: true
wrapMode: Text.Wrap
}
RowLayout {
Layout.fillWidth: true
Button {
text: "Restart"
focusPolicy: Qt.NoFocus
onClicked: onboarding.restartFlow()
}
Switch {
id: ctrlBiometrics
text: "Biometrics available"
checked: true
}
ToolSeparator {}
Switch {
id: ctrlLoginScreen
text: "Show login screen"
checkable: true
onToggled: onboarding.restartFlow()
}
Switch {
id: ctrlTouchIdUser
text: "Touch ID login"
enabled: ctrlBiometrics.checked
checked: ctrlBiometrics.checked
}
Switch {
id: ctrlUnlockWithPuk
text: "Unlock with PUK available"
checked: true
}
Text {
id: ctrlLoginResult
property string result: "🯄"
text: "Login result: %1".arg(result)
}
}
RowLayout {
Label {
text: "Keycard state:"
}
Flow {
Layout.fillWidth: true
spacing: 2
ButtonGroup {
id: keycardStateButtonGroup
}
Repeater {
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" }
]
RoundButton {
text: modelData.text
checkable: true
checked: store.keycardState === modelData.value
ButtonGroup.group: keycardStateButtonGroup
onClicked: {
store.keycardState = modelData.value
ctrlLoginResult.result = "🯄"
}
}
}
}
}
RowLayout {
Label {
text: "Add key pair state:"
}
Flow {
spacing: 2
ButtonGroup {
id: addKeypairStateButtonGroup
}
Repeater {
model: [
{ value: Onboarding.AddKeyPairState.InProgress, text: "InProgress" },
{ value: Onboarding.AddKeyPairState.Success, text: "Success" },
{ value: Onboarding.AddKeyPairState.Failed, text: "Failed" }
]
RoundButton {
text: modelData.text
checkable: true
checked: store.addKeyPairState === modelData.value
ButtonGroup.group: addKeypairStateButtonGroup
onClicked: store.addKeyPairState = modelData.value
}
}
}
ToolSeparator {}
Label {
text: "Sync state:"
}
Flow {
Layout.fillWidth: true
spacing: 2
ButtonGroup {
id: syncStateButtonGroup
}
Repeater {
model: [
{ value: Onboarding.SyncState.InProgress, text: "InProgress" },
{ value: Onboarding.SyncState.Success, text: "Success" },
{ value: Onboarding.SyncState.Failed, text: "Failed" }
]
RoundButton {
text: modelData.text
checkable: true
checked: store.syncState === modelData.value
ButtonGroup.group: syncStateButtonGroup
onClicked: store.syncState = modelData.value
}
}
}
}
Item {
Layout.fillHeight: true
}
}
}
}
// category: Onboarding
// status: good
// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1-25&node-type=canvas&m=dev