From 159544b1b848464068a144745a2949ffb1dfbe5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tinkl?= Date: Tue, 21 Jan 2025 22:13:05 +0100 Subject: [PATCH] feat(Onboarding): implement the new `UnlockWithPukFlow` - integrate the PUK unlock flow into the Onboarding and added a dedicated SB page for it - TODO: integrate into Login screen - remove the `Locked` keycard state everywhere in favor of `BlockedPIN` and `BlockedPUK` - fix the various "Locked" buttons, based on the context and the state of the keycard Fixes: #17092 --- storybook/pages/KeycardEnterPukPagePage.qml | 6 +- storybook/pages/KeycardIntroPagePage.qml | 3 +- storybook/pages/LoginScreenPage.qml | 22 +- storybook/pages/OnboardingLayoutPage.qml | 53 +++-- storybook/pages/UnlockWithPukFlowPage.qml | 210 ++++++++++++++++++ ui/StatusQ/src/onboarding/enums.h | 3 +- .../Onboarding2/KeycardCreateProfileFlow.qml | 1 + .../Onboarding2/LoginWithKeycardFlow.qml | 11 +- .../AppLayouts/Onboarding2/OnboardingFlow.qml | 28 ++- .../Onboarding2/OnboardingLayout.qml | 16 +- .../Onboarding2/OnboardingStackView.qml | 3 + .../Onboarding2/UnlockWithPukFlow.qml | 108 +++++++++ .../components/LoginKeycardBox.qml | 26 ++- .../Onboarding2/pages/KeycardEnterPinPage.qml | 12 +- .../Onboarding2/pages/KeycardEnterPukPage.qml | 41 +++- .../Onboarding2/pages/KeycardIntroPage.qml | 64 ++++-- .../Onboarding2/pages/LoginScreen.qml | 5 +- ui/app/AppLayouts/Onboarding2/pages/qmldir | 1 + ui/app/AppLayouts/Onboarding2/qmldir | 1 + .../Onboarding2/stores/OnboardingStore.qml | 5 + 20 files changed, 548 insertions(+), 71 deletions(-) create mode 100644 storybook/pages/UnlockWithPukFlowPage.qml create mode 100644 ui/app/AppLayouts/Onboarding2/UnlockWithPukFlow.qml diff --git a/storybook/pages/KeycardEnterPukPagePage.qml b/storybook/pages/KeycardEnterPukPagePage.qml index f5a591a88b3..60f4121e966 100644 --- a/storybook/pages/KeycardEnterPukPagePage.qml +++ b/storybook/pages/KeycardEnterPukPagePage.qml @@ -11,9 +11,13 @@ Item { KeycardEnterPukPage { id: page anchors.fill: parent + remainingAttempts: 3 tryToSetPukFunction: (puk) => { console.warn("!!! ATTEMPTED PUK:", puk) - return puk === root.existingPuk + const valid = puk === root.existingPuk + if (!valid) + remainingAttempts-- + return valid } onKeycardPukEntered: (puk) => { console.warn("!!! CORRECT PUK:", puk) diff --git a/storybook/pages/KeycardIntroPagePage.qml b/storybook/pages/KeycardIntroPagePage.qml index 609600b9d80..ba46d6d7205 100644 --- a/storybook/pages/KeycardIntroPagePage.qml +++ b/storybook/pages/KeycardIntroPagePage.qml @@ -80,7 +80,8 @@ Item { { 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.BlockedPIN, text: "BlockedPIN" }, + { value: Onboarding.KeycardState.BlockedPUK, text: "BlockedPUK" }, { value: Onboarding.KeycardState.NotEmpty, text: "NotEmpty" }, { value: Onboarding.KeycardState.Empty, text: "Empty" } ] diff --git a/storybook/pages/LoginScreenPage.qml b/storybook/pages/LoginScreenPage.qml index 39d5b54d6b2..766f128623a 100644 --- a/storybook/pages/LoginScreenPage.qml +++ b/storybook/pages/LoginScreenPage.qml @@ -30,16 +30,16 @@ SplitView { // keycard property int keycardState: Onboarding.KeycardState.NoPCSCService - property int keycardRemainingPinAttempts: ctrlUnlockWithPuk.checked ? 1 : 5 + property int keycardRemainingPinAttempts: 3 function setPin(pin: string) { // -> bool logs.logEvent("OnboardingStore.setPin", ["pin"], arguments) const valid = pin === ctrlPin.text if (!valid) keycardRemainingPinAttempts-- // SIMULATION: decrease the remaining PIN attempts - if (keycardRemainingPinAttempts <= 0) { // SIMULATION: "lock" the keycard - keycardState = Onboarding.KeycardState.Locked - keycardRemainingPinAttempts = ctrlUnlockWithPuk.checked ? 1 : 5 + if (keycardRemainingPinAttempts <= 0) { // SIMULATION: "block" the keycard + keycardState = Onboarding.KeycardState.BlockedPIN + keycardRemainingPinAttempts = 0 } return valid } @@ -111,6 +111,7 @@ SplitView { onUnlockWithSeedphraseRequested: logs.logEvent("onUnlockWithSeedphraseRequested") onUnlockWithPukRequested: logs.logEvent("onUnlockWithPukRequested") onLostKeycard: logs.logEvent("onLostKeycard") + onKeycardFactoryResetRequested: logs.logEvent("onKeycardFactoryResetRequested") // mocks QtObject { @@ -229,11 +230,6 @@ SplitView { enabled: ctrlBiometrics.checked checked: ctrlBiometrics.checked } - Switch { - id: ctrlUnlockWithPuk - text: "Unlock with PUK available" - checked: true - } } RowLayout { @@ -264,11 +260,15 @@ SplitView { { 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.BlockedPIN, text: "BlockedPIN" }, + { value: Onboarding.KeycardState.BlockedPUK, text: "BlockedPUK" }, { value: Onboarding.KeycardState.NotEmpty, text: "NotEmpty" }, { value: Onboarding.KeycardState.Empty, text: "Empty" } ] - onActivated: store.keycardState = currentValue + onActivated: { + store.keycardState = currentValue + store.keycardRemainingPinAttempts = 3 + } Component.onCompleted: currentIndex = Qt.binding(() => indexOfValue(store.keycardState)) } } diff --git a/storybook/pages/OnboardingLayoutPage.qml b/storybook/pages/OnboardingLayoutPage.qml index c935f67409c..28a8b8dee08 100644 --- a/storybook/pages/OnboardingLayoutPage.qml +++ b/storybook/pages/OnboardingLayoutPage.qml @@ -31,6 +31,7 @@ SplitView { 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 puk: "111111111111" readonly property string password: "somepassword" // TODO simulation @@ -96,7 +97,8 @@ SplitView { property int addKeyPairState: Onboarding.AddKeyPairState.InProgress property int syncState: Onboarding.SyncState.InProgress - property int keycardRemainingPinAttempts: ctrlUnlockWithPuk.checked ? 1 : 5 + property int keycardRemainingPinAttempts: 2 + property int keycardRemainingPukAttempts: 3 function setPin(pin: string) { // -> bool logs.logEvent("OnboardingStore.setPin", ["pin"], arguments) @@ -104,9 +106,21 @@ SplitView { const valid = pin === mockDriver.pin if (!valid) keycardRemainingPinAttempts-- - if (keycardRemainingPinAttempts <= 0) { // SIMULATION: "lock" the keycard - keycardState = Onboarding.KeycardState.Locked - keycardRemainingPinAttempts = ctrlUnlockWithPuk.checked ? 1 : 5 + if (keycardRemainingPinAttempts <= 0) { // SIMULATION: "block" the keycard + keycardState = Onboarding.KeycardState.BlockedPIN + keycardRemainingPinAttempts = 0 + } + return valid + } + + function setPuk(puk) { // -> bool + logs.logEvent("OnboardingStore.setPuk", ["puk"], arguments) + const valid = puk === mockDriver.puk + if (!valid) + keycardRemainingPukAttempts-- + if (keycardRemainingPukAttempts <= 0) { // SIMULATION: "block" the keycard + keycardState = Onboarding.KeycardState.BlockedPUK + keycardRemainingPukAttempts = 0 } return valid } @@ -187,7 +201,11 @@ SplitView { } } - onReloadKeycardRequested: store.keycardState = Onboarding.KeycardState.NoPCSCService + onReloadKeycardRequested: { + store.keycardState = Onboarding.KeycardState.NoPCSCService + store.keycardRemainingPinAttempts = 2 + store.keycardRemainingPukAttempts = 3 + } // mocks QtObject { @@ -263,13 +281,25 @@ SplitView { visible: onboarding.stack.currentItem instanceof KeycardEnterPinPage || onboarding.stack.currentItem instanceof KeycardCreatePinPage || - (onboarding.stack.currentItem instanceof LoginScreen && onboarding.stack.currentItem.selectedProfileIsKeycard) + (onboarding.stack.currentItem instanceof LoginScreen && onboarding.stack.currentItem.selectedProfileIsKeycard && store.keycardState === Onboarding.KeycardState.NotEmpty) 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 KeycardEnterPukPage + + text: "Copy valid PUK (\"%1\")".arg(mockDriver.puk) + focusPolicy: Qt.NoFocus + onClicked: ClipboardUtils.setText(mockDriver.puk) + } + Button { anchors.bottom: parent.bottom anchors.right: parent.right @@ -483,19 +513,15 @@ SplitView { Switch { id: ctrlTouchIdUser text: "Touch ID login" + visible: ctrlLoginScreen.checked enabled: ctrlBiometrics.checked checked: ctrlBiometrics.checked } - Switch { - id: ctrlUnlockWithPuk - text: "Unlock with PUK available" - checked: true - } - Text { id: ctrlLoginResult property string result: "🯄" + visible: ctrlLoginScreen.checked text: "Login result: %1".arg(result) } } @@ -522,7 +548,8 @@ SplitView { { 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.BlockedPIN, text: "BlockedPIN" }, + { value: Onboarding.KeycardState.BlockedPUK, text: "BlockedPUK" }, { value: Onboarding.KeycardState.NotEmpty, text: "NotEmpty" }, { value: Onboarding.KeycardState.Empty, text: "Empty" } ] diff --git a/storybook/pages/UnlockWithPukFlowPage.qml b/storybook/pages/UnlockWithPukFlowPage.qml new file mode 100644 index 00000000000..b73197454ad --- /dev/null +++ b/storybook/pages/UnlockWithPukFlowPage.qml @@ -0,0 +1,210 @@ +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.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.pages 1.0 +import AppLayouts.Onboarding.enums 1.0 + +SplitView { + id: root + orientation: Qt.Vertical + + Logs { id: logs } + + Item { + SplitView.fillWidth: true + SplitView.fillHeight: true + + OnboardingStackView { + id: stackView + anchors.fill: parent + Component.onCompleted: flow.init() + } + + // needs to be on top of the stack + // we're here only to provide the Back button feature + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.BackButton + cursorShape: undefined // don't override the cursor coming from the stack + enabled: stackView.depth > 1 && !stackView.busy + onClicked: stackView.pop() + } + + StatusBackButton { + width: 44 + height: 44 + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: Theme.padding + + opacity: stackView.depth > 1 && !stackView.busy && stackView.backAvailable ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { duration: 100 } + } + + onClicked: stackView.pop() + } + + Button { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 10 + + visible: stackView.currentItem instanceof KeycardEnterPukPage + + text: "Copy valid PUK (\"%1\")".arg(mockDriver.puk) + focusPolicy: Qt.NoFocus + onClicked: { + ClipboardUtils.setText(mockDriver.puk) + } + } + } + + UnlockWithPukFlow { + id: flow + stackView: stackView + keycardState: mockDriver.keycardState + tryToSetPukFunction: mockDriver.setPuk + remainingAttempts: mockDriver.keycardRemainingPukAttempts + keycardPinInfoPageDelay: 1000 + onKeycardPinCreated: (pin) => { + logs.logEvent("keycardPinCreated", ["pin"], arguments) + console.warn("!!! PIN CREATED:", pin) + } + onReloadKeycardRequested: mockDriver.keycardState = Onboarding.KeycardState.NoPCSCService + onKeycardFactoryResetRequested: { + logs.logEvent("keycardFactoryResetRequested", ["pin"], arguments) + console.warn("!!! FACTORY RESET REQUESTED") + } + onFinished: { + console.warn("!!! UNLOCK WITH PUK FINISHED") + logs.logEvent("finished") + console.warn("!!! RESTARTING FLOW") + + stackView.clear() + mockDriver.reset() + flow.init() + } + } + + QtObject { + id: mockDriver + + function reset() { + keycardState = Onboarding.KeycardState.NoPCSCService + keycardRemainingPukAttempts = 3 + } + + property int keycardState: Onboarding.KeycardState.NoPCSCService + property int keycardRemainingPukAttempts: 3 + + function setPuk(puk) { // -> bool + logs.logEvent("setPuk", ["puk"], arguments) + console.warn("!!! SET PUK:", puk) + const valid = puk === mockDriver.puk + if (!valid) + keycardRemainingPukAttempts-- + if (keycardRemainingPukAttempts <= 0) { // SIMULATION: "block" the keycard + keycardState = Onboarding.KeycardState.BlockedPUK + keycardRemainingPukAttempts = 0 + } + return valid + } + + readonly property string puk: "111111111111" + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 200 + SplitView.preferredHeight: 200 + + logsView.logText: logs.logText + + ColumnLayout { + anchors.fill: parent + + spacing: 10 + + TextField { + Layout.fillWidth: true + + text: { + const stack = stackView + 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 { + 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.BlockedPIN, text: "BlockedPIN" }, + { value: Onboarding.KeycardState.BlockedPUK, text: "BlockedPUK" }, + { value: Onboarding.KeycardState.NotEmpty, text: "NotEmpty" }, + { value: Onboarding.KeycardState.Empty, text: "Empty" } + ] + + RoundButton { + text: modelData.text + checkable: true + checked: flow.keycardState === modelData.value + + ButtonGroup.group: keycardStateButtonGroup + + onClicked: mockDriver.keycardState = modelData.value + } + } + } + } + } + } +} + +// category: Onboarding +// status: good diff --git a/ui/StatusQ/src/onboarding/enums.h b/ui/StatusQ/src/onboarding/enums.h index 029d612a7c2..f27d18c6ba0 100644 --- a/ui/StatusQ/src/onboarding/enums.h +++ b/ui/StatusQ/src/onboarding/enums.h @@ -39,7 +39,8 @@ class OnboardingEnums WrongKeycard, NotKeycard, MaxPairingSlotsReached, - Locked, + BlockedPIN, // PIN remaining attempts == 0 + BlockedPUK, // PUK remaining attempts == 0 // exit states NotEmpty, Empty diff --git a/ui/app/AppLayouts/Onboarding2/KeycardCreateProfileFlow.qml b/ui/app/AppLayouts/Onboarding2/KeycardCreateProfileFlow.qml index 38e201563f9..836487c715b 100644 --- a/ui/app/AppLayouts/Onboarding2/KeycardCreateProfileFlow.qml +++ b/ui/app/AppLayouts/Onboarding2/KeycardCreateProfileFlow.qml @@ -61,6 +61,7 @@ SQUtils.QObject { KeycardIntroPage { keycardState: root.keycardState displayPromoBanner: root.displayKeycardPromoBanner + factoryResetAvailable: true onReloadKeycardRequested: { root.reloadKeycardRequested() diff --git a/ui/app/AppLayouts/Onboarding2/LoginWithKeycardFlow.qml b/ui/app/AppLayouts/Onboarding2/LoginWithKeycardFlow.qml index c296a5e785b..ebf40cf141e 100644 --- a/ui/app/AppLayouts/Onboarding2/LoginWithKeycardFlow.qml +++ b/ui/app/AppLayouts/Onboarding2/LoginWithKeycardFlow.qml @@ -26,6 +26,7 @@ SQUtils.QObject { signal seedphraseSubmitted(string seedphrase) signal reloadKeycardRequested signal keycardFactoryResetRequested + signal unlockWithPukRequested() signal createProfileWithEmptyKeycardRequested signal finished @@ -59,11 +60,13 @@ SQUtils.QObject { KeycardIntroPage { keycardState: root.keycardState displayPromoBanner: root.displayKeycardPromoBanner - unlockUsingSeedphrase: true + unblockUsingSeedphraseAvailable: true + unblockWithPukAvailable: root.remainingAttempts === 1 || root.remainingAttempts === 2 onReloadKeycardRequested: d.reload() onKeycardFactoryResetRequested: root.keycardFactoryResetRequested() onUnlockWithSeedphraseRequested: root.stackView.push(seedphrasePage) + onUnlockWithPukRequested: root.unlockWithPukRequested() onEmptyKeycardDetected: root.stackView.replace(keycardEmptyPage) onNotEmptyKeycardDetected: root.stackView.replace(keycardEnterPinPage) } @@ -86,7 +89,7 @@ SQUtils.QObject { KeycardEnterPinPage { tryToSetPinFunction: root.tryToSetPinFunction remainingAttempts: root.remainingAttempts - unlockUsingSeedphrase: true + unblockUsingSeedphraseAvailable: true onKeycardPinEntered: (pin) => { Backpressure.debounce(root, root.keycardPinInfoPageDelay, () => { @@ -105,8 +108,8 @@ SQUtils.QObject { id: seedphrasePage SeedphrasePage { - title: qsTr("Unlock Keycard using the recovery phrase") - btnContinueText: qsTr("Unlock") + title: qsTr("Unblock Keycard using the recovery phrase") + btnContinueText: qsTr("Unblock Keycard") isSeedPhraseValid: root.isSeedPhraseValid onSeedphraseSubmitted: (seedphrase) => { root.seedphraseSubmitted(seedphrase) diff --git a/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml b/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml index eea22ea7882..8de7571bdb0 100644 --- a/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml +++ b/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml @@ -16,7 +16,8 @@ SQUtils.QObject { required property int addKeyPairState required property int syncState required property var seedWords - required property int remainingAttempts + required property int remainingAttempts // FIXME change to `remainingPinAttempts` + required property int remainingPukAttempts required property bool biometricsAvailable required property bool displayKeycardPromoBanner @@ -29,6 +30,7 @@ SQUtils.QObject { required property var isSeedPhraseValid required property var validateConnectionString required property var tryToSetPinFunction + required property var tryToSetPukFunction signal keycardPinCreated(string pin) signal keycardPinEntered(string pin) @@ -58,6 +60,10 @@ SQUtils.QObject { root.stackView.push(loginPage) } + function startUnlockWithPukFlow() { + unlockWithPukFlow.init() + } + QtObject { id: d @@ -243,7 +249,27 @@ SQUtils.QObject { onReloadKeycardRequested: root.reloadKeycardRequested() onCreateProfileWithEmptyKeycardRequested: keycardCreateProfileFlow.init() onKeycardFactoryResetRequested: root.keycardFactoryResetRequested() + onUnlockWithPukRequested: startUnlockWithPukFlow() + + onFinished: { + d.flow = Onboarding.SecondaryFlow.LoginWithKeycard + d.pushOrSkipBiometricsPage() + } + } + + UnlockWithPukFlow { + id: unlockWithPukFlow + stackView: root.stackView + keycardState: root.keycardState + tryToSetPukFunction: root.tryToSetPukFunction + remainingAttempts: root.remainingPukAttempts + + keycardPinInfoPageDelay: root.keycardPinInfoPageDelay + + onReloadKeycardRequested: root.reloadKeycardRequested() + onKeycardPinCreated: (pin) => root.keycardPinCreated(pin) + onKeycardFactoryResetRequested: root.keycardFactoryResetRequested() onFinished: { d.flow = Onboarding.SecondaryFlow.LoginWithKeycard d.pushOrSkipBiometricsPage() diff --git a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml index 5854d7d91f1..65b2c4ba850 100644 --- a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml +++ b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml @@ -103,10 +103,6 @@ Page { objectName: "stack" anchors.fill: parent - - readonly property bool backAvailable: - stack.currentItem ? (stack.currentItem.backAvailableHint ?? true) - : false } // needs to be on top of the stack @@ -155,7 +151,9 @@ Page { isSeedPhraseValid: root.onboardingStore.validMnemonic validateConnectionString: root.onboardingStore.validateLocalPairingConnectionString tryToSetPinFunction: root.onboardingStore.setPin + tryToSetPukFunction: root.onboardingStore.setPuk remainingAttempts: root.onboardingStore.keycardRemainingPinAttempts + remainingPukAttempts: root.onboardingStore.keycardRemainingPukAttempts onKeycardPinCreated: (pin) => { d.keycardPin = pin @@ -179,12 +177,13 @@ Page { onSetPasswordRequested: (password) => d.password = password onEnableBiometricsRequested: (enabled) => d.enableBiometrics = enabled onFinished: (flow) => d.finishFlow(flow) - onKeycardFactoryResetRequested: ; // TODO invoke external popup and finish the flow + onKeycardFactoryResetRequested: console.warn("!!! FIXME OnboardingLayout::onKeycardFactoryResetRequested") } Component { id: loginScreenComponent LoginScreen { + id: loginScreen onboardingStore: root.onboardingStore loginAccountsModel: root.loginAccountsModel biometricsAvailable: root.biometricsAvailable @@ -194,9 +193,10 @@ Page { onOnboardingCreateProfileFlowRequested: onboardingFlow.startCreateProfileFlow() onOnboardingLoginFlowRequested: onboardingFlow.startLoginFlow() - onUnlockWithSeedphraseRequested: console.warn("!!! FIXME onUnlockWithSeedphraseRequested") - onUnlockWithPukRequested: console.warn("!!! FIXME onUnlockWithPukRequested") - onLostKeycard: console.warn("!!! FIXME onLostKeycard flow") + onUnlockWithSeedphraseRequested: console.warn("!!! FIXME OnboardingLayout::onUnlockWithSeedphraseRequested") + onUnlockWithPukRequested: onboardingFlow.startUnlockWithPukFlow() // TODO should it finish with an onboarding flow or a login screen flow? :o) + onKeycardFactoryResetRequested: console.warn("!!! FIXME OnboardingLayout::onKeycardFactoryResetRequested") + onLostKeycard: console.warn("!!! FIXME OnboardingLayout::onLostKeycard") } } diff --git a/ui/app/AppLayouts/Onboarding2/OnboardingStackView.qml b/ui/app/AppLayouts/Onboarding2/OnboardingStackView.qml index 2e37a192406..f4cc204f6fb 100644 --- a/ui/app/AppLayouts/Onboarding2/OnboardingStackView.qml +++ b/ui/app/AppLayouts/Onboarding2/OnboardingStackView.qml @@ -6,6 +6,9 @@ import StatusQ.Core.Theme 0.1 StackView { id: root + readonly property bool backAvailable: currentItem ? (currentItem.backAvailableHint ?? true) + : false + QtObject { id: d diff --git a/ui/app/AppLayouts/Onboarding2/UnlockWithPukFlow.qml b/ui/app/AppLayouts/Onboarding2/UnlockWithPukFlow.qml new file mode 100644 index 00000000000..a7a10b3b14c --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/UnlockWithPukFlow.qml @@ -0,0 +1,108 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +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 AppLayouts.Onboarding2.pages 1.0 +import AppLayouts.Onboarding.enums 1.0 + +SQUtils.QObject { + id: root + + required property StackView stackView + + required property int keycardState + required property var tryToSetPukFunction + required property int remainingAttempts + + required property int keycardPinInfoPageDelay + + signal keycardPinCreated(string pin) + signal reloadKeycardRequested + signal keycardFactoryResetRequested + signal finished + + function init() { + root.stackView.push(d.initialComponent()) + } + + QtObject { + id: d + + function initialComponent() { + if (root.keycardState === Onboarding.KeycardState.BlockedPIN) + return keycardEnterPukPage + if (root.keycardState === Onboarding.KeycardState.Empty || root.keycardState === Onboarding.KeycardState.NotEmpty) + return keycardUnlockedPage + return keycardIntroPage + } + + function reload() { + root.reloadKeycardRequested() + root.stackView.replace(d.initialComponent(), StackView.PopTransition) + } + + function finishWithFactoryReset() { + root.keycardFactoryResetRequested() + root.finished() + } + } + + Component { + id: keycardIntroPage + + KeycardIntroPage { + keycardState: root.keycardState + unblockWithPukAvailable: true + unblockUsingSeedphraseAvailable: true + onReloadKeycardRequested: d.reload() + onKeycardFactoryResetRequested: d.finishWithFactoryReset() + onEmptyKeycardDetected: root.stackView.replace(keycardUnlockedPage) + onNotEmptyKeycardDetected: root.stackView.replace(keycardUnlockedPage) + onUnlockWithPukRequested: root.stackView.push(keycardEnterPukPage) + } + } + + Component { + id: keycardEnterPukPage + + KeycardEnterPukPage { + tryToSetPukFunction: root.tryToSetPukFunction + remainingAttempts: root.remainingAttempts + onKeycardPukEntered: (puk) => root.stackView.replace(keycardCreatePinPage) + onKeycardFactoryResetRequested: d.finishWithFactoryReset() + } + } + + Component { + id: keycardCreatePinPage + + KeycardCreatePinPage { + onKeycardPinCreated: (pin) => { + Backpressure.debounce(root, root.keycardPinInfoPageDelay, () => { + root.keycardPinCreated(pin) + root.stackView.replace(keycardUnlockedPage, {title: qsTr("Unlock successful")}) + })() + } + } + } + + Component { + id: keycardUnlockedPage + + KeycardBasePage { + image.source: Theme.png("onboarding/keycard/success") + title: qsTr("Your Keycard is already unlocked!") + buttons: [ + StatusButton { + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Continue") + onClicked: root.finished() + } + ] + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/components/LoginKeycardBox.qml b/ui/app/AppLayouts/Onboarding2/components/LoginKeycardBox.qml index 5f903c09abe..61ec99af50d 100644 --- a/ui/app/AppLayouts/Onboarding2/components/LoginKeycardBox.qml +++ b/ui/app/AppLayouts/Onboarding2/components/LoginKeycardBox.qml @@ -24,9 +24,11 @@ Control { signal pinEditedManually() - signal loginRequested(string pin) signal unlockWithSeedphraseRequested() signal unlockWithPukRequested() + signal keycardFactoryResetRequested() + + signal loginRequested(string pin) function clear() { d.wrongPin = false @@ -76,16 +78,21 @@ Control { spacing: 12 visible: false MaybeOutlineButton { - id: btnUnlockWithPuk width: parent.width - visible: root.keycardRemainingPinAttempts === 1 || root.keycardRemainingPinAttempts === 2 - text: qsTr("Unlock with PUK") + visible: root.keycardState === Onboarding.KeycardState.BlockedPUK + text: qsTr("Factory reset Keycard") + onClicked: root.keycardFactoryResetRequested() + } + MaybeOutlineButton { + width: parent.width + visible: root.keycardState === Onboarding.KeycardState.BlockedPIN + text: qsTr("Unblock with PUK") onClicked: root.unlockWithPukRequested() } MaybeOutlineButton { - id: btnUnlockWithSeedphrase width: parent.width - text: qsTr("Unlock with recovery phrase") + visible: root.keycardState === Onboarding.KeycardState.BlockedPIN + text: qsTr("Unblock with recovery phrase") onClicked: root.unlockWithSeedphraseRequested() } } @@ -172,12 +179,13 @@ Control { } }, State { - name: "locked" - when: root.keycardState === Onboarding.KeycardState.Locked + name: "blocked" + when: root.keycardState === Onboarding.KeycardState.BlockedPIN || + root.keycardState === Onboarding.KeycardState.BlockedPUK PropertyChanges { target: infoText color: Theme.palette.dangerColor1 - text: qsTr("Keycard locked") + text: qsTr("Keycard blocked") } PropertyChanges { target: lockedButtons diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml index 047838ab8ef..ad263aeb6b5 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml @@ -17,7 +17,7 @@ KeycardBasePage { property var tryToSetPinFunction: (pin) => { console.error("tryToSetPinFunction: IMPLEMENT ME"); return false } required property int remainingAttempts - property bool unlockUsingSeedphrase + property bool unblockUsingSeedphraseAvailable signal keycardPinEntered(string pin) signal reloadKeycardRequested() @@ -68,7 +68,7 @@ KeycardBasePage { StatusButton { id: btnUnlockWithSeedphrase visible: false - text: qsTr("Unlock with recovery phrase") + text: qsTr("Unblock with recovery phrase") anchors.horizontalCenter: parent.horizontalCenter onClicked: root.unlockWithSeedphraseRequested() }, @@ -89,11 +89,11 @@ KeycardBasePage { states: [ State { - name: "locked" + name: "blocked" when: root.remainingAttempts <= 0 PropertyChanges { target: root - title: "".arg(Theme.palette.dangerColor1) + qsTr("Keycard locked") + "" + title: "".arg(Theme.palette.dangerColor1) + qsTr("Keycard blocked") + "" } PropertyChanges { target: pinInput @@ -105,11 +105,11 @@ KeycardBasePage { } PropertyChanges { target: btnFactoryReset - visible: !root.unlockUsingSeedphrase + visible: !root.unblockUsingSeedphraseAvailable } PropertyChanges { target: btnUnlockWithSeedphrase - visible: root.unlockUsingSeedphrase + visible: root.unblockUsingSeedphraseAvailable } PropertyChanges { target: btnReload diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPukPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPukPage.qml index bf1f0fb54f1..c644fbd599e 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPukPage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPukPage.qml @@ -5,6 +5,7 @@ import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import StatusQ.Controls.Validators 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Backpressure 0.1 import AppLayouts.Onboarding2.controls 1.0 @@ -14,8 +15,10 @@ KeycardBasePage { id: root property var tryToSetPukFunction: (puk) => { console.error("tryToSetPukFunction: IMPLEMENT ME"); return false } + required property int remainingAttempts signal keycardPukEntered(string puk) + signal keycardFactoryResetRequested() image.source: Theme.png("onboarding/keycard/reading") @@ -47,16 +50,52 @@ KeycardBasePage { StatusBaseText { id: errorText anchors.horizontalCenter: parent.horizontalCenter - text: qsTr("The PUK is incorrect, try entering it again") + text: qsTr("%n attempt(s) remaining", "", root.remainingAttempts) font.pixelSize: Theme.tertiaryTextFontSize color: Theme.palette.dangerColor1 visible: false + }, + StatusButton { + id: btnFactoryReset + width: 320 + anchors.horizontalCenter: parent.horizontalCenter + anchors.topMargin: Theme.halfPadding + visible: false + text: qsTr("Factory reset Keycard") + onClicked: root.keycardFactoryResetRequested() } ] state: "entering" states: [ + State { + name: "locked" + when: root.remainingAttempts <= 0 + PropertyChanges { + target: root + title: "".arg(Theme.palette.dangerColor1) + qsTr("Keycard locked") + "" + } + PropertyChanges { + target: pukInput + enabled: false + } + PropertyChanges { + target: image + source: Theme.png("onboarding/keycard/error") + } + PropertyChanges { + target: btnFactoryReset + visible: true + } + StateChangeScript { + script: { + Backpressure.debounce(root, 100, function() { + pukInput.clearPin() + })() + } + } + }, State { name: "incorrect" when: !!d.tempPuk && !d.pukValid diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml index 31e7d46f419..04bf12929ea 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml @@ -17,10 +17,14 @@ KeycardBasePage { required property int keycardState // cf Onboarding.KeycardState property bool displayPromoBanner - property bool unlockUsingSeedphrase + + property bool unblockWithPukAvailable + property bool unblockUsingSeedphraseAvailable + property bool factoryResetAvailable signal keycardFactoryResetRequested() signal unlockWithSeedphraseRequested() + signal unlockWithPukRequested() signal reloadKeycardRequested() signal emptyKeycardDetected() signal notEmptyKeycardDetected() @@ -81,19 +85,26 @@ KeycardBasePage { buttons: [ MaybeOutlineButton { - id: btnFactoryReset + id: btnUnblockWithPuk visible: false - text: qsTr("Factory reset Keycard") + text: qsTr("Unblock using PUK") anchors.horizontalCenter: parent.horizontalCenter - onClicked: root.keycardFactoryResetRequested() + onClicked: root.unlockWithPukRequested() }, MaybeOutlineButton { - id: btnUnlockWithSeedphrase + id: btnUnblockWithSeedphrase visible: false - text: qsTr("Unlock with recovery phrase") + text: qsTr("Unblock with recovery phrase") anchors.horizontalCenter: parent.horizontalCenter onClicked: root.unlockWithSeedphraseRequested() }, + MaybeOutlineButton { + id: btnFactoryReset + visible: false + text: qsTr("Factory reset Keycard") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.keycardFactoryResetRequested() + }, MaybeOutlineButton { id: btnReload visible: false @@ -192,22 +203,47 @@ KeycardBasePage { } }, State { - name: "locked" - when: root.keycardState === Onboarding.KeycardState.Locked + name: "blockedPin" + when: root.keycardState === Onboarding.KeycardState.BlockedPIN PropertyChanges { target: root - title: "".arg(Theme.palette.dangerColor1) + qsTr("Keycard locked") + "" - subtitle: root.unlockUsingSeedphrase ? qsTr("The Keycard you have inserted is locked, you will need to unlock it using the recovery phrase or insert a different one") - : qsTr("The Keycard you have inserted is locked, you will need to factory reset it or insert a different one") + title: "".arg(Theme.palette.dangerColor1) + qsTr("Keycard blocked") + "" + subtitle: qsTr("The Keycard you have inserted is blocked, you will need to unblock it or insert a different one") image.source: Theme.png("onboarding/keycard/error") } + PropertyChanges { + target: btnUnblockWithPuk + visible: root.unblockWithPukAvailable + } + PropertyChanges { + target: btnUnblockWithSeedphrase + visible: root.unblockUsingSeedphraseAvailable + } PropertyChanges { target: btnFactoryReset - visible: !root.unlockUsingSeedphrase + visible: root.factoryResetAvailable } PropertyChanges { - target: btnUnlockWithSeedphrase - visible: root.unlockUsingSeedphrase + target: btnReload + visible: true + } + }, + State { + name: "blockedPuk" + when: root.keycardState === Onboarding.KeycardState.BlockedPUK + PropertyChanges { + target: root + title: "".arg(Theme.palette.dangerColor1) + qsTr("Keycard blocked") + "" + subtitle: qsTr("The Keycard you have inserted is blocked, you will need to unblock it, factory reset or insert a different one") + image.source: Theme.png("onboarding/keycard/error") + } + PropertyChanges { + target: btnUnblockWithSeedphrase + visible: root.unblockUsingSeedphraseAvailable + } + PropertyChanges { + target: btnFactoryReset + visible: true } PropertyChanges { target: btnReload diff --git a/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml b/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml index 20aab1d796e..557b03ed1d2 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml @@ -41,6 +41,7 @@ OnboardingPage { signal onboardingLoginFlowRequested() signal unlockWithSeedphraseRequested() signal unlockWithPukRequested() + signal keycardFactoryResetRequested() signal lostKeycard() QtObject { @@ -176,7 +177,8 @@ OnboardingPage { Layout.fillWidth: true Layout.preferredHeight: 64 loginAccountsModel: root.loginAccountsModel - currentKeycardLocked: root.onboardingStore.keycardState === Onboarding.KeycardState.Locked + currentKeycardLocked: root.onboardingStore.keycardState === Onboarding.KeycardState.BlockedPIN || + root.onboardingStore.keycardState === Onboarding.KeycardState.BlockedPUK onLoginUserRequested: (keyUid) => { d.resetBiometricsResult() d.settings.lastKeyUid = keyUid @@ -224,6 +226,7 @@ OnboardingPage { keycardRemainingPinAttempts: root.onboardingStore.keycardRemainingPinAttempts onUnlockWithSeedphraseRequested: root.unlockWithSeedphraseRequested() onUnlockWithPukRequested: root.unlockWithPukRequested() + onKeycardFactoryResetRequested: root.keycardFactoryResetRequested() onPinEditedManually: { // reset state when typing the PIN manually; not to break the bindings inside the component d.resetBiometricsResult() diff --git a/ui/app/AppLayouts/Onboarding2/pages/qmldir b/ui/app/AppLayouts/Onboarding2/pages/qmldir index 452e42e2be1..23b552d8cfe 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/qmldir +++ b/ui/app/AppLayouts/Onboarding2/pages/qmldir @@ -8,6 +8,7 @@ CreatePasswordPage 1.0 CreatePasswordPage.qml CreateProfilePage 1.0 CreateProfilePage.qml EnableBiometricsPage 1.0 EnableBiometricsPage.qml HelpUsImproveStatusPage 1.0 HelpUsImproveStatusPage.qml +KeycardBasePage 1.0 KeycardBasePage.qml KeycardCreatePinPage 1.0 KeycardCreatePinPage.qml KeycardEmptyPage 1.0 KeycardEmptyPage.qml KeycardEnterPinPage 1.0 KeycardEnterPinPage.qml diff --git a/ui/app/AppLayouts/Onboarding2/qmldir b/ui/app/AppLayouts/Onboarding2/qmldir index 43c877b234f..b8ff437d1ff 100644 --- a/ui/app/AppLayouts/Onboarding2/qmldir +++ b/ui/app/AppLayouts/Onboarding2/qmldir @@ -6,4 +6,5 @@ OnboardingFlow 1.0 OnboardingFlow.qml OnboardingLayout 1.0 OnboardingLayout.qml OnboardingStackView 1.0 OnboardingStackView.qml RecoveryPhraseCreateProfileFlow 1.0 RecoveryPhraseCreateProfileFlow.qml +UnlockWithPukFlow 1.0 UnlockWithPukFlow.qml UseRecoveryPhraseFlow 1.0 UseRecoveryPhraseFlow.qml diff --git a/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml b/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml index d1265bf6b90..58343d567b4 100644 --- a/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml +++ b/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml @@ -21,11 +21,16 @@ QtObject { // keycard readonly property int keycardState: d.onboardingModuleInst.keycardState // cf. enum Onboarding.KeycardState readonly property int keycardRemainingPinAttempts: d.onboardingModuleInst.keycardRemainingPinAttempts + readonly property int keycardRemainingPukAttempts: d.onboardingModuleInst.keycardRemainingPukAttempts function setPin(pin: string) { // -> bool return d.onboardingModuleInst.setPin(pin) } + function setPuk(puk: string) { // -> bool + return d.onboardingModuleInst.setPuk(puk) + } + readonly property int addKeyPairState: d.onboardingModuleInst.addKeyPairState // cf. enum Onboarding.AddKeyPairState function startKeypairTransfer() { // -> void d.onboardingModuleInst.startKeypairTransfer()