Skip to content

Commit

Permalink
feat(Onboarding): implement the KeycardFactoryReset flow
Browse files Browse the repository at this point in the history
- integrate it into the UI and StoryBook
- a new keycardState is introduced: `FactoryResetting` (matching the
backend)
- a new store method introduced: `startKeycardFactoryReset()`

Fixes: #17094
  • Loading branch information
caybro committed Feb 4, 2025
1 parent 6e2e6ff commit 09bdb95
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 48 deletions.
3 changes: 1 addition & 2 deletions storybook/pages/LoginScreenPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ SplitView {
logs.logEvent("setPuk", ["puk"], arguments)
const valid = puk === ctrlPuk.text
if (!valid)
keycardRemainingPukAttempts--
keycardRemainingPukAttempts-- // SIMULATION: decrease the remaining PUK attempts
if (keycardRemainingPukAttempts <= 0) { // SIMULATION: "block" the keycard
keycardState = Onboarding.KeycardState.BlockedPUK
keycardRemainingPukAttempts = 0
Expand Down Expand Up @@ -86,7 +86,6 @@ SplitView {
onUnblockWithSeedphraseRequested: logs.logEvent("onUnblockWithSeedphraseRequested")
onUnblockWithPukRequested: logs.logEvent("onUnblockWithPukRequested")
onLostKeycard: logs.logEvent("onLostKeycard")
onKeycardFactoryResetRequested: logs.logEvent("onKeycardFactoryResetRequested")

// mocks
QtObject {
Expand Down
38 changes: 16 additions & 22 deletions storybook/pages/OnboardingLayoutPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15

import StatusQ.Core.Backpressure 0.1

import Qt.labs.settings 1.0

import StatusQ 0.1
Expand Down Expand Up @@ -65,7 +67,7 @@ SplitView {
property int syncState: Onboarding.ProgressState.Idle
property var loginAccountsModel: ctrlLoginScreen.checked ? loginAccountsModel : emptyModel

property int keycardRemainingPinAttempts: 2
property int keycardRemainingPinAttempts: 3
property int keycardRemainingPukAttempts: 3

function setPin(pin: string) { // -> bool
Expand All @@ -81,7 +83,7 @@ SplitView {
return valid
}

function setPuk(puk) { // -> bool
function setPuk(puk: string) { // -> bool
logs.logEvent("OnboardingStore.setPuk", ["puk"], arguments)
const valid = puk === mockDriver.puk
if (!valid)
Expand All @@ -105,6 +107,15 @@ SplitView {
logs.logEvent("OnboardingStore.exportRecoverKeys")
}

function startKeycardFactoryReset() {
logs.logEvent("OnboardingStore.startKeycardFactoryReset")
console.warn("!!! SIMULATION: KEYCARD FACTORY RESET")
keycardState = Onboarding.KeycardState.FactoryResetting // SIMULATION: factory reset
Backpressure.debounce(root, 2000, () => {
keycardState = Onboarding.KeycardState.Empty
})()
}

// password
function getPasswordStrengthScore(password: string) { // -> int
logs.logEvent("OnboardingStore.getPasswordStrengthScore", ["password"], arguments)
Expand All @@ -122,10 +133,6 @@ SplitView {
return JSON.stringify(mockDriver.seedWords)
}

function mnemonicWasShown() { // -> void
logs.logEvent("OnboardingStore.mnemonicWasShown()")
}

function validateLocalPairingConnectionString(connectionString: string) { // -> bool
logs.logEvent("OnboardingStore.validateLocalPairingConnectionString", ["connectionString"], arguments)
return !Number.isNaN(parseInt(connectionString))
Expand Down Expand Up @@ -349,24 +356,11 @@ SplitView {
}
}

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
SplitView.minimumHeight: 300
SplitView.preferredHeight: 300

logsView.logText: logs.logText

Expand Down Expand Up @@ -527,7 +521,7 @@ SplitView {
ToolSeparator {}

Label {
text: "Pin Setting state:"
text: "PIN Setting state:"
}

Flow {
Expand Down
108 changes: 108 additions & 0 deletions ui/app/AppLayouts/Onboarding2/KeycardFactoryResetFlow.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import QtQuick 2.15
import QtQuick.Controls 2.15

import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils

import AppLayouts.Onboarding2.pages 1.0
import AppLayouts.Onboarding.enums 1.0

SQUtils.QObject {
id: root

required property StackView stackView
required property int keycardState

signal performKeycardFactoryResetRequested

signal finished

function init(fromLoginScreen = false) {
d.fromLoginScreen = fromLoginScreen
root.stackView.push(d.initialComponent())
}

QtObject {
id: d

property bool fromLoginScreen

function initialComponent() {
if (root.keycardState === Onboarding.KeycardState.FactoryResetting)
return keycardResetPageComponent
return keycardResetAcks
}
}

Component {
id: keycardResetAcks

KeycardBasePage {
image.source: Theme.png("onboarding/keycard/reading")
title: qsTr("Factory reset Keycard")
subtitle: "<font color='%1'>".arg(Theme.palette.dangerColor1) +
qsTr("All data including the stored key pair and derived accounts will be removed from the Keycard") +
"</font>"
buttons: [
StatusCheckBox {
id: ack
width: Math.min(implicitWidth, parent.width)
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("I understand the key pair will be deleted")
},
Item {
width: parent.width
height: parent.spacing
},
StatusButton {
anchors.horizontalCenter: parent.horizontalCenter
type: StatusBaseButton.Type.Danger
text: qsTr("Factory reset this Keycard")
enabled: ack.checked
onClicked: {
root.performKeycardFactoryResetRequested()
root.stackView.replace(null, keycardResetPageComponent)
}
}
]
}
}

Component {
id: keycardResetPageComponent

KeycardBasePage {
id: keycardResetPage
readonly property bool resetting: root.keycardState === Onboarding.KeycardState.FactoryResetting

image.source: resetting ? Theme.png("onboarding/keycard/empty") // FIXME correct image
: Theme.png("onboarding/keycard/success")
title: resetting ? qsTr("Reseting Keycard") : qsTr("Keycard successfully factory reset")
subtitle: resetting ? "" : qsTr("You can now use this Keycard like it's a brand-new, empty Keycard")
infoText.text: resetting ? qsTr("Do not remove your Keycard or reader") : ""
buttons: [
Row {
spacing: 4
visible: keycardResetPage.resetting
anchors.horizontalCenter: parent.horizontalCenter
StatusLoadingIndicator {
color: Theme.palette.directColor1
}
StatusBaseText {
text: qsTr("Please wait while the Keycard is being reset")
}
},
StatusButton {
visible: !keycardResetPage.resetting
anchors.horizontalCenter: parent.horizontalCenter
width: 320
text: d.fromLoginScreen ? qsTr("Back to Login screen") : qsTr("Log in or Create profile")
onClicked: root.finished()
}
]
}
}
}
1 change: 1 addition & 0 deletions ui/app/AppLayouts/Onboarding2/LoginWithKeycardFlow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ SQUtils.QObject {
remainingAttempts: root.remainingPinAttempts
unblockWithPukAvailable: root.remainingPukAttempts > 0
keycardPinInfoPageDelay: root.keycardPinInfoPageDelay
pinCorrectText: qsTr("PIN correct. Exporting keys.")

onExportKeysRequested: root.exportKeysRequested()
onExportKeysDone: root.finished()
Expand Down
41 changes: 30 additions & 11 deletions ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ SQUtils.QObject {
signal seedphraseSubmitted(string seedphrase)
signal setPasswordRequested(string password)
signal reloadKeycardRequested
signal keycardFactoryResetRequested
signal exportKeysRequested
signal loadMnemonicRequested
signal authorizationRequested(string pin)

signal performKeycardFactoryResetRequested

signal linkActivated(string link)

signal finished(int flow)
Expand All @@ -71,6 +72,8 @@ SQUtils.QObject {
property int flow
property LoginScreen loginScreen: null

property bool seenUsageDataPrompt

function pushOrSkipBiometricsPage() {
if (root.biometricsAvailable) {
root.stackView.replace(null, enableBiometricsPage)
Expand Down Expand Up @@ -102,12 +105,17 @@ SQUtils.QObject {

WelcomePage {
function pushWithProxy(component) {
const page = root.stackView.push(helpUsImproveStatusPage)

page.shareUsageDataRequested.connect(enabled => {
root.shareUsageDataRequested(enabled)
if (d.seenUsageDataPrompt) { // don't ask for "Share usage data" a second time (e.g. after a factory reset)
root.stackView.push(component)
})
} else {
const page = root.stackView.push(helpUsImproveStatusPage)

page.shareUsageDataRequested.connect(enabled => {
root.shareUsageDataRequested(enabled)
root.stackView.push(component)
d.seenUsageDataPrompt = true
})
}
}

onCreateProfileRequested: pushWithProxy(createProfilePage)
Expand Down Expand Up @@ -141,7 +149,6 @@ SQUtils.QObject {
onLostKeycard: root.stackView.push(keycardLostPage)
onUnblockWithSeedphraseRequested: unblockWithSeedphraseFlow.init()
onUnblockWithPukRequested: unblockWithPukFlow.init()
onKeycardFactoryResetRequested: console.warn("!!! FIXME OnboardingLayout::onKeycardFactoryResetRequested")

Binding {
target: d
Expand Down Expand Up @@ -245,7 +252,7 @@ SQUtils.QObject {
keycardPinInfoPageDelay: root.keycardPinInfoPageDelay

onReloadKeycardRequested: root.reloadKeycardRequested()
onKeycardFactoryResetRequested: root.keycardFactoryResetRequested()
onKeycardFactoryResetRequested: keycardFactoryResetFlow.init()
onLoadMnemonicRequested: root.loadMnemonicRequested()
onKeycardPinCreated: (pin) => root.keycardPinCreated(pin)
onLoginWithKeycardRequested: loginWithKeycardFlow.init()
Expand Down Expand Up @@ -309,8 +316,9 @@ SQUtils.QObject {
onSeedphraseSubmitted: (seedphrase) => root.seedphraseSubmitted(seedphrase)
onReloadKeycardRequested: root.reloadKeycardRequested()
onCreateProfileWithEmptyKeycardRequested: keycardCreateProfileFlow.init()
onKeycardFactoryResetRequested: root.keycardFactoryResetRequested()
onExportKeysRequested: root.exportKeysRequested()
onKeycardFactoryResetRequested: keycardFactoryResetFlow.init()
onUnblockWithSeedphraseRequested: unblockWithSeedphraseFlow.init()
onUnblockWithPukRequested: unblockWithPukFlow.init()

onFinished: {
Expand Down Expand Up @@ -358,7 +366,7 @@ SQUtils.QObject {
unblockWithPukFlow.pin = pin
root.keycardPinCreated(pin)
}
onKeycardFactoryResetRequested: root.keycardFactoryResetRequested()
onKeycardFactoryResetRequested: keycardFactoryResetFlow.init()

onFinished: (success) => {
if (!success)
Expand Down Expand Up @@ -389,7 +397,7 @@ SQUtils.QObject {
keycardPinInfoPageDelay: root.keycardPinInfoPageDelay

onReloadKeycardRequested: root.reloadKeycardRequested()
onKeycardFactoryResetRequested: root.keycardFactoryResetRequested()
onKeycardFactoryResetRequested: keycardFactoryResetFlow.init(true)
onKeycardPinCreated: (pin) => root.keycardPinCreated(pin)
onLoginWithKeycardRequested: loginWithKeycardFlow.init()
onAuthorizationRequested: root.authorizationRequested("") // Pin was saved locally already
Expand All @@ -407,6 +415,17 @@ SQUtils.QObject {
onFinished: d.pushOrSkipBiometricsPage()
}

KeycardFactoryResetFlow {
id: keycardFactoryResetFlow
stackView: root.stackView
keycardState: root.keycardState
onPerformKeycardFactoryResetRequested: root.performKeycardFactoryResetRequested()
onFinished: {
stackView.clear()
root.init()
}
}

Component {
id: enableBiometricsPage

Expand Down
3 changes: 1 addition & 2 deletions ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Page {
onAuthorizationRequested: d.authorize(pin)
onShareUsageDataRequested: (enabled) => root.shareUsageDataRequested(enabled)
onReloadKeycardRequested: root.reloadKeycardRequested()

onPerformKeycardFactoryResetRequested: root.onboardingStore.startKeycardFactoryReset()
onSyncProceedWithConnectionString: (connectionString) =>
root.onboardingStore.inputConnectionStringForBootstrapping(connectionString)
onSeedphraseSubmitted: (seedphrase) => d.seedphrase = seedphrase
Expand All @@ -194,7 +194,6 @@ Page {
onLinkActivated: (link) => Qt.openUrlExternally(link)
onExportKeysRequested: root.onboardingStore.exportRecoverKeys()
onFinished: (flow) => d.finishFlow(flow)
onKeycardFactoryResetRequested: console.warn("!!! FIXME OnboardingLayout::onKeycardFactoryResetRequested")
}

Connections {
Expand Down
9 changes: 1 addition & 8 deletions ui/app/AppLayouts/Onboarding2/components/LoginKeycardBox.qml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ Control {

signal unblockWithSeedphraseRequested()
signal unblockWithPukRequested()
signal keycardFactoryResetRequested()

signal loginRequested(string pin)

Expand Down Expand Up @@ -92,16 +91,10 @@ Control {
}
MaybeOutlineButton {
width: parent.width
visible: root.keycardState === Onboarding.KeycardState.BlockedPIN
visible: root.keycardState === Onboarding.KeycardState.BlockedPIN || root.keycardState === Onboarding.KeycardState.BlockedPUK
text: qsTr("Unblock with recovery phrase")
onClicked: root.unblockWithSeedphraseRequested()
}
MaybeOutlineButton {
width: parent.width
visible: root.keycardState === Onboarding.KeycardState.BlockedPUK
text: qsTr("Factory reset Keycard")
onClicked: root.keycardFactoryResetRequested()
}
}
StatusPinInput {
Layout.alignment: Qt.AlignHCenter
Expand Down
Loading

0 comments on commit 09bdb95

Please sign in to comment.