diff --git a/libs/StatusQ/qml/Status/Core/StatusBaseText.qml b/libs/StatusQ/qml/Status/Core/StatusBaseText.qml index bc128ebfac..5cc724774a 100644 --- a/libs/StatusQ/qml/Status/Core/StatusBaseText.qml +++ b/libs/StatusQ/qml/Status/Core/StatusBaseText.qml @@ -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 diff --git a/storybook/pages/BackupSeedphraseFlowPage.qml b/storybook/pages/BackupSeedphraseFlowPage.qml new file mode 100644 index 0000000000..c7c7b6a8ee --- /dev/null +++ b/storybook/pages/BackupSeedphraseFlowPage.qml @@ -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 diff --git a/storybook/pages/ColorsPage.qml b/storybook/pages/ColorsPage.qml index c8d4b68639..fc05a4c3e5 100644 --- a/storybook/pages/ColorsPage.qml +++ b/storybook/pages/ColorsPage.qml @@ -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 { diff --git a/storybook/pages/DidYouKnowSplashScreenPage.qml b/storybook/pages/DidYouKnowSplashScreenPage.qml index bb55d7a49e..77d3bf30d2 100644 --- a/storybook/pages/DidYouKnowSplashScreenPage.qml +++ b/storybook/pages/DidYouKnowSplashScreenPage.qml @@ -19,23 +19,29 @@ SplitView { SplitView.fillHeight: true SplitView.fillWidth: true progress: progressSlider.position + messagesEnabled: ctrlMessagesEnabled.checked } } Pane { - SplitView.minimumWidth: 300 - SplitView.preferredWidth: 300 - RowLayout { - Label { - text: "Progress" - } - Slider { + SplitView.minimumWidth: 300 + SplitView.preferredWidth: 300 + ColumnLayout { + Layout.fillWidth: true + Label { + text: "Progress" + } + Slider { id: progressSlider - } - } + } + Switch { + id: ctrlMessagesEnabled + text: "Messages enabled" + } + } } } // category: Panels - +// status: good // https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba⎜Desktop?node-id=25878%3A518438&t=C7xTpNib38t7s7XU-4 diff --git a/storybook/pages/KeycardAddKeyPairPagePage.qml b/storybook/pages/KeycardAddKeyPairPagePage.qml new file mode 100644 index 0000000000..a8e7ca5c19 --- /dev/null +++ b/storybook/pages/KeycardAddKeyPairPagePage.qml @@ -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 diff --git a/storybook/pages/KeycardCreatePinPagePage.qml b/storybook/pages/KeycardCreatePinPagePage.qml new file mode 100644 index 0000000000..01dc4f57e5 --- /dev/null +++ b/storybook/pages/KeycardCreatePinPagePage.qml @@ -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 diff --git a/storybook/pages/KeycardEnterPinPagePage.qml b/storybook/pages/KeycardEnterPinPagePage.qml new file mode 100644 index 0000000000..4a58a5e7a1 --- /dev/null +++ b/storybook/pages/KeycardEnterPinPagePage.qml @@ -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 diff --git a/storybook/pages/KeycardIntroPagePage.qml b/storybook/pages/KeycardIntroPagePage.qml new file mode 100644 index 0000000000..609600b9d8 --- /dev/null +++ b/storybook/pages/KeycardIntroPagePage.qml @@ -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 diff --git a/storybook/pages/OnboardingLayoutPage.qml b/storybook/pages/OnboardingLayoutPage.qml new file mode 100644 index 0000000000..47c97badda --- /dev/null +++ b/storybook/pages/OnboardingLayoutPage.qml @@ -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 diff --git a/storybook/pages/StatusPinInputPage.qml b/storybook/pages/StatusPinInputPage.qml new file mode 100644 index 0000000000..b33b4e65b8 --- /dev/null +++ b/storybook/pages/StatusPinInputPage.qml @@ -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 diff --git a/storybook/pages/SyncProgressPagePage.qml b/storybook/pages/SyncProgressPagePage.qml new file mode 100644 index 0000000000..4963773aad --- /dev/null +++ b/storybook/pages/SyncProgressPagePage.qml @@ -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 diff --git a/storybook/pages/SyncingEnterCodePage.qml b/storybook/pages/SyncingEnterCodePage.qml index d423f036d7..0b762994b7 100644 --- a/storybook/pages/SyncingEnterCodePage.qml +++ b/storybook/pages/SyncingEnterCodePage.qml @@ -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 { diff --git a/storybook/qmlTests/tests/tst_OnboardingLayout.qml b/storybook/qmlTests/tests/tst_OnboardingLayout.qml new file mode 100644 index 0000000000..4a6e89b1bb --- /dev/null +++ b/storybook/qmlTests/tests/tst_OnboardingLayout.qml @@ -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) + } + } +} diff --git a/storybook/src/Storybook/FigmaUtils.qml b/storybook/src/Storybook/FigmaUtils.qml index a7902ca892..c68b2e00bf 100644 --- a/storybook/src/Storybook/FigmaUtils.qml +++ b/storybook/src/Storybook/FigmaUtils.qml @@ -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%-]+)/ diff --git a/storybook/stubs/AppLayouts/Onboarding2/stores/OnboardingStore.qml b/storybook/stubs/AppLayouts/Onboarding2/stores/OnboardingStore.qml new file mode 100644 index 0000000000..2587cd419c --- /dev/null +++ b/storybook/stubs/AppLayouts/Onboarding2/stores/OnboardingStore.qml @@ -0,0 +1,3 @@ +import QtQml 2.15 + +QtObject {} diff --git a/storybook/stubs/AppLayouts/Onboarding2/stores/qmldir b/storybook/stubs/AppLayouts/Onboarding2/stores/qmldir new file mode 100644 index 0000000000..ca75101238 --- /dev/null +++ b/storybook/stubs/AppLayouts/Onboarding2/stores/qmldir @@ -0,0 +1 @@ +OnboardingStore 1.0 OnboardingStore.qml diff --git a/storybook/stubs/AppLayouts/Profile/stores/PrivacyStore.qml b/storybook/stubs/AppLayouts/Profile/stores/PrivacyStore.qml index dcece8a991..2587cd419c 100644 --- a/storybook/stubs/AppLayouts/Profile/stores/PrivacyStore.qml +++ b/storybook/stubs/AppLayouts/Profile/stores/PrivacyStore.qml @@ -1,28 +1,3 @@ -import QtQuick 2.15 +import QtQml 2.15 -QtObject { - property QtObject privacyModule: QtObject { - signal passwordChanged(success: bool, errorMsg: string) - signal storeToKeychainError(errorDescription: string) - signal storeToKeychainSuccess() - } - - function tryStoreToKeyChain(errorDescription) { - if (generateMacKeyChainStoreError.checked) { - privacyModule.storeToKeychainError(errorDescription) - } else { - passwordView.localAccountSettings.storeToKeychainValue = Constants.keychain.storedValue.store - privacyModule.storeToKeychainSuccess() - privacyModule.passwordChanged(true, "") - } - } - - function tryRemoveFromKeyChain() { - if (generateMacKeyChainStoreError.checked) { - privacyModule.storeToKeychainError("Error removing from keychain") - } else { - passwordView.localAccountSettings.storeToKeychainValue = Constants.keychain.storedValue.notNow - privacyModule.storeToKeychainSuccess() - } - } -} +QtObject {} diff --git a/storybook/stubs/shared/stores/BIP39_en.qml b/storybook/stubs/shared/stores/BIP39_en.qml index b275a1e685..7301449ae4 100644 --- a/storybook/stubs/shared/stores/BIP39_en.qml +++ b/storybook/stubs/shared/stores/BIP39_en.qml @@ -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... diff --git a/storybook/stubs/shared/stores/MetricsStore.qml b/storybook/stubs/shared/stores/MetricsStore.qml new file mode 100644 index 0000000000..2587cd419c --- /dev/null +++ b/storybook/stubs/shared/stores/MetricsStore.qml @@ -0,0 +1,3 @@ +import QtQml 2.15 + +QtObject {} diff --git a/storybook/stubs/shared/stores/qmldir b/storybook/stubs/shared/stores/qmldir index 73ce556341..6e110ff922 100644 --- a/storybook/stubs/shared/stores/qmldir +++ b/storybook/stubs/shared/stores/qmldir @@ -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 diff --git a/test/e2e/constants/keycard.py b/test/e2e/constants/keycard.py index 19d897b233..7c47a63f05 100644 --- a/test/e2e/constants/keycard.py +++ b/test/e2e/constants/keycard.py @@ -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...' diff --git a/test/e2e/constants/onboarding.py b/test/e2e/constants/onboarding.py index 43f68fca96..8ede6b25a2 100644 --- a/test/e2e/constants/onboarding.py +++ b/test/e2e/constants/onboarding.py @@ -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']) diff --git a/test/e2e/constants/wallet.py b/test/e2e/constants/wallet.py index ae2b1281b1..e6e38cbbe2 100644 --- a/test/e2e/constants/wallet.py +++ b/test/e2e/constants/wallet.py @@ -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): diff --git a/test/e2e/gui/objects_map/onboarding_names.py b/test/e2e/gui/objects_map/onboarding_names.py index 00b8485a3d..65f47c3f59 100644 --- a/test/e2e/gui/objects_map/onboarding_names.py +++ b/test/e2e/gui/objects_map/onboarding_names.py @@ -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} diff --git a/test/e2e/gui/screens/onboarding.py b/test/e2e/gui/screens/onboarding.py index 81194bfdb9..84847c9838 100755 --- a/test/e2e/gui/screens/onboarding.py +++ b/test/e2e/gui/screens/onboarding.py @@ -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 diff --git a/test/e2e/tests/crtitical_tests_prs/test_onboarding_sync_with_code.py b/test/e2e/tests/crtitical_tests_prs/test_onboarding_sync_with_code.py index d2681227f6..b97d14ec12 100644 --- a/test/e2e/tests/crtitical_tests_prs/test_onboarding_sync_with_code.py +++ b/test/e2e/tests/crtitical_tests_prs/test_onboarding_sync_with_code.py @@ -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) diff --git a/test/e2e/tests/onboarding/test_onboarding_syncing.py b/test/e2e/tests/onboarding/test_onboarding_syncing.py index 1561ecb589..64f1e167f7 100644 --- a/test/e2e/tests/onboarding/test_onboarding_syncing.py +++ b/test/e2e/tests/onboarding/test_onboarding_syncing.py @@ -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') diff --git a/test/e2e/tests/onboarding/test_password_strength.py b/test/e2e/tests/onboarding/test_password_strength.py index c10b1381d8..585435b4f7 100644 --- a/test/e2e/tests/onboarding/test_password_strength.py +++ b/test/e2e/tests/onboarding/test_password_strength.py @@ -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 diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt index 0794b6b380..f96771eab7 100644 --- a/ui/StatusQ/CMakeLists.txt +++ b/ui/StatusQ/CMakeLists.txt @@ -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) diff --git a/ui/StatusQ/include/StatusQ/networkchecker.h b/ui/StatusQ/include/StatusQ/networkchecker.h index 39e4f91c3f..fb8bc24b0f 100644 --- a/ui/StatusQ/include/StatusQ/networkchecker.h +++ b/ui/StatusQ/include/StatusQ/networkchecker.h @@ -1,41 +1,51 @@ #pragma once -#include #include #include +#include #include -#include - -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); -}; \ No newline at end of file + + bool m_checking{false}; + bool checking() const; + void setChecking(bool checking); +}; diff --git a/ui/StatusQ/src/StatusQ/Components/StatusDotsLoadingIndicator.qml b/ui/StatusQ/src/StatusQ/Components/StatusDotsLoadingIndicator.qml index 0eeb14234a..cc1ca5e0dc 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusDotsLoadingIndicator.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusDotsLoadingIndicator.qml @@ -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 } } } diff --git a/ui/StatusQ/src/StatusQ/Components/StatusImage.qml b/ui/StatusQ/src/StatusQ/Components/StatusImage.qml index 608414d4ab..c2b1915c42 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusImage.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusImage.qml @@ -1,5 +1,4 @@ -import QtQuick 2.13 -import QtQuick.Window 2.15 +import QtQuick 2.15 /*! \qmltype StatusImage diff --git a/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml b/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml index 90180869ca..436144cb54 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml @@ -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 diff --git a/ui/StatusQ/src/StatusQ/Components/private/LoadingDotItem.qml b/ui/StatusQ/src/StatusQ/Components/private/LoadingDotItem.qml new file mode 100644 index 0000000000..7fc38e00ad --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Components/private/LoadingDotItem.qml @@ -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() +} diff --git a/ui/StatusQ/src/StatusQ/Components/qmldir b/ui/StatusQ/src/StatusQ/Components/qmldir index 28cd64f3bb..1aac49fb44 100644 --- a/ui/StatusQ/src/StatusQ/Components/qmldir +++ b/ui/StatusQ/src/StatusQ/Components/qmldir @@ -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 diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusItemDelegate.qml b/ui/StatusQ/src/StatusQ/Controls/StatusItemDelegate.qml index 50bf3f2402..8a942e83a6 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusItemDelegate.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusItemDelegate.qml @@ -11,6 +11,7 @@ ItemDelegate { property bool centerTextHorizontally: false property int radius: 0 property int cursorShape: Qt.PointingHandCursor + property color highlightColor: Theme.palette.statusMenu.hoverBackgroundColor padding: 8 spacing: 8 @@ -19,7 +20,7 @@ ItemDelegate { icon.height: 16 font.family: Theme.baseFont.name - font.pixelSize: 15 + font.pixelSize: Theme.primaryTextFontSize contentItem: RowLayout { spacing: root.spacing @@ -40,7 +41,7 @@ ItemDelegate { text: root.text verticalAlignment: Text.AlignVCenter elide: Text.ElideRight - color: root.enabled ? Theme.palette.directColor1 : Theme.palette.baseColor1 + color: root.highlighted ? Theme.palette.white : root.enabled ? Theme.palette.directColor1 : Theme.palette.baseColor1 Binding on horizontalAlignment { when: root.centerTextHorizontally @@ -50,16 +51,11 @@ ItemDelegate { } background: Rectangle { - color: root.highlighted - ? Theme.palette.statusMenu.hoverBackgroundColor - : "transparent" - + color: root.highlighted ? root.highlightColor : "transparent" radius: root.radius + } - MouseArea { - anchors.fill: parent - cursorShape: root.cursorShape - acceptedButtons: Qt.NoButton - } + HoverHandler { + cursorShape: root.cursorShape } } diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusPasswordStrengthIndicator.qml b/ui/StatusQ/src/StatusQ/Controls/StatusPasswordStrengthIndicator.qml index d8c58f95e6..013de9caf7 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusPasswordStrengthIndicator.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusPasswordStrengthIndicator.qml @@ -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 diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusPinInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusPinInput.qml index 2a7e16e9cc..d82e97a4ef 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusPinInput.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusPinInput.qml @@ -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 diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml index c252a2691f..6591413e90 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml @@ -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 diff --git a/ui/StatusQ/src/StatusQ/Controls/Validators/StatusIntValidator.qml b/ui/StatusQ/src/StatusQ/Controls/Validators/StatusIntValidator.qml index b3f1ba7e51..1ee9d15577 100644 --- a/ui/StatusQ/src/StatusQ/Controls/Validators/StatusIntValidator.qml +++ b/ui/StatusQ/src/StatusQ/Controls/Validators/StatusIntValidator.qml @@ -1,4 +1,4 @@ -import QtQuick 2.14 +import QtQuick 2.15 import StatusQ.Controls 0.1 diff --git a/ui/StatusQ/src/StatusQ/Core/Theme/StatusColors.qml b/ui/StatusQ/src/StatusQ/Core/Theme/StatusColors.qml index abb02f68d9..b6ee9b6d1f 100644 --- a/ui/StatusQ/src/StatusQ/Core/Theme/StatusColors.qml +++ b/ui/StatusQ/src/StatusQ/Core/Theme/StatusColors.qml @@ -86,5 +86,8 @@ QtObject { 'lightDesktopBlue10': '#ECEFFB', 'darkDesktopBlue10': '#273251', + + // new/mobile colors + 'neutral-95': '#0D1625' } } diff --git a/ui/StatusQ/src/StatusQ/Core/Theme/StatusLightTheme.qml b/ui/StatusQ/src/StatusQ/Core/Theme/StatusLightTheme.qml index 9067439989..74f143f11d 100644 --- a/ui/StatusQ/src/StatusQ/Core/Theme/StatusLightTheme.qml +++ b/ui/StatusQ/src/StatusQ/Core/Theme/StatusLightTheme.qml @@ -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') diff --git a/ui/StatusQ/src/StatusQ/Core/Theme/ThemePalette.qml b/ui/StatusQ/src/StatusQ/Core/Theme/ThemePalette.qml index 434ecea5f7..31cd629226 100644 --- a/ui/StatusQ/src/StatusQ/Core/Theme/ThemePalette.qml +++ b/ui/StatusQ/src/StatusQ/Core/Theme/ThemePalette.qml @@ -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) diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/Utils.qml b/ui/StatusQ/src/StatusQ/Core/Utils/Utils.qml index 0ecce225bb..dc90cbff69 100644 --- a/ui/StatusQ/src/StatusQ/Core/Utils/Utils.qml +++ b/ui/StatusQ/src/StatusQ/Core/Utils/Utils.qml @@ -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) + } } diff --git a/ui/StatusQ/src/StatusQ/Popups/StatusSimpleTextPopup.qml b/ui/StatusQ/src/StatusQ/Popups/StatusSimpleTextPopup.qml new file mode 100644 index 0000000000..a2a31c9c1b --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Popups/StatusSimpleTextPopup.qml @@ -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 + } + } +} diff --git a/ui/StatusQ/src/StatusQ/Popups/qmldir b/ui/StatusQ/src/StatusQ/Popups/qmldir index a5acf21cb0..db0110b4a8 100644 --- a/ui/StatusQ/src/StatusQ/Popups/qmldir +++ b/ui/StatusQ/src/StatusQ/Popups/qmldir @@ -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 diff --git a/ui/StatusQ/src/assets.qrc b/ui/StatusQ/src/assets.qrc index 5b4a8438d7..df7e418ae1 100644 --- a/ui/StatusQ/src/assets.qrc +++ b/ui/StatusQ/src/assets.qrc @@ -8340,6 +8340,35 @@ assets/png/onboarding/profile_fetching_in_progress.png assets/png/onboarding/seed-phrase.png assets/png/onboarding/welcome.png + assets/png/onboarding/status_totebag_artwork_1.png + assets/png/onboarding/status_generate_keys.png + assets/png/onboarding/status_generate_keycard.png + assets/png/onboarding/create_profile_seed.png + assets/png/onboarding/create_profile_keycard.png + assets/png/onboarding/login_syncing.png + assets/png/onboarding/status_chat.png + assets/png/onboarding/status_login_seedphrase.png + assets/png/onboarding/status_key.png + assets/png/onboarding/status_keycard.png + assets/png/onboarding/status_keycard_multiple.png + assets/png/onboarding/status_keycard_adding_keypair.png + assets/png/onboarding/status_keycard_adding_keypair_success.png + assets/png/onboarding/status_keycard_adding_keypair_failed.png + assets/png/onboarding/status_seedphrase.png + assets/png/onboarding/status_sync.png + assets/png/onboarding/status_sync_progress.png + assets/png/onboarding/status_sync_success.png + assets/png/onboarding/status_sync_failed.png + assets/png/onboarding/enable_biometrics.png + assets/png/onboarding/carousel/crypto.png + assets/png/onboarding/carousel/chat.png + assets/png/onboarding/carousel/keycard.png + assets/png/onboarding/keycard/empty.png + assets/png/onboarding/keycard/insert.png + assets/png/onboarding/keycard/invalid.png + assets/png/onboarding/keycard/reading.png + assets/png/onboarding/keycard/error.png + assets/png/onboarding/keycard/success.png assets/png/onRampProviders/latamex.png assets/png/onRampProviders/mercuryo.png assets/png/onRampProviders/moonPay.png @@ -8978,6 +9007,7 @@ assets/png/status-logo-dev-round-rect.png assets/png/status-logo-icon.png assets/png/status-logo-round-rect.png + assets/png/status-preparing.png assets/png/unfurling-image.png assets/img/icons/arrow-next.svg assets/img/icons/arrow-previous.svg diff --git a/ui/StatusQ/src/assets/png/onboarding/carousel/chat.png b/ui/StatusQ/src/assets/png/onboarding/carousel/chat.png new file mode 100644 index 0000000000..6f2f35bc69 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/carousel/chat.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/carousel/crypto.png b/ui/StatusQ/src/assets/png/onboarding/carousel/crypto.png new file mode 100644 index 0000000000..5c1e46fbb2 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/carousel/crypto.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/carousel/keycard.png b/ui/StatusQ/src/assets/png/onboarding/carousel/keycard.png new file mode 100644 index 0000000000..32854e3c88 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/carousel/keycard.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/create_profile_keycard.png b/ui/StatusQ/src/assets/png/onboarding/create_profile_keycard.png new file mode 100644 index 0000000000..993e3bacf3 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/create_profile_keycard.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/create_profile_seed.png b/ui/StatusQ/src/assets/png/onboarding/create_profile_seed.png new file mode 100644 index 0000000000..ba8cf2fc3e Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/create_profile_seed.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/enable_biometrics.png b/ui/StatusQ/src/assets/png/onboarding/enable_biometrics.png new file mode 100644 index 0000000000..6191df3b28 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/enable_biometrics.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/empty.png b/ui/StatusQ/src/assets/png/onboarding/keycard/empty.png new file mode 100644 index 0000000000..aa50cf9098 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/empty.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/error.png b/ui/StatusQ/src/assets/png/onboarding/keycard/error.png new file mode 100644 index 0000000000..245ea6610b Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/error.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/insert.png b/ui/StatusQ/src/assets/png/onboarding/keycard/insert.png new file mode 100644 index 0000000000..4ea5822092 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/insert.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/invalid.png b/ui/StatusQ/src/assets/png/onboarding/keycard/invalid.png new file mode 100644 index 0000000000..b9e1258c4c Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/invalid.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/reading.png b/ui/StatusQ/src/assets/png/onboarding/keycard/reading.png new file mode 100644 index 0000000000..afcd019ee4 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/reading.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/success.png b/ui/StatusQ/src/assets/png/onboarding/keycard/success.png new file mode 100644 index 0000000000..80f41fc28c Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/success.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/login_syncing.png b/ui/StatusQ/src/assets/png/onboarding/login_syncing.png new file mode 100644 index 0000000000..06886d5666 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/login_syncing.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_chat.png b/ui/StatusQ/src/assets/png/onboarding/status_chat.png new file mode 100644 index 0000000000..7757a05b6d Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_chat.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_generate_keycard.png b/ui/StatusQ/src/assets/png/onboarding/status_generate_keycard.png new file mode 100644 index 0000000000..6780a0f515 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_generate_keycard.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_generate_keys.png b/ui/StatusQ/src/assets/png/onboarding/status_generate_keys.png new file mode 100644 index 0000000000..4c34857933 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_generate_keys.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_key.png b/ui/StatusQ/src/assets/png/onboarding/status_key.png new file mode 100644 index 0000000000..37d2b2ac4f Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_key.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_keycard.png b/ui/StatusQ/src/assets/png/onboarding/status_keycard.png new file mode 100644 index 0000000000..68755a7c5a Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_keycard.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_keycard_adding_keypair.png b/ui/StatusQ/src/assets/png/onboarding/status_keycard_adding_keypair.png new file mode 100644 index 0000000000..afbe69da05 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_keycard_adding_keypair.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_keycard_adding_keypair_failed.png b/ui/StatusQ/src/assets/png/onboarding/status_keycard_adding_keypair_failed.png new file mode 100644 index 0000000000..0c0b704e96 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_keycard_adding_keypair_failed.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_keycard_adding_keypair_success.png b/ui/StatusQ/src/assets/png/onboarding/status_keycard_adding_keypair_success.png new file mode 100644 index 0000000000..d8302e4302 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_keycard_adding_keypair_success.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_keycard_multiple.png b/ui/StatusQ/src/assets/png/onboarding/status_keycard_multiple.png new file mode 100644 index 0000000000..87ac7d2175 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_keycard_multiple.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_login_seedphrase.png b/ui/StatusQ/src/assets/png/onboarding/status_login_seedphrase.png new file mode 100644 index 0000000000..15b1e7443b Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_login_seedphrase.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_seedphrase.png b/ui/StatusQ/src/assets/png/onboarding/status_seedphrase.png new file mode 100644 index 0000000000..c76c843d30 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_seedphrase.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_sync.png b/ui/StatusQ/src/assets/png/onboarding/status_sync.png new file mode 100644 index 0000000000..d1034b3082 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_sync.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_sync_failed.png b/ui/StatusQ/src/assets/png/onboarding/status_sync_failed.png new file mode 100644 index 0000000000..a249af252f Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_sync_failed.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_sync_progress.png b/ui/StatusQ/src/assets/png/onboarding/status_sync_progress.png new file mode 100644 index 0000000000..8c7cf058df Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_sync_progress.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_sync_success.png b/ui/StatusQ/src/assets/png/onboarding/status_sync_success.png new file mode 100644 index 0000000000..57ee6a7a08 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_sync_success.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_totebag_artwork_1.png b/ui/StatusQ/src/assets/png/onboarding/status_totebag_artwork_1.png new file mode 100644 index 0000000000..d2371dd07c Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_totebag_artwork_1.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/welcome.png b/ui/StatusQ/src/assets/png/onboarding/welcome.png index 2f37f7147a..22668b7759 100644 Binary files a/ui/StatusQ/src/assets/png/onboarding/welcome.png and b/ui/StatusQ/src/assets/png/onboarding/welcome.png differ diff --git a/ui/StatusQ/src/assets/png/status-preparing.png b/ui/StatusQ/src/assets/png/status-preparing.png new file mode 100644 index 0000000000..e715586d32 Binary files /dev/null and b/ui/StatusQ/src/assets/png/status-preparing.png differ diff --git a/ui/StatusQ/src/networkchecker.cpp b/ui/StatusQ/src/networkchecker.cpp index 204a4b1f46..0922d805ae 100644 --- a/ui/StatusQ/src/networkchecker.cpp +++ b/ui/StatusQ/src/networkchecker.cpp @@ -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(); } -} \ No newline at end of file +} + +bool NetworkChecker::checking() const +{ + return m_checking; +} + +void NetworkChecker::setChecking(bool checking) +{ + if (m_checking == checking) + return; + + m_checking = checking; + emit checkingChanged(); +} diff --git a/ui/StatusQ/src/onboarding/enums.h b/ui/StatusQ/src/onboarding/enums.h new file mode 100644 index 0000000000..ff7c1b97a0 --- /dev/null +++ b/ui/StatusQ/src/onboarding/enums.h @@ -0,0 +1,61 @@ +#include + +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) +}; diff --git a/ui/StatusQ/src/statusq.qrc b/ui/StatusQ/src/statusq.qrc index 8c776ba514..1379cbb10e 100644 --- a/ui/StatusQ/src/statusq.qrc +++ b/ui/StatusQ/src/statusq.qrc @@ -66,6 +66,7 @@ StatusQ/Components/StatusVideo.qml StatusQ/Components/StatusWizardStepper.qml StatusQ/Components/WebEngineLoader.qml + StatusQ/Components/private/LoadingDotItem.qml StatusQ/Components/private/StatusComboboxBackground.qml StatusQ/Components/private/StatusComboboxIndicator.qml StatusQ/Components/private/chart/ChartCanvas.qml @@ -246,6 +247,7 @@ StatusQ/Popups/StatusSearchLocationMenu.qml StatusQ/Popups/StatusSearchPopup.qml StatusQ/Popups/StatusSearchPopupMenuItem.qml + StatusQ/Popups/StatusSimpleTextPopup.qml StatusQ/Popups/StatusStackModal.qml StatusQ/Popups/StatusSuccessAction.qml StatusQ/Popups/qmldir diff --git a/ui/StatusQ/src/typesregistration.cpp b/ui/StatusQ/src/typesregistration.cpp index d40d10abe1..3c0506b154 100644 --- a/ui/StatusQ/src/typesregistration.cpp +++ b/ui/StatusQ/src/typesregistration.cpp @@ -35,6 +35,8 @@ #include "wallet/managetokenscontroller.h" #include "wallet/managetokensmodel.h" +#include "onboarding/enums.h" + void registerStatusQTypes() { qmlRegisterType("StatusQ", 0, 1, "StatusWindow"); qmlRegisterType("StatusQ", 0, 1, "StatusSyntaxHighlighter"); @@ -102,6 +104,10 @@ void registerStatusQTypes() { return new PermissionUtilsInternal; }); + // onboarding + qmlRegisterUncreatableType("AppLayouts.Onboarding.enums", 1, 0, + "Onboarding", "This is an enum type, cannot be created directly."); + QZXing::registerQMLTypes(); qqsfpm::registerTypes(); } diff --git a/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml b/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml index 2cad3daf76..64c5394cda 100644 --- a/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml +++ b/ui/app/AppLayouts/Onboarding/OnboardingLayout.qml @@ -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...") diff --git a/ui/app/AppLayouts/Onboarding/popups/ConfirmAddExistingKeyModal.qml b/ui/app/AppLayouts/Onboarding/popups/ConfirmAddExistingKeyModal.qml index ad9ef6ed81..302d762488 100644 --- a/ui/app/AppLayouts/Onboarding/popups/ConfirmAddExistingKeyModal.qml +++ b/ui/app/AppLayouts/Onboarding/popups/ConfirmAddExistingKeyModal.qml @@ -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() diff --git a/ui/app/AppLayouts/Onboarding/views/KeycardStateView.qml b/ui/app/AppLayouts/Onboarding/views/KeycardStateView.qml index c816d7a77d..42528f4e2c 100644 --- a/ui/app/AppLayouts/Onboarding/views/KeycardStateView.qml +++ b/ui/app/AppLayouts/Onboarding/views/KeycardStateView.qml @@ -349,7 +349,7 @@ Item { } PropertyChanges { target: button - text: qsTr("Unlock using seed phrase") + text: qsTr("Unlock using recovery phrase") type: StatusBaseButton.Type.Normal } PropertyChanges { @@ -420,19 +420,19 @@ Item { } PropertyChanges { target: title - text: qsTr("Seed phrase doesn’t match any user") + text: qsTr("Recovery phrase doesn’t match any user") color: Theme.palette.directColor1 font.pixelSize: Constants.keycard.general.fontSize1 } PropertyChanges { target: info - text: qsTr("The seed phrase you enter needs to match the seed phrase of an existing user on this device") + text: qsTr("The recovery phrase you enter needs to match the recovery phrase of an existing user on this device") color: Theme.palette.directColor1 font.pixelSize: Constants.keycard.general.fontSize2 } PropertyChanges { target: button - text: qsTr("Try entering seed phrase again") + text: qsTr("Try entering recovery phrase again") } PropertyChanges { target: link diff --git a/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml b/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml index f9c80d4006..1b55a5e2e0 100644 --- a/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml +++ b/ui/app/AppLayouts/Onboarding/views/KeysMainView.qml @@ -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 diff --git a/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml b/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml index 04c21bd7ce..e4a2f9d5b3 100644 --- a/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml +++ b/ui/app/AppLayouts/Onboarding/views/SeedPhraseInputView.qml @@ -29,10 +29,10 @@ Item { onWrongSeedPhraseChanged: { if (wrongSeedPhrase) { if (root.startupStore.startupModuleInst.flowType === Constants.startupFlow.firstRunOldUserImportSeedPhrase) { - seedPhraseView.setWrongSeedPhraseMessage(qsTr("Profile key pair for the inserted seed phrase is already set up")) + seedPhraseView.setWrongSeedPhraseMessage(qsTr("Profile key pair for the inserted recovery phrase is already set up")) return } - seedPhraseView.setWrongSeedPhraseMessage(qsTr("Seed phrase doesn’t match the profile of an existing Keycard user on this device")) + seedPhraseView.setWrongSeedPhraseMessage(qsTr("Recovery phrase doesn’t match the profile of an existing Keycard user on this device")) } else { seedPhraseView.setWrongSeedPhraseMessage("") @@ -52,7 +52,7 @@ Item { font.weight: Font.Bold color: Theme.palette.directColor1 Layout.alignment: Qt.AlignHCenter - text: qsTr("Enter seed phrase") + text: qsTr("Enter recovery phrase") } EnterSeedPhrase { diff --git a/ui/app/AppLayouts/Onboarding/views/SeedPhraseView.qml b/ui/app/AppLayouts/Onboarding/views/SeedPhraseView.qml index 2981314ca6..80ea9df816 100644 --- a/ui/app/AppLayouts/Onboarding/views/SeedPhraseView.qml +++ b/ui/app/AppLayouts/Onboarding/views/SeedPhraseView.qml @@ -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 { diff --git a/ui/app/AppLayouts/Onboarding/views/SeedPhraseWordsInputView.qml b/ui/app/AppLayouts/Onboarding/views/SeedPhraseWordsInputView.qml index c82fcc4fef..024cd20c97 100644 --- a/ui/app/AppLayouts/Onboarding/views/SeedPhraseWordsInputView.qml +++ b/ui/app/AppLayouts/Onboarding/views/SeedPhraseWordsInputView.qml @@ -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 { diff --git a/ui/app/AppLayouts/Onboarding/views/SyncCodeView.qml b/ui/app/AppLayouts/Onboarding/views/SyncCodeView.qml index ebe2c3f45d..c052fa4e39 100644 --- a/ui/app/AppLayouts/Onboarding/views/SyncCodeView.qml +++ b/ui/app/AppLayouts/Onboarding/views/SyncCodeView.qml @@ -1,4 +1,4 @@ -import QtQuick 2.13 +import QtQuick 2.15 import shared.popups 1.0 import shared.views 1.0 diff --git a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml new file mode 100644 index 0000000000..0af9e806f5 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml @@ -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 : "" + 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 + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml b/ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml new file mode 100644 index 0000000000..30f044b24f --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml @@ -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 + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/components/SeedphraseVerifyInput.qml b/ui/app/AppLayouts/Onboarding2/components/SeedphraseVerifyInput.qml new file mode 100644 index 0000000000..f74a409fa8 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/components/SeedphraseVerifyInput.qml @@ -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 + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/components/StepIndicator.qml b/ui/app/AppLayouts/Onboarding2/components/StepIndicator.qml new file mode 100644 index 0000000000..818f4d60d8 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/components/StepIndicator.qml @@ -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 + } +} diff --git a/ui/app/AppLayouts/Onboarding2/components/qmldir b/ui/app/AppLayouts/Onboarding2/components/qmldir new file mode 100644 index 0000000000..db34fbf6bf --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/components/qmldir @@ -0,0 +1,3 @@ +NewsCarousel 1.0 NewsCarousel.qml +SeedphraseVerifyInput 1.0 SeedphraseVerifyInput.qml +StepIndicator 1.0 StepIndicator.qml diff --git a/ui/app/AppLayouts/Onboarding2/controls/BulletPoint.qml b/ui/app/AppLayouts/Onboarding2/controls/BulletPoint.qml new file mode 100644 index 0000000000..e615995f01 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/controls/BulletPoint.qml @@ -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 + } +} diff --git a/ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml b/ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml new file mode 100644 index 0000000000..1127b5d480 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml @@ -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 + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/controls/MaybeOutlineButton.qml b/ui/app/AppLayouts/Onboarding2/controls/MaybeOutlineButton.qml new file mode 100644 index 0000000000..4527718177 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/controls/MaybeOutlineButton.qml @@ -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 + } +} diff --git a/ui/app/AppLayouts/Onboarding2/controls/OnboardingButtonFrame.qml b/ui/app/AppLayouts/Onboarding2/controls/OnboardingButtonFrame.qml new file mode 100644 index 0000000000..b91579b55f --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/controls/OnboardingButtonFrame.qml @@ -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 + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml b/ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml new file mode 100644 index 0000000000..012c7905a6 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml @@ -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 + } +} diff --git a/ui/app/AppLayouts/Onboarding2/controls/qmldir b/ui/app/AppLayouts/Onboarding2/controls/qmldir new file mode 100644 index 0000000000..1c48f65534 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/controls/qmldir @@ -0,0 +1,5 @@ +BulletPoint 1.0 BulletPoint.qml +OnboardingButtonFrame 1.0 OnboardingButtonFrame.qml +OnboardingFrame 1.0 OnboardingFrame.qml +ListItemButton 1.0 ListItemButton.qml +MaybeOutlineButton 1.0 MaybeOutlineButton.qml diff --git a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseAcks.qml b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseAcks.qml new file mode 100644 index 0000000000..afe4c2d5e3 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseAcks.qml @@ -0,0 +1,99 @@ +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 + +OnboardingPage { + id: root + + signal backupSeedphraseContinue() + + pageClassName: "BackupSeedphraseAcks" + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(440, root.availableWidth) + spacing: Theme.xlPadding + + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Backup your recovery phrase") + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + Frame { + Layout.fillWidth: true + padding: 12 + background: Rectangle { + color: Theme.palette.dangerColor3 + radius: Theme.radius + } + contentItem: StatusBaseText { + text: qsTr("Store your recovery phrase in a secure location so you never lose access to your funds.") + color: Theme.palette.dangerColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Qt.AlignHCenter + lineHeightMode: Text.FixedHeight + lineHeight: 22 + } + } + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.weight: Font.DemiBold + text: qsTr("Backup checklist:") + } + Frame { + Layout.fillWidth: true + Layout.topMargin: -20 + padding: 20 + background: Rectangle { + color: "transparent" + radius: 12 + border.width: 1 + border.color: Theme.palette.baseColor2 + } + contentItem: ColumnLayout { + StatusCheckBox { + objectName: "ack1" + Layout.fillWidth: true + id: ack1 + text: qsTr("I have a pen and paper") + } + StatusCheckBox { + objectName: "ack2" + Layout.fillWidth: true + id: ack2 + text: qsTr("I am ready to write down my recovery phrase") + } + StatusCheckBox { + objectName: "ack3" + Layout.fillWidth: true + id: ack3 + text: qsTr("I know where I’ll store it") + } + StatusCheckBox { + objectName: "ack4" + Layout.fillWidth: true + id: ack4 + text: qsTr("I know I can only reveal it once") + } + } + } + StatusButton { + objectName: "btnContinue" + Layout.alignment: Qt.AlignHCenter + text: qsTr("Continue") + enabled: ack1.checked && ack2.checked && ack3.checked && ack4.checked + onClicked: root.backupSeedphraseContinue() + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseIntro.qml b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseIntro.qml new file mode 100644 index 0000000000..7964175668 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseIntro.qml @@ -0,0 +1,55 @@ +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 + +OnboardingPage { + id: root + + signal backupSeedphraseRequested() + + pageClassName: "BackupSeedphraseIntro" + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(400, root.availableWidth) + spacing: 20 + + StatusImage { + id: image + Layout.preferredWidth: 296 + Layout.preferredHeight: 260 + Layout.alignment: Qt.AlignHCenter + mipmap: true + source: Theme.png("onboarding/status_seedphrase") + } + + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Backup your recovery phrase") + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + text: qsTr("Your recovery phrase is a 12 word passcode to your funds that cannot be recovered if lost. Write it down offline and store it somewhere secure.") + } + StatusButton { + objectName: "btnBackupSeedphrase" + Layout.alignment: Qt.AlignHCenter + text: qsTr("Backup recovery phrase") + onClicked: root.backupSeedphraseRequested() + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseOutro.qml b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseOutro.qml new file mode 100644 index 0000000000..a9f98225aa --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseOutro.qml @@ -0,0 +1,67 @@ +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 AppLayouts.Onboarding2.components 1.0 + +OnboardingPage { + id: root + + signal backupSeedphraseRemovalConfirmed() + + pageClassName: "BackupSeedphraseOutro" + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(440, root.availableWidth) + spacing: Theme.xlPadding + + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Backup your recovery phrase") + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StepIndicator { + Layout.fillWidth: true + spacing: Theme.halfPadding + currentStep: 3 + totalSteps: 3 + caption: qsTr("Store your phrase offline") + } + + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + text: qsTr("Ensure you have written down your recovery phrase and have a safe place to keep it. Remember, anyone who has your recovery phrase has access to your funds.") + } + + Item { Layout.preferredHeight: 120 } + + StatusCheckBox { + objectName: "cbAck" + Layout.fillWidth: true + id: cbAck + text: qsTr("I understand my recovery phrase will now be removed and I will no longer be able to access it via Status") + } + + StatusButton { + objectName: "btnContinue" + Layout.alignment: Qt.AlignHCenter + text: qsTr("Continue") + enabled: cbAck.checked + onClicked: root.backupSeedphraseRemovalConfirmed() + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseReveal.qml b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseReveal.qml new file mode 100644 index 0000000000..80fdde8d61 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseReveal.qml @@ -0,0 +1,136 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.components 1.0 + +OnboardingPage { + id: root + + required property var seedWords + + signal backupSeedphraseConfirmed() + + pageClassName: "BackupSeedphraseReveal" + + QtObject { + id: d + property bool seedphraseRevealed + } + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(440, root.availableWidth) + spacing: Theme.xlPadding + + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Backup your recovery phrase") + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StepIndicator { + Layout.fillWidth: true + spacing: Theme.halfPadding + currentStep: 1 + totalSteps: 3 + caption: qsTr("Write down your 12-word recovery phrase to keep offline") + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: seedGrid.height + + GridLayout { + objectName: "seedGrid" + id: seedGrid + width: parent.width + columns: 2 + columnSpacing: Theme.halfPadding + rowSpacing: columnSpacing + + Repeater { + model: root.seedWords + delegate: Frame { + Layout.fillWidth: true + Layout.fillHeight: true + padding: Theme.smallPadding + background: Rectangle { + radius: Theme.radius + color: "transparent" + border.width: 1 + border.color: Theme.palette.baseColor2 + } + contentItem: RowLayout { + spacing: Theme.halfPadding + StatusBaseText { + Layout.preferredWidth: idxMetrics.advanceWidth + horizontalAlignment: Qt.AlignHCenter + text: index + 1 + color: Theme.palette.baseColor1 + font: idxMetrics.font + } + StatusBaseText { + Layout.fillWidth: true + text: modelData + } + } + } + } + layer.enabled: !d.seedphraseRevealed + layer.effect: GaussianBlur { + radius: 16 + samples: 33 + transparentBorder: true + } + } + + StatusButton { + objectName: "btnReveal" + anchors.centerIn: parent + text: qsTr("Reveal recovery phrase") + icon.name: "show" + type: StatusBaseButton.Type.Primary + visible: !d.seedphraseRevealed + onClicked: d.seedphraseRevealed = true + } + } + + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Anyone who sees this will have access to your funds.") + color: Theme.palette.dangerColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Qt.AlignHCenter + } + + StatusButton { + objectName: "btnConfirm" + Layout.alignment: Qt.AlignHCenter + text: qsTr("Confirm recovery phrase") + enabled: d.seedphraseRevealed + onClicked: { + root.backupSeedphraseConfirmed() + d.seedphraseRevealed = false + } + } + } + } + + TextMetrics { + id: idxMetrics + font.family: Theme.monoFont.name + font.pixelSize: Theme.primaryTextFontSize + text: "99" + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseVerify.qml b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseVerify.qml new file mode 100644 index 0000000000..f3b32ad78a --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseVerify.qml @@ -0,0 +1,117 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.components 1.0 + +import shared.stores 1.0 + +import SortFilterProxyModel 0.2 + +OnboardingPage { + id: root + + required property var seedWordsToVerify // [{seedWordNumber:int, seedWord:string}, ...] + + signal backupSeedphraseVerified() + + pageClassName: "BackupSeedphraseVerify" + + QtObject { + id: d + readonly property var seedSuggestions: BIP39_en {} // [{seedWord:string}, ...] + } + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(440, root.availableWidth) + spacing: Theme.xlPadding + + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Backup your recovery phrase") + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StepIndicator { + Layout.fillWidth: true + spacing: Theme.halfPadding + currentStep: 2 + totalSteps: 3 + caption: qsTr("Confirm the following words from your recovery phrase...") + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Theme.halfPadding + Repeater { + readonly property bool allValid: { + for (let i = 0; i < count; i++) { + if (!!itemAt(i) && !itemAt(i).valid) + return false + } + return true + } + + id: seedRepeater + model: root.seedWordsToVerify + delegate: RowLayout { + id: seedWordDelegate + + required property var modelData + required property int index + + readonly property bool valid: seedInput.valid + readonly property alias input: seedInput + + Layout.fillWidth: true + Layout.topMargin: Theme.halfPadding + Layout.bottomMargin: Theme.halfPadding + spacing: 12 + StatusBaseText { + Layout.preferredWidth: 20 + text: modelData.seedWordNumber + } + SeedphraseVerifyInput { + readonly property int seedWordIndex: modelData.seedWordNumber - 1 // 0 based idx into the seedWords + objectName: "seedInput_%1".arg(index) + Layout.fillWidth: true + id: seedInput + valid: text === modelData.seedWord + seedSuggestions: d.seedSuggestions + Component.onCompleted: if (index === 0) forceActiveFocus() + onAccepted: { + if (seedRepeater.allValid) { // move to next page + root.backupSeedphraseVerified() + } else { // move to next field + const nextItem = seedRepeater.itemAt(index + 1) ?? seedRepeater.itemAt(0) + if (!!nextItem) { + nextItem.input.forceActiveFocus() + } + } + } + } + } + } + } + + StatusButton { + objectName: "btnContinue" + Layout.alignment: Qt.AlignHCenter + text: qsTr("Continue") + enabled: seedRepeater.allValid + onClicked: root.backupSeedphraseVerified() + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/CreateKeycardProfilePage.qml b/ui/app/AppLayouts/Onboarding2/pages/CreateKeycardProfilePage.qml new file mode 100644 index 0000000000..58cc9268cb --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/CreateKeycardProfilePage.qml @@ -0,0 +1,106 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +OnboardingPage { + id: root + + title: qsTr("Create profile on empty Keycard") + + signal createKeycardProfileWithNewSeedphrase() + signal createKeycardProfileWithExistingSeedphrase() + + pageClassName: "CreateKeycardProfilePage" + + contentItem: Item { + ColumnLayout { + width: parent.width + anchors.centerIn: parent + + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("You will require your Keycard to log in to Status and sign transactions") + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + ColumnLayout { + Layout.maximumWidth: Math.min(380, root.availableWidth) + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 56 + spacing: Theme.bigPadding + + OnboardingFrame { + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 24 + StatusImage { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(252, parent.width) + Layout.preferredHeight: Math.min(164, height) + source: Theme.png("onboarding/status_generate_keycard") + mipmap: true + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Use a new recovery phrase") + font.pixelSize: Theme.secondaryAdditionalTextSize + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -Theme.padding + text: qsTr("To create your Keycard-stored profile ") + font.pixelSize: Theme.additionalTextSize + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: Theme.palette.baseColor1 + } + StatusButton { + objectName: "btnCreateWithEmptySeedphrase" + Layout.fillWidth: true + text: qsTr("Let’s go!") + font.pixelSize: Theme.additionalTextSize + onClicked: root.createKeycardProfileWithNewSeedphrase() + } + } + } + + OnboardingButtonFrame { + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 0 + ListItemButton { + objectName: "btnCreateWithExistingSeedphrase" + Layout.fillWidth: true + text: qsTr("Use an existing recovery phrase") + subTitle: qsTr("To create your Keycard-stored profile ") + icon.source: Theme.png("onboarding/create_profile_seed") + onClicked: root.createKeycardProfileWithExistingSeedphrase() + } + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml b/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml new file mode 100644 index 0000000000..a5298889a1 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml @@ -0,0 +1,92 @@ +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.Core.Theme 0.1 +import StatusQ.Popups 0.1 + +import utils 1.0 +import shared.views 1.0 + +OnboardingPage { + id: root + + property var passwordStrengthScoreFunction: (password) => { console.error("passwordStrengthScoreFunction: IMPLEMENT ME") } + + signal setPasswordRequested(string password) + + title: qsTr("Create profile password") + + pageClassName: "CreatePasswordPage" + + QtObject { + id: d + + function submit() { + if (!passView.ready) + return + root.setPasswordRequested(passView.newPswText) + } + } + + Component.onCompleted: passView.forceNewPswInputFocus() + + contentItem: Item { + ColumnLayout { + spacing: Theme.padding + anchors.centerIn: parent + width: Math.min(400, root.availableWidth) + + PasswordView { + id: passView + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + highSizeIntro: true + title: root.title + introText: qsTr("This password can’t be recovered") + recoverText: "" + passwordStrengthScoreFunction: root.passwordStrengthScoreFunction + onReturnPressed: d.submit() + } + StatusButton { + objectName: "btnConfirmPassword" + Layout.alignment: Qt.AlignHCenter + text: qsTr("Confirm password") + enabled: passView.ready + onClicked: d.submit() + } + } + } + + StatusButton { + objectName: "infoButton" + width: 32 + height: 32 + icon.width: 20 + icon.height: 20 + icon.color: Theme.palette.directColor1 + normalColor: Theme.palette.baseColor2 + padding: 0 + anchors.right: parent.right + anchors.top: parent.top + icon.name: "info" + onClicked: passwordDetailsPopup.createObject(root).open() + } + + Component { + id: passwordDetailsPopup + StatusSimpleTextPopup { + objectName: "passwordDetailsPopup" + title: qsTr("Create profile password") + width: 480 + destroyOnClose: true + content.text: qsTr("Your Status keys are the foundation of your self-sovereign identity in Web3. You have complete control over these keys, which you can use to sign transactions, access your data, and interact with Web3 services. + +Your keys are always securely stored on your device and protected by your Status profile password. Status doesn't know your password and can't reset it for you. If you forget your password, you may lose access to your Status profile and wallet funds. + +Remember your password and don't share it with anyone.") + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml b/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml new file mode 100644 index 0000000000..bc45538537 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml @@ -0,0 +1,119 @@ +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 StatusQ.Popups 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +import utils 1.0 + +OnboardingPage { + id: root + + title: qsTr("Create your profile") + + signal createProfileWithPasswordRequested() + signal createProfileWithSeedphraseRequested() + signal createProfileWithEmptyKeycardRequested() + + pageClassName: "CreateProfilePage" + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(380, root.availableWidth) + spacing: Theme.bigPadding + + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -Theme.padding + text: qsTr("How would you like to start using Status?") + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + OnboardingFrame { + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 20 + StatusImage { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(164, parent.width) + Layout.preferredHeight: Math.min(164, height) + source: Theme.png("onboarding/status_key") + mipmap: true + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Start fresh") + font.pixelSize: Theme.secondaryAdditionalTextSize + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -Theme.padding + text: qsTr("Create a new profile from scratch") + font.pixelSize: Theme.additionalTextSize + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: Theme.palette.baseColor1 + } + StatusButton { + objectName: "btnCreateWithPassword" + Layout.fillWidth: true + text: qsTr("Let’s go!") + font.pixelSize: Theme.additionalTextSize + onClicked: root.createProfileWithPasswordRequested() + } + } + } + + OnboardingButtonFrame { + Layout.fillWidth: true + id: buttonFrame + contentItem: ColumnLayout { + spacing: 0 + ListItemButton { + objectName: "btnCreateWithSeedPhrase" + Layout.fillWidth: true + text: qsTr("Use a recovery phrase") + subTitle: qsTr("If you already have an Ethereum wallet") + icon.source: Theme.png("onboarding/create_profile_seed") + onClicked: root.createProfileWithSeedphraseRequested() + } + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: -buttonFrame.padding + Layout.rightMargin: -buttonFrame.padding + Layout.preferredHeight: 1 + color: Theme.palette.statusMenu.separatorColor + } + ListItemButton { + objectName: "btnCreateWithEmptyKeycard" + Layout.fillWidth: true + text: qsTr("Use an empty Keycard") + subTitle: qsTr("Store your new profile keys on Keycard") + icon.source: Theme.png("onboarding/create_profile_keycard") + onClicked: root.createProfileWithEmptyKeycardRequested() + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml b/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml new file mode 100644 index 0000000000..ad5b821eb7 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml @@ -0,0 +1,70 @@ +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.Components 0.1 +import StatusQ.Core.Theme 0.1 + +OnboardingPage { + id: root + + title: qsTr("Enable biometrics") + + property string subtitle: qsTr("Would you like to enable biometrics to fill in your password? You will use biometrics for signing in to Status and for signing transactions.") + + signal enableBiometricsRequested(bool enable) + + pageClassName: "EnableBiometricsPage" + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + spacing: Theme.bigPadding + width: Math.min(400, root.availableWidth) + + StatusImage { + Layout.preferredWidth: 270 + Layout.preferredHeight: 260 + Layout.alignment: Qt.AlignHCenter + mipmap: true + smooth: false + source: Theme.png("onboarding/enable_biometrics") + } + + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -12 + text: root.subtitle + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StatusButton { + objectName: "btnEnableBiometrics" + Layout.topMargin: Theme.halfPadding + Layout.alignment: Qt.AlignHCenter + text: qsTr("Yes, use biometrics") + onClicked: root.enableBiometricsRequested(true) + } + + StatusFlatButton { + objectName: "btnDontEnableBiometrics" + Layout.topMargin: -Theme.halfPadding + Layout.alignment: Qt.AlignHCenter + text: qsTr("Maybe later") + onClicked: root.enableBiometricsRequested(false) + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/HelpUsImproveStatusPage.qml b/ui/app/AppLayouts/Onboarding2/pages/HelpUsImproveStatusPage.qml new file mode 100644 index 0000000000..7ed2d51906 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/HelpUsImproveStatusPage.qml @@ -0,0 +1,152 @@ +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 StatusQ.Popups.Dialog 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +import utils 1.0 + +OnboardingPage { + id: root + + title: qsTr("Help us improve Status") + + signal shareUsageDataRequested(bool enabled) + signal privacyPolicyRequested() + + pageClassName: "HelpUsImproveStatusPage" + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(400, root.availableWidth) + spacing: root.padding + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Your usage data helps us make Status better") + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StatusImage { + Layout.preferredWidth: 320 + Layout.preferredHeight: 354 + Layout.topMargin: Theme.bigPadding + Layout.bottomMargin: Theme.bigPadding + Layout.alignment: Qt.AlignHCenter + source: Theme.png("onboarding/status_totebag_artwork_1") + } + + StatusButton { + objectName: "btnShare" + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 320 + text: qsTr("Share usage data") + onClicked: root.shareUsageDataRequested(true) + } + StatusButton { + objectName: "btnDontShare" + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 320 + text: qsTr("Not now") + normalColor: "transparent" + borderWidth: 1 + borderColor: Theme.palette.baseColor2 + onClicked: root.shareUsageDataRequested(false) + } + } + } + + StatusButton { + objectName: "infoButton" + width: 32 + height: 32 + icon.width: 20 + icon.height: 20 + icon.color: Theme.palette.directColor1 + normalColor: Theme.palette.baseColor2 + padding: 0 + anchors.right: parent.right + anchors.top: parent.top + icon.name: "info" + onClicked: helpUsImproveDetails.createObject(root).open() + } + + Component { + id: helpUsImproveDetails + StatusDialog { + objectName: "helpUsImproveDetailsPopup" + title: qsTr("Help us improve Status") + width: 480 + standardButtons: Dialog.Ok + padding: 20 + destroyOnClose: true + contentItem: ColumnLayout { + spacing: 20 + StatusBaseText { + Layout.fillWidth: true + text: qsTr("We’ll collect anonymous analytics and diagnostics from your app to enhance Status’s quality and performance.") + wrapMode: Text.WordWrap + } + OnboardingFrame { + Layout.fillWidth: true + dropShadow: false + contentItem: ColumnLayout { + spacing: 12 + BulletPoint { + text: qsTr("Gather basic usage data, like clicks and page views") + checked: true + } + BulletPoint { + text: qsTr("Gather core diagnostics, like bandwidth usage") + checked: true + } + BulletPoint { + text: qsTr("Never collect your profile information or wallet address") + } + BulletPoint { + text: qsTr("Never collect information you input or send") + } + BulletPoint { + text: qsTr("Never sell your usage analytics data") + } + } + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("For more details and other cases where we handle your data, refer to our %1.") + .arg(Utils.getStyledLink(qsTr("Privacy Policy"), "#privacy", hoveredLink, Theme.palette.primaryColor1, Theme.palette.primaryColor1, false)) + color: Theme.palette.baseColor1 + font.pixelSize: Theme.additionalTextSize + wrapMode: Text.WordWrap + textFormat: Text.RichText + onLinkActivated: { + if (link == "#privacy") { + close() + root.privacyPolicyRequested() + } + } + HoverHandler { + // Qt CSS doesn't support custom cursor shape + cursorShape: !!parent.hoveredLink ? Qt.PointingHandCursor : undefined + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardAddKeyPairPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardAddKeyPairPage.qml new file mode 100644 index 0000000000..f123b7ef01 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardAddKeyPairPage.qml @@ -0,0 +1,219 @@ +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 AppLayouts.Onboarding2.controls 1.0 +import AppLayouts.Onboarding.enums 1.0 + +OnboardingPage { + id: root + + required property int addKeyPairState // Onboarding.AddKeyPairState.xxx + property int timeoutInterval: 30000 + + signal keypairAddContinueRequested() + signal keypairAddTryAgainRequested() + signal reloadKeycardRequested() + signal createProfilePageRequested() + + pageClassName: "KeycardAddKeyPairPage" + + Timer { + id: timer + interval: root.timeoutInterval + running: root.addKeyPairState === Onboarding.AddKeyPairState.InProgress + onTriggered: root.addKeyPairState = Onboarding.AddKeyPairState.Failed + } + + states: [ + State { + name: "inprogress" + when: root.addKeyPairState === Onboarding.AddKeyPairState.InProgress + PropertyChanges { + target: root + title: qsTr("Adding key pair to Keycard") + } + PropertyChanges { + target: iconLoader + sourceComponent: loadingIndicator + } + PropertyChanges { + target: subImageText + text: qsTr("Please keep the Keycard plugged in until the migration is complete") + visible: true + } + PropertyChanges { + target: image + source: Theme.png("onboarding/status_keycard_adding_keypair") + } + }, + State { + name: "success" + when: root.addKeyPairState === Onboarding.AddKeyPairState.Success + PropertyChanges { + target: root + title: qsTr("Key pair added to Keycard") + } + PropertyChanges { + target: subtitle + text: qsTr("You will now require this Keycard to log into Status and transact with any accounts derived from this key pair") + } + PropertyChanges { + target: iconLoader + sourceComponent: successIcon + } + PropertyChanges { + target: continueButton + visible: true + } + PropertyChanges { + target: image + source: Theme.png("onboarding/status_keycard_adding_keypair_success") + } + }, + State { + name: "failed" + when: root.addKeyPairState === Onboarding.AddKeyPairState.Failed + PropertyChanges { + target: root + title: "".arg(Theme.palette.dangerColor1) + qsTr("Failed to add key pair to Keycard") + "" + } + PropertyChanges { + target: subtitle + text: qsTr("Something went wrong...") + } + PropertyChanges { + target: iconLoader + sourceComponent: failedIcon + } + PropertyChanges { + target: buttonColumn + visible: true + } + PropertyChanges { + target: image + source: Theme.png("onboarding/status_keycard_adding_keypair_failed") + } + } + ] + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(350, root.availableWidth) + spacing: Theme.halfPadding + + Loader { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + Layout.alignment: Qt.AlignHCenter + id: iconLoader + } + + StatusBaseText { + Layout.fillWidth: true + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + text: root.title + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + id: subtitle + Layout.fillWidth: true + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + visible: !!text + } + + StatusImage { + id: image + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(231, parent.width) + Layout.preferredHeight: Math.min(211, height) + Layout.topMargin: Theme.bigPadding + Layout.bottomMargin: Theme.bigPadding + source: Theme.png("onboarding/status_keycard_adding_keypair") + mipmap: true + } + + StatusBaseText { + id: subImageText + Layout.fillWidth: true + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + visible: false + } + + StatusButton { + objectName: "btnContinue" + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 240 + id: continueButton + text: qsTr("Continue") + visible: false + onClicked: root.keypairAddContinueRequested() + } + + Column { + id: buttonColumn + Layout.preferredWidth: 280 + Layout.alignment: Qt.AlignHCenter + spacing: 12 + visible: false + + MaybeOutlineButton { + width: parent.width + text: qsTr("Try again") + onClicked: root.keypairAddTryAgainRequested() + } + MaybeOutlineButton { + text: qsTr("I’ve inserted a different Keycard") + width: parent.width + onClicked: root.reloadKeycardRequested() + } + MaybeOutlineButton { + text: qsTr("Create profile without Keycard") + width: parent.width + onClicked: root.createProfilePageRequested() + } + } + } + } + + Component { + id: loadingIndicator + Rectangle { + color: Theme.palette.baseColor2 + radius: width/2 + StatusDotsLoadingIndicator { + anchors.centerIn: parent + } + } + } + + Component { + id: successIcon + StatusRoundIcon { + asset.name: "check-circle" + asset.color: Theme.palette.successColor1 + asset.bgColor: Theme.palette.successColor2 + } + } + + Component { + id: failedIcon + StatusRoundIcon { + asset.name: "close-circle" + asset.color: Theme.palette.dangerColor1 + asset.bgColor: Theme.palette.dangerColor3 + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardBasePage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardBasePage.qml new file mode 100644 index 0000000000..2ffd2b72b0 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardBasePage.qml @@ -0,0 +1,74 @@ +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 StatusQ.Core.Utils 0.1 as SQUtils + +OnboardingPage { + id: root + + property string subtitle + property alias image: image + property alias infoText: infoText + property alias buttons: buttonsWrapper.children + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(400, root.availableWidth) + spacing: 20 + + StatusImage { + id: image + Layout.preferredWidth: 280 + Layout.preferredHeight: 280 + Layout.alignment: Qt.AlignHCenter + mipmap: true + } + + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + text: root.subtitle + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + visible: !!text + } + StatusBaseText { + Layout.fillWidth: true + id: infoText + textFormat: Text.RichText + font.pixelSize: Theme.tertiaryTextFontSize + wrapMode: Text.WordWrap + color: Theme.palette.baseColor1 + horizontalAlignment: Text.AlignHCenter + visible: !!text + onLinkActivated: openLinkWithConfirmation(link, SQUtils.StringUtils.extractDomainFromLink(link)) + + HoverHandler { + // Qt CSS doesn't support custom cursor shape + cursorShape: !!parent.hoveredLink ? Qt.PointingHandCursor : undefined + } + } + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 4 + id: buttonsWrapper + spacing: 12 + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardCreatePinPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardCreatePinPage.qml new file mode 100644 index 0000000000..acc9b5f670 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardCreatePinPage.qml @@ -0,0 +1,124 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQml 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +import utils 1.0 + +KeycardBasePage { + id: root + + signal keycardPinCreated(string pin) + + pageClassName: "KeycardCreatePinPage" + image.source: Theme.png("onboarding/keycard/reading") + + QtObject { + id: d + property string pin + property string pin2 + + function setPins() { + if (pinInput.valid) { + if (root.state === "creating") + d.pin = pinInput.pinInput + else if (root.state === "repeating" || root.state === "mismatch") + d.pin2 = pinInput.pinInput + + if (root.state === "mismatch") + pinInput.statesInitialization() + } + } + } + + buttons: [ + StatusPinInput { + id: pinInput + anchors.horizontalCenter: parent.horizontalCenter + validator: StatusIntValidator { bottom: 0; top: 999999 } + Component.onCompleted: { + statesInitialization() + forceFocus() + } + onPinInputChanged: { + Qt.callLater(d.setPins) + } + }, + StatusBaseText { + id: errorText + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("PINs don’t match") + font.pixelSize: Theme.tertiaryTextFontSize + color: Theme.palette.dangerColor1 + visible: false + } + ] + + state: "creating" + + states: [ + State { + name: "creating" + PropertyChanges { + target: root + title: qsTr("Create new Keycard PIN") + } + }, + State { + name: "mismatch" + extend: "repeating" + when: !!d.pin && !!d.pin2 && d.pin !== d.pin2 + PropertyChanges { + target: errorText + visible: true + } + PropertyChanges { + target: root + image.source: Theme.png("onboarding/keycard/error") + } + }, + State { + name: "success" + extend: "repeating" + when: !!d.pin && !!d.pin2 && d.pin === d.pin2 + PropertyChanges { + target: root + title: qsTr("Keycard PIN set") + } + PropertyChanges { + target: pinInput + enabled: false + } + PropertyChanges { + target: root + image.source: Theme.png("onboarding/keycard/success") + } + StateChangeScript { + script: { + pinInput.setPin(d.pin) + root.keycardPinCreated(d.pin) + } + } + }, + State { + name: "repeating" + when: d.pin !== "" + PropertyChanges { + target: root + title: qsTr("Repeat Keycard PIN") + } + StateChangeScript { + script: { + pinInput.statesInitialization() + } + } + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardEmptyPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardEmptyPage.qml new file mode 100644 index 0000000000..4047c79712 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardEmptyPage.qml @@ -0,0 +1,31 @@ +import QtQuick 2.15 + +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +KeycardBasePage { + id: root + + signal createProfileWithEmptyKeycardRequested() + signal reloadKeycardRequested() + + title: qsTr("Keycard is empty") + subtitle: qsTr("There is no profile key pair on this Keycard") + image.source: Theme.png("onboarding/keycard/error") + + pageClassName: "KeycardEmptyPage" + + buttons: [ + MaybeOutlineButton { + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("Create new profile on this Keycard") + onClicked: root.createProfileWithEmptyKeycardRequested() + }, + MaybeOutlineButton { + text: qsTr("I’ve inserted a different Keycard") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.reloadKeycardRequested() + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml new file mode 100644 index 0000000000..6ee8eabc73 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml @@ -0,0 +1,160 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +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 + +import utils 1.0 + +KeycardBasePage { + id: root + + property var tryToSetPinFunction: (pin) => { console.error("tryToSetPinFunction: IMPLEMENT ME"); return false } + required property int remainingAttempts + + signal keycardPinEntered(string pin) + signal reloadKeycardRequested() + signal keycardFactoryResetRequested() + + pageClassName: "KeycardEnterPinPage" + image.source: Theme.png("onboarding/keycard/reading") + + QtObject { + id: d + property string tempPin + property bool pinValid + } + + buttons: [ + StatusPinInput { + id: pinInput + anchors.horizontalCenter: parent.horizontalCenter + validator: StatusIntValidator { bottom: 0; top: 999999 } + onPinInputChanged: { + if (pinInput.pinInput.length === pinInput.pinLen) { // we have the full length PIN now + d.tempPin = pinInput.pinInput + d.pinValid = root.tryToSetPinFunction(d.tempPin) + if (!d.pinValid) { + pinInput.statesInitialization() + } + } + } + }, + StatusBaseText { + id: errorText + anchors.horizontalCenter: parent.horizontalCenter + 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() + }, + StatusButton { + id: btnReload + width: 320 + anchors.horizontalCenter: parent.horizontalCenter + visible: false + text: qsTr("I’ve inserted a Keycard") + normalColor: "transparent" + borderWidth: 1 + borderColor: Theme.palette.baseColor2 + onClicked: root.reloadKeycardRequested() + } + ] + + state: "entering" + + states: [ + State { + name: "locked" + when: root.remainingAttempts <= 0 + PropertyChanges { + target: root + title: "".arg(Theme.palette.dangerColor1) + qsTr("Keycard locked") + "" + } + PropertyChanges { + target: pinInput + enabled: false + } + PropertyChanges { + target: image + source: Theme.png("onboarding/keycard/error") + } + PropertyChanges { + target: btnFactoryReset + visible: true + } + PropertyChanges { + target: btnReload + visible: true + } + StateChangeScript { + script: { + Backpressure.debounce(root, 100, function() { + pinInput.clearPin() + })() + } + } + }, + State { + name: "incorrect" + when: !!d.tempPin && !d.pinValid + PropertyChanges { + target: root + title: qsTr("PIN incorrect") + } + PropertyChanges { + target: errorText + visible: true + } + }, + State { + name: "success" + when: d.pinValid + PropertyChanges { + target: root + title: qsTr("PIN correct") + } + PropertyChanges { + target: pinInput + enabled: false + } + StateChangeScript { + script: { + Backpressure.debounce(root, 2000, function() { + root.keycardPinEntered(pinInput.pinInput) + })() + } + } + }, + State { + name: "entering" + PropertyChanges { + target: root + title: qsTr("Enter Keycard PIN") + } + StateChangeScript { + script: { + pinInput.statesInitialization() + pinInput.forceFocus() + d.tempPin = "" + d.pinValid = false + } + } + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml new file mode 100644 index 0000000000..e61866a718 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml @@ -0,0 +1,221 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQml 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.controls 1.0 +import AppLayouts.Onboarding.enums 1.0 + +import utils 1.0 + +KeycardBasePage { + id: root + + required property int keycardState // cf Onboarding.KeycardState + property bool displayPromoBanner + + signal keycardFactoryResetRequested() + signal reloadKeycardRequested() + signal emptyKeycardDetected() + signal notEmptyKeycardDetected() + + pageClassName: "KeycardIntroPage" + + OnboardingFrame { + id: promoBanner + visible: false + dropShadow: false + cornerRadius: 12 + width: 600 + leftPadding: 0 + rightPadding: 20 + topPadding: Theme.halfPadding + bottomPadding: 0 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.bigPadding + + contentItem: RowLayout { + spacing: 0 + StatusImage { + Layout.bottomMargin: -2 + Layout.preferredWidth: 154 + Layout.preferredHeight: 82 + source: Theme.png("onboarding/status_keycard_multiple") + } + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: -promoBanner.topPadding + spacing: 2 + StatusBaseText { + Layout.fillWidth: true + text: qsTr("New to Keycard?") + font.pixelSize: Theme.additionalTextSize + font.weight: Font.DemiBold + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Store and trade your crypto with a simple, secure and slim hardware wallet.") + wrapMode: Text.Wrap + font.pixelSize: Theme.additionalTextSize + color: Theme.palette.baseColor1 + } + } + StatusButton { + Layout.leftMargin: 20 + Layout.topMargin: -promoBanner.topPadding + size: StatusBaseButton.Size.Small + text: qsTr("keycard.tech") + icon.name: "external-link" + icon.width: 24 + icon.height: 24 + onClicked: openLink("https://keycard.tech/") + } + } + } + + buttons: [ + MaybeOutlineButton { + id: btnFactoryReset + visible: false + text: qsTr("Factory reset Keycard") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.keycardFactoryResetRequested() + }, + MaybeOutlineButton { + id: btnReload + visible: false + text: qsTr("I’ve inserted a different Keycard") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.reloadKeycardRequested() + } + ] + + states: [ + // normal/intro states + State { + name: "plugin" + when: root.keycardState === Onboarding.KeycardState.PluginReader || + root.keycardState === -1 + PropertyChanges { + target: root + title: qsTr("Plug in your Keycard reader") + image.source: Theme.png("onboarding/keycard/empty") + } + PropertyChanges { + target: promoBanner + visible: root.displayPromoBanner + } + }, + State { + name: "insert" + when: root.keycardState === Onboarding.KeycardState.InsertKeycard + PropertyChanges { + target: root + title: qsTr("Insert your Keycard") + infoText.text: qsTr("Get help via %1 🔗").arg(Utils.getStyledLink("https://keycard.tech", "https://keycard.tech/docs/", + infoText.hoveredLink, + Theme.palette.baseColor1, + Theme.palette.primaryColor1)) + image.source: Theme.png("onboarding/keycard/insert") + } + }, + State { + name: "reading" + when: root.keycardState === Onboarding.KeycardState.ReadingKeycard + PropertyChanges { + target: root + title: qsTr("Reading Keycard...") + image.source: Theme.png("onboarding/keycard/reading") + } + }, + // error states + State { + name: "notKeycard" + when: root.keycardState === Onboarding.KeycardState.WrongKeycard || + root.keycardState === Onboarding.KeycardState.NotKeycard + PropertyChanges { + target: root + title: qsTr("Oops this isn’t a Keycard") + subtitle: qsTr("Remove card and insert a Keycard") + image.source: Theme.png("onboarding/keycard/invalid") + } + PropertyChanges { + target: btnReload + visible: true + text: qsTr("I’ve inserted a Keycard") + } + }, + State { + name: "noService" + when: root.keycardState === Onboarding.KeycardState.NoPCSCService + PropertyChanges { + target: root + title: qsTr("Smartcard reader service unavailable") + subtitle: qsTr("The Smartcard reader service (PCSC service), required for using Keycard, is not currently working. Ensure PCSC is installed and running and try again.") + image.source: Theme.png("onboarding/keycard/error") + } + PropertyChanges { + target: btnReload + visible: true + text: qsTr("Retry") + } + }, + State { + name: "occupied" + when: root.keycardState === Onboarding.KeycardState.MaxPairingSlotsReached + PropertyChanges { + target: root + title: qsTr("All pairing slots occupied") + subtitle: qsTr("Factory reset this Keycard or insert a different one") + image.source: Theme.png("onboarding/keycard/error") + } + PropertyChanges { + target: btnFactoryReset + visible: true + } + PropertyChanges { + target: btnReload + visible: true + } + }, + State { + name: "locked" + when: root.keycardState === Onboarding.KeycardState.Locked + PropertyChanges { + target: root + title: "".arg(Theme.palette.dangerColor1) + qsTr("Keycard locked") + "" + subtitle: qsTr("The Keycard you have inserted is locked, you will need to factory reset it or insert a different one") + image.source: Theme.png("onboarding/keycard/error") + } + PropertyChanges { + target: btnFactoryReset + visible: true + } + PropertyChanges { + target: btnReload + visible: true + } + }, + // exit states + State { + name: "empty" + when: root.keycardState === Onboarding.KeycardState.Empty + StateChangeScript { + script: root.emptyKeycardDetected() + } + }, + State { + name: "notEmpty" + when: root.keycardState === Onboarding.KeycardState.NotEmpty + StateChangeScript { + script: root.notEmptyKeycardDetected() + } + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardNotEmptyPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardNotEmptyPage.qml new file mode 100644 index 0000000000..4701147bcf --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardNotEmptyPage.qml @@ -0,0 +1,37 @@ +import QtQuick 2.15 + +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +KeycardBasePage { + id: root + + signal reloadKeycardRequested() + signal loginWithThisKeycardRequested() + signal keycardFactoryResetRequested() + + title: qsTr("Keycard is not empty") + subtitle: qsTr("You can’t use it to store new keys right now") + image.source: Theme.png("onboarding/keycard/error") + + pageClassName: "KeycardNotEmptyPage" + + buttons: [ + MaybeOutlineButton { + text: qsTr("I’ve inserted a different Keycard") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.reloadKeycardRequested() + }, + MaybeOutlineButton { + text: qsTr("Log in with this Keycard") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.loginWithThisKeycardRequested() + }, + MaybeOutlineButton { + text: qsTr("Factory reset Keycard") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.keycardFactoryResetRequested() + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/LoginBySyncingPage.qml b/ui/app/AppLayouts/Onboarding2/pages/LoginBySyncingPage.qml new file mode 100644 index 0000000000..2ea249c969 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/LoginBySyncingPage.qml @@ -0,0 +1,67 @@ +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 shared.views 1.0 +import shared.popups 1.0 + +OnboardingPage { + id: root + + property var validateConnectionString: (stringValue) => { console.error("validateConnectionString IMPLEMENT ME"); return false } + + signal syncProceedWithConnectionString(string connectionString) + + title: qsTr("Log in by syncing") + + pageClassName: "LoginBySyncingPage" + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(440, root.availableWidth) + spacing: Theme.xlPadding + + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -12 + text: qsTr("If you have Status on another device") + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + SyncingEnterCode { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + validateConnectionString: root.validateConnectionString + + secondTabName: qsTr("Enter code") + showBetaTag: false + + onDisplayInstructions: instructionsPopup.createObject(root).open() + onProceed: (connectionString) => root.syncProceedWithConnectionString(connectionString) + } + } + } + + Component { + id: instructionsPopup + GetSyncCodeInstructionsPopup { + destroyOnClose: true + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/LoginPage.qml b/ui/app/AppLayouts/Onboarding2/pages/LoginPage.qml new file mode 100644 index 0000000000..145c69f10a --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/LoginPage.qml @@ -0,0 +1,290 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml.Models 2.15 + +import StatusQ 0.1 +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups 0.1 +import StatusQ.Popups.Dialog 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +import utils 1.0 + +OnboardingPage { + id: root + + required property bool networkChecksEnabled + + title: qsTr("Log in") + + signal loginWithSeedphraseRequested() + signal loginWithSyncingRequested() + signal loginWithKeycardRequested() + + pageClassName: "LoginPage" + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(380, root.availableWidth) + spacing: Theme.bigPadding + + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -Theme.padding + text: qsTr("How would you like to log in to Status?") + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + OnboardingFrame { + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 20 + StatusImage { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(250, parent.width) + Layout.preferredHeight: Math.min(250, height) + source: Theme.png("onboarding/status_login_seedphrase") + mipmap: true + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Log in with recovery phrase") + font.pixelSize: Theme.secondaryAdditionalTextSize + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -Theme.padding + text: qsTr("If you have your Status recovery phrase") + font.pixelSize: Theme.additionalTextSize + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: Theme.palette.baseColor1 + } + StatusButton { + objectName: "btnWithSeedphrase" + Layout.fillWidth: true + text: qsTr("Enter recovery phrase") + font.pixelSize: Theme.additionalTextSize + onClicked: root.loginWithSeedphraseRequested() + } + } + } + + OnboardingButtonFrame { + Layout.fillWidth: true + id: buttonFrame + contentItem: ColumnLayout { + spacing: 0 + ListItemButton { + objectName: "btnBySyncing" + Layout.fillWidth: true + text: qsTr("Log in by syncing") // FIXME wording, "Log in by pairing"? + subTitle: qsTr("If you have Status on another device") + icon.source: Theme.png("onboarding/login_syncing") + onClicked: loginWithSyncAck.createObject(root).open() + } + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: -buttonFrame.padding + Layout.rightMargin: -buttonFrame.padding + Layout.preferredHeight: 1 + color: Theme.palette.statusMenu.separatorColor + } + ListItemButton { + objectName: "btnWithKeycard" + Layout.fillWidth: true + text: qsTr("Log in with Keycard") + subTitle: qsTr("If your profile keys are stored on a Keycard") + icon.source: Theme.png("onboarding/create_profile_keycard") + onClicked: root.loginWithKeycardRequested() + } + } + } + } + } + + NetworkChecker { + id: netChecker + active: root.networkChecksEnabled + } + + Component { + id: loginWithSyncAck + StatusDialog { + objectName: "loginWithSyncAckPopup" + title: qsTr("Log in by syncing") + width: 480 + padding: 20 + destroyOnClose: true + onOpened: if (root.networkChecksEnabled) netChecker.checkNetwork() + contentItem: ColumnLayout { + spacing: 20 + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.Wrap + text: qsTr("To pair your devices and sync your profile, make sure to check and complete the following steps:") + } + ColumnLayout { + Layout.fillWidth: true + spacing: Theme.padding + StatusCheckBox { + objectName: "ack1" + Layout.fillWidth: true + id: ack1 + text: qsTr("Connect both devices to the same network") + } + StatusCheckBox { + objectName: "ack2" + Layout.fillWidth: true + id: ack2 + text: qsTr("Make sure you are logged in on the other device") + } + StatusCheckBox { + objectName: "ack3" + Layout.fillWidth: true + id: ack3 + text: qsTr("Disable the firewall and VPN on both devices") + } + } + } + footer: StatusDialogFooter { + spacing: Theme.padding + rightButtons: ObjectModel { + StatusFlatButton { + text: qsTr("Cancel") + onClicked: close() + } + StatusButton { + objectName: "btnContinue" + text: qsTr("Continue") + enabled: ack1.checked && ack2.checked && ack3.checked + onClicked: { + if (root.networkChecksEnabled && !netChecker.isOnline) { + networkCheckPopup.createObject(root, {netChecker}).open() + } else { + root.loginWithSyncingRequested() + } + close() + } + } + } + } + } + } + + + + Component { + id: networkCheckPopup + StatusDialog { + objectName: "networkCheckPopup" + title: qsTr("Status does not have access to local network") + width: 480 + padding: 20 + destroyOnClose: true + + required property var netChecker + + contentItem: ColumnLayout { + spacing: 20 + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.Wrap + text: qsTr("Status must be connected to the local network on this device for you to be able to log in via syncing. To rectify this...") + } + OnboardingFrame { + Layout.fillWidth: true + dropShadow: false + cornerRadius: Theme.radius + horizontalPadding: 20 + verticalPadding: 12 + contentItem: ColumnLayout { + spacing: 12 + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.Wrap + color: Theme.palette.baseColor1 + text: qsTr("1. Open System Settings") + } + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.Wrap + color: Theme.palette.baseColor1 + text: qsTr("2. Click Privacy & Security") + } + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.Wrap + color: Theme.palette.baseColor1 + text: qsTr("3. Click Local Network") + } + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.Wrap + color: Theme.palette.baseColor1 + text: qsTr("4. Find Status") + } + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.Wrap + color: Theme.palette.baseColor1 + text: qsTr("5. Toggle the switch to grant access") + } + StatusBaseText { + Layout.fillWidth: true + wrapMode: Text.Wrap + color: Theme.palette.baseColor1 + text: qsTr("6. Click %1 below").arg(`` + + qsTr("Verify local network access") + + "") + } + } + } + } + footer: StatusDialogFooter { + spacing: Theme.padding + rightButtons: ObjectModel { + StatusFlatButton { + text: qsTr("Cancel") + onClicked: close() + } + StatusButton { + objectName: "btnVerifyNet" + text: loading ? qsTr("Verifying") : qsTr("Verify local network access") + loading: netChecker.checking + interactive: !loading + onClicked: netChecker.checkNetwork() + } + } + } + Connections { + target: netChecker + function onIsOnlineChanged() { + if (netChecker.isOnline) { + root.loginWithSyncingRequested() + close() + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml b/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml new file mode 100644 index 0000000000..6a9efcbc0f --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml @@ -0,0 +1,20 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core.Theme 0.1 + +Page { + required property string pageClassName + + signal openLink(string link) + signal openLinkWithConfirmation(string link, string domain) + + implicitWidth: 1200 + implicitHeight: 700 + + padding: 12 + + background: Rectangle { + color: Theme.palette.background + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/SeedphrasePage.qml b/ui/app/AppLayouts/Onboarding2/pages/SeedphrasePage.qml new file mode 100644 index 0000000000..b79e842bcf --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/SeedphrasePage.qml @@ -0,0 +1,65 @@ +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 shared.panels 1.0 + +OnboardingPage { + id: root + + title: qsTr("Create profile using a recovery phrase") + property string subtitle: qsTr("Enter your 12, 18 or 24 word recovery phrase") + + property var isSeedPhraseValid: (mnemonic) => { console.error("isSeedPhraseValid IMPLEMENT ME"); return false } + + signal seedphraseSubmitted(string seedphrase) + + pageClassName: "SeedphrasePage" + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(610, root.availableWidth) + spacing: Theme.bigPadding + + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -Theme.padding + text: root.subtitle + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + EnterSeedPhrase { + id: seedPanel + Layout.preferredWidth: 580 + Layout.alignment: Qt.AlignHCenter + isSeedPhraseValid: root.isSeedPhraseValid + onSubmitSeedPhrase: root.seedphraseSubmitted(getSeedPhraseAsString()) + } + + StatusButton { + objectName: "btnContinue" + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: -Theme.halfPadding + enabled: seedPanel.seedPhraseIsValid + text: qsTr("Continue") + onClicked: root.seedphraseSubmitted(seedPanel.getSeedPhraseAsString()) + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/SyncProgressPage.qml b/ui/app/AppLayouts/Onboarding2/pages/SyncProgressPage.qml new file mode 100644 index 0000000000..59595e4bef --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/SyncProgressPage.qml @@ -0,0 +1,237 @@ +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 AppLayouts.Onboarding.enums 1.0 + +OnboardingPage { + id: root + + required property int syncState // Onboarding.SyncState.xxx + property int timeoutInterval: 30000 + + signal loginToAppRequested() + signal restartSyncRequested() + signal loginWithSeedphraseRequested() + + pageClassName: "SyncProgressPage" + + Timer { + id: timer + interval: root.timeoutInterval + running: root.syncState === Onboarding.SyncState.InProgress + onTriggered: root.syncState = Onboarding.SyncState.Failed + } + + states: [ + State { + name: "inprogress" + when: root.syncState === Onboarding.SyncState.InProgress + PropertyChanges { + target: root + title: qsTr("Profile sync in progress...") + } + PropertyChanges { + target: subtitle + text: qsTr("Your profile data is being synced to this device") + } + PropertyChanges { + target: iconLoader + sourceComponent: loadingIndicator + } + PropertyChanges { + target: image + source: Theme.png("onboarding/status_sync_progress") + } + PropertyChanges { + target: subImageText + text: qsTr("Please keep both devices switched on and connected to the same network until the sync is complete") + visible: true + } + }, + State { + name: "success" + when: root.syncState === Onboarding.SyncState.Success + PropertyChanges { + target: root + title: qsTr("Profile synced") + } + PropertyChanges { + target: subtitle + text: qsTr("Your profile data has been synced to this device") + } + PropertyChanges { + target: iconLoader + sourceComponent: successIcon + } + PropertyChanges { + target: image + source: Theme.png("onboarding/status_sync_success") + } + PropertyChanges { + target: loginButton + visible: true + } + }, + State { + name: "failed" + when: root.syncState === Onboarding.SyncState.Failed + PropertyChanges { + target: root + title: "".arg(Theme.palette.dangerColor1) + qsTr("Profile syncing failed") + "" + } + PropertyChanges { + target: subtitle + text: qsTr("Try again and double-check the instructions") + } + PropertyChanges { + target: iconLoader + sourceComponent: failedIcon + } + PropertyChanges { + target: image + source: Theme.png("onboarding/status_sync_failed") + } + PropertyChanges { + target: tryAgainButton + visible: true + } + PropertyChanges { + target: loginWithSeedphraseButton + visible: true + } + PropertyChanges { + target: loginAnywayButton + visible: true + } + } + ] + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(400, root.availableWidth) + spacing: Theme.halfPadding + + Loader { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + Layout.alignment: Qt.AlignHCenter + id: iconLoader + } + + StatusBaseText { + Layout.fillWidth: true + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + text: root.title + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + id: subtitle + Layout.fillWidth: true + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StatusImage { + id: image + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(224, parent.width) + Layout.preferredHeight: Math.min(214, height) + Layout.topMargin: Theme.bigPadding + Layout.bottomMargin: Theme.bigPadding + source: Theme.png("onboarding/status_generate_keys") + mipmap: true + } + + StatusBaseText { + id: subImageText + Layout.fillWidth: true + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + visible: false + } + + StatusButton { + objectName: "btnLogin" + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 240 + id: loginButton + text: qsTr("Log in") + visible: false + onClicked: root.loginToAppRequested() + } + + StatusButton { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 240 + id: tryAgainButton + text: qsTr("Try to sync again") + visible: false + onClicked: root.restartSyncRequested() + } + + StatusButton { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 240 + id: loginWithSeedphraseButton + text: qsTr("Log in via recovery phrase") + visible: false + normalColor: "transparent" + borderWidth: 1 + borderColor: Theme.palette.baseColor2 + onClicked: root.loginWithSeedphraseRequested() + } + + StatusButton { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 240 + id: loginAnywayButton + text: qsTr("Log in anyway") + visible: false + normalColor: "transparent" + borderWidth: 1 + borderColor: Theme.palette.baseColor2 + onClicked: root.loginToAppRequested() + } + } + } + + Component { + id: loadingIndicator + Rectangle { + color: Theme.palette.baseColor2 + radius: width/2 + StatusDotsLoadingIndicator { + anchors.centerIn: parent + } + } + } + + Component { + id: successIcon + StatusRoundIcon { + asset.name: "check-circle" + asset.color: Theme.palette.successColor1 + asset.bgColor: Theme.palette.successColor2 + } + } + + Component { + id: failedIcon + StatusRoundIcon { + asset.name: "close-circle" + asset.color: Theme.palette.dangerColor1 + asset.bgColor: Theme.palette.dangerColor3 + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml b/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml new file mode 100644 index 0000000000..a0efb3cfc6 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml @@ -0,0 +1,156 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.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 AppLayouts.Onboarding2.components 1.0 + +import utils 1.0 + +OnboardingPage { + id: root + + pageClassName: "WelcomePage" + title: qsTr("Welcome to Status") + + signal createProfileRequested() + signal loginRequested() + + signal privacyPolicyRequested() + signal termsOfUseRequested() + + QtObject { + id: d + readonly property ListModel newsModel: ListModel { + ListElement { + primary: qsTr("Own your crypto") + secondary: qsTr("Use the leading multi-chain self-custodial wallet") + image: "onboarding/carousel/crypto" + } + ListElement { + primary: qsTr("Chat privately with friends") + secondary: qsTr("With full metadata privacy and e2e encryption") + image: "onboarding/carousel/chat" + } + ListElement { + primary: qsTr("Store your assets on Keycard") + secondary: qsTr("Be safe with secure cold wallet") + image: "onboarding/carousel/keycard" + } + } + } + + contentItem: RowLayout { + spacing: root.padding + + // left part (welcome + buttons) + Item { + Layout.preferredWidth: root.availableWidth/2 - root.horizontalPadding + Layout.fillHeight: true + + ColumnLayout { + width: Math.min(400, parent.width) + spacing: 28 + anchors.centerIn: parent + anchors.verticalCenterOffset: -headerText.height/2 + + StatusImage { + Layout.preferredWidth: 90 + Layout.preferredHeight: 90 + Layout.alignment: Qt.AlignHCenter + source: Theme.png("status") + mipmap: true + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 4 + radius: 12 + samples: 25 + spread: 0.2 + color: Theme.palette.dropShadow + } + } + + StatusBaseText { + id: headerText + Layout.fillWidth: true + text: root.title + font.pixelSize: 40 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StatusBaseText { + Layout.fillWidth: true + text: qsTr("The open-source, decentralised wallet and messenger") + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.xlPadding + width: Math.min(320, parent.width) + spacing: 12 + + StatusButton { + objectName: "btnCreateProfile" + Layout.fillWidth: true + text: qsTr("Create profile") + onClicked: root.createProfileRequested() + } + StatusButton { + objectName: "btnLogin" + Layout.fillWidth: true + text: qsTr("Log in") + onClicked: root.loginRequested() + normalColor: "transparent" + borderWidth: 1 + borderColor: Theme.palette.baseColor2 + } + StatusBaseText { + objectName: "approvalLinks" + Layout.fillWidth: true + Layout.topMargin: Theme.halfPadding + text: qsTr("By proceeding you accept Status
%1 and %2") + .arg(Utils.getStyledLink(qsTr("Terms of Use"), "#terms", hoveredLink, Theme.palette.primaryColor1, Theme.palette.primaryColor1, false)) + .arg(Utils.getStyledLink(qsTr("Privacy Policy"), "#privacy", hoveredLink, Theme.palette.primaryColor1, Theme.palette.primaryColor1, false)) + textFormat: Text.RichText + font.pixelSize: Theme.tertiaryTextFontSize + lineHeightMode: Text.FixedHeight + lineHeight: 16 + wrapMode: Text.WordWrap + color: Theme.palette.baseColor1 + horizontalAlignment: Text.AlignHCenter + onLinkActivated: { + if (link == "#terms") + root.termsOfUseRequested() + else if (link == "#privacy") + root.privacyPolicyRequested() + } + + HoverHandler { + // Qt CSS doesn't support custom cursor shape + cursorShape: !!parent.hoveredLink ? Qt.PointingHandCursor : undefined + } + } + } + } + + + // right part (news carousel) + NewsCarousel { + Layout.preferredWidth: root.availableWidth/2 - root.horizontalPadding + Layout.fillHeight: true + newsModel: d.newsModel + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/qmldir b/ui/app/AppLayouts/Onboarding2/pages/qmldir new file mode 100644 index 0000000000..171992b751 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/qmldir @@ -0,0 +1,21 @@ +BackupSeedphraseAcks 1.0 BackupSeedphraseAcks.qml +BackupSeedphraseIntro 1.0 BackupSeedphraseIntro.qml +BackupSeedphraseOutro 1.0 BackupSeedphraseOutro.qml +BackupSeedphraseReveal 1.0 BackupSeedphraseReveal.qml +BackupSeedphraseVerify 1.0 BackupSeedphraseVerify.qml +CreateKeycardProfilePage 1.0 CreateKeycardProfilePage.qml +CreatePasswordPage 1.0 CreatePasswordPage.qml +CreateProfilePage 1.0 CreateProfilePage.qml +EnableBiometricsPage 1.0 EnableBiometricsPage.qml +HelpUsImproveStatusPage 1.0 HelpUsImproveStatusPage.qml +KeycardCreatePinPage 1.0 KeycardCreatePinPage.qml +KeycardEmptyPage 1.0 KeycardEmptyPage.qml +KeycardEnterPinPage 1.0 KeycardEnterPinPage.qml +KeycardIntroPage 1.0 KeycardIntroPage.qml +KeycardNotEmptyPage 1.0 KeycardNotEmptyPage.qml +KeycardAddKeyPairPage 1.0 KeycardAddKeyPairPage.qml +LoginPage 1.0 LoginPage.qml +LoginBySyncingPage 1.0 LoginBySyncingPage.qml +SeedphrasePage 1.0 SeedphrasePage.qml +WelcomePage 1.0 WelcomePage.qml +SyncProgressPage 1.0 SyncProgressPage.qml diff --git a/ui/app/AppLayouts/Onboarding2/qmldir b/ui/app/AppLayouts/Onboarding2/qmldir new file mode 100644 index 0000000000..ac7c41394a --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/qmldir @@ -0,0 +1 @@ +OnboardingLayout 1.0 OnboardingLayout.qml diff --git a/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml b/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml new file mode 100644 index 0000000000..96b6857d85 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml @@ -0,0 +1,53 @@ +import QtQml 2.15 + +import StatusQ.Core.Utils 0.1 as StatusQUtils + +import AppLayouts.Onboarding.enums 1.0 + +QtObject { + readonly property QtObject d: StatusQUtils.QObject { + id: d + readonly property var onboardingModuleInst: onboardingModule + } + + // keycard + readonly property int keycardState: d.onboardingModuleInst.keycardState // cf. enum Onboarding.KeycardState + readonly property int keycardRemainingPinAttempts: d.onboardingModuleInst.keycardRemainingPinAttempts + + function setPin(pin: string) { // -> bool + return d.onboardingModuleInst.setPin(pin) + } + + readonly property int addKeyPairState: d.onboardingModuleInst.addKeyPairState // cf. enum Onboarding.AddKeyPairState + function startKeypairTransfer() { // -> void + d.onboardingModuleInst.startKeypairTransfer() + } + + // password + function getPasswordStrengthScore(password: string) { // -> int + return d.onboardingModuleInst.getPasswordStrengthScore(password) + } + + // seedphrase/mnemonic + function validMnemonic(mnemonic: string) { // -> bool + return d.onboardingModuleInst.validMnemonic(mnemonic) + } + function getMnemonic() { // -> string + return d.onboardingModuleInst.mnemonic() + } + function mnemonicWasShown() { // -> void + d.onboardingModuleInst.mnemonicWasShown() + } + function removeMnemonic() { // -> void + d.onboardingModuleInst.removeMnemonic() + } + + // sync + readonly property int syncState: d.onboardingModuleInst.syncState // cf. enum Onboarding.SyncState + function validateLocalPairingConnectionString(connectionString: string) { // -> bool + return d.onboardingModuleInst.validateLocalPairingConnectionString(connectionString) + } + function inputConnectionStringForBootstrapping(connectionString: string) { // -> void + d.onboardingModuleInst.inputConnectionStringForBootstrapping(connectionString) + } +} diff --git a/ui/app/AppLayouts/Onboarding2/stores/qmldir b/ui/app/AppLayouts/Onboarding2/stores/qmldir new file mode 100644 index 0000000000..ca75101238 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/stores/qmldir @@ -0,0 +1 @@ +OnboardingStore 1.0 OnboardingStore.qml diff --git a/ui/app/AppLayouts/Profile/helpers/SettingsEntriesModel.qml b/ui/app/AppLayouts/Profile/helpers/SettingsEntriesModel.qml index 6522875a33..171e5045bb 100644 --- a/ui/app/AppLayouts/Profile/helpers/SettingsEntriesModel.qml +++ b/ui/app/AppLayouts/Profile/helpers/SettingsEntriesModel.qml @@ -43,7 +43,7 @@ SortFilterProxyModel { readonly property var entries: [ { subsection: Constants.settingsSubsection.backUpSeed, - text: qsTr("Back up seed phrase"), + text: qsTr("Back up recovery phrase"), icon: "seed-phrase" }, { @@ -73,7 +73,7 @@ SortFilterProxyModel { text: qsTr("Syncing"), icon: "rotate", isExperimental: true, - experimentalTooltip: qsTr("Connection problems can happen.
If they do, please use the Enter a Seed Phrase feature instead.") + experimentalTooltip: qsTr("Connection problems can happen.
If they do, please use the Enter a Recovery Phrase feature instead.") }, { subsection: Constants.settingsSubsection.messaging, diff --git a/ui/app/AppLayouts/Profile/popups/BackupSeedModal.qml b/ui/app/AppLayouts/Profile/popups/BackupSeedModal.qml index 5a732bab76..c2240005fa 100644 --- a/ui/app/AppLayouts/Profile/popups/BackupSeedModal.qml +++ b/ui/app/AppLayouts/Profile/popups/BackupSeedModal.qml @@ -63,8 +63,8 @@ StatusStackModal { } implicitHeight: 748 - width: 480 - headerSettings.title: qsTr("Back up your seed phrase") + width: 500 + headerSettings.title: qsTr("Back up your recovery phrase") rightButtons: [ d.skipButton, nextButton, finishButton ] nextButton: StatusButton { @@ -88,7 +88,7 @@ StatusStackModal { switch (root.currentIndex) { case 0: case 1: - return qsTr("Confirm Seed Phrase"); + return qsTr("Confirm Recovery Phrase"); case 2: case 3: return qsTr("Continue"); @@ -100,7 +100,7 @@ StatusStackModal { } finishButton: StatusButton { - text: qsTr("Complete & Delete My Seed Phrase") + text: qsTr("Complete & Delete My Recovery Phrase") objectName: "BackupSeedModal_completeAndDeleteSeedPhraseButton" enabled: d.seedStored onClicked: { @@ -137,7 +137,7 @@ StatusStackModal { BackupSeedStepBase { id: confirmFirstWord objectName: "BackupSeedModal_BackupSeedStepBase_confirmFirstWord" - titleText: qsTr("Confirm word #%1 of your seed phrase").arg(d.firstRandomNo + 1) + titleText: qsTr("Confirm word #%1 of your recovery phrase").arg(d.firstRandomNo + 1) wordRandomNumber: d.firstRandomNo wordAtRandomNumber: root.privacyStore.getMnemonicWordAtIndex(d.firstRandomNo) onEnterPressed: { @@ -147,7 +147,7 @@ StatusStackModal { BackupSeedStepBase { id: confirmSecondWord objectName: "BackupSeedModal_BackupSeedStepBase_confirmSecondWord" - titleText: qsTr("Confirm word #%1 of your seed phrase").arg(d.secondRandomNo + 1) + titleText: qsTr("Confirm word #%1 of your recovery phrase").arg(d.secondRandomNo + 1) wordRandomNumber: d.secondRandomNo wordAtRandomNumber: root.privacyStore.getMnemonicWordAtIndex(d.secondRandomNo) }, diff --git a/ui/app/AppLayouts/Profile/popups/RemoveKeypairPopup.qml b/ui/app/AppLayouts/Profile/popups/RemoveKeypairPopup.qml index fb69669c80..f12731a395 100644 --- a/ui/app/AppLayouts/Profile/popups/RemoveKeypairPopup.qml +++ b/ui/app/AppLayouts/Profile/popups/RemoveKeypairPopup.qml @@ -31,7 +31,7 @@ StatusDialog { StatusBaseText { Layout.fillWidth: true - text: qsTr("Are you sure you want to remove %1 key pair? The key pair will be removed from all of your synced devices. Make sure you have a backup of your keys or seed phrase before proceeding.").arg(name) + text: qsTr("Are you sure you want to remove %1 key pair? The key pair will be removed from all of your synced devices. Make sure you have a backup of your keys or recovery phrase before proceeding.").arg(name) wrapMode: Text.WordWrap font.pixelSize: 15 } diff --git a/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml b/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml index 66518369ff..f587d9b7c9 100644 --- a/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml +++ b/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml @@ -62,7 +62,7 @@ StatusMenu { } StatusAction { - text: enabled? root.keyPair.pairType === Constants.keypair.type.privateKeyImport? qsTr("Import via entering private key") : qsTr("Import via entering seed phrase") : "" + text: enabled? root.keyPair.pairType === Constants.keypair.type.privateKeyImport? qsTr("Import via entering private key") : qsTr("Import via entering recovery phrase") : "" enabled: !!root.keyPair && !root.keyPair.migratedToKeycard && root.keyPair.operability === Constants.keypair.operability.nonOperable && diff --git a/ui/app/AppLayouts/Profile/popups/backupseed/Acknowledgements.qml b/ui/app/AppLayouts/Profile/popups/backupseed/Acknowledgements.qml index 76482285f8..cf859e1e16 100644 --- a/ui/app/AppLayouts/Profile/popups/backupseed/Acknowledgements.qml +++ b/ui/app/AppLayouts/Profile/popups/backupseed/Acknowledgements.qml @@ -71,7 +71,7 @@ ColumnLayout { font.pixelSize: Theme.primaryTextFontSize horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap - text: qsTr("Your seed phrase is a 12-word passcode to your funds.") + text: qsTr("Your recovery phrase is a 12-word passcode to your funds.") Layout.fillWidth: true } @@ -82,7 +82,7 @@ ColumnLayout { textFormat: Text.RichText font.pixelSize: Theme.primaryTextFontSize lineHeight: 1.2 - text: qsTr("Your seed phrase cannot be recovered if lost. Therefore, you must back it up. The simplest way is to write it down offline and store it somewhere secure.") + text: qsTr("Your recovery phrase cannot be recovered if lost. Therefore, you must back it up. The simplest way is to write it down offline and store it somewhere secure.") Layout.fillWidth: true } } @@ -107,7 +107,7 @@ ColumnLayout { id: writeDown objectName: "Acknowledgements_writeDown" spacing: Theme.padding - text: qsTr("I am ready to write down my seed phrase") + text: qsTr("I am ready to write down my recovery phrase") font.pixelSize: Theme.primaryTextFontSize Layout.fillWidth: true } @@ -140,7 +140,7 @@ ColumnLayout { wrapMode: Text.WordWrap color: Theme.palette.dangerColor1 lineHeight: 1.2 - text: qsTr("You can only complete this process once. Status will not store your seed phrase and can never help you recover it.") + text: qsTr("You can only complete this process once. Status will not store your recovery phrase and can never help you recover it.") } Rectangle { diff --git a/ui/app/AppLayouts/Profile/popups/backupseed/BackupSeedStepBase.qml b/ui/app/AppLayouts/Profile/popups/backupseed/BackupSeedStepBase.qml index f5a87401f9..25f704b62f 100644 --- a/ui/app/AppLayouts/Profile/popups/backupseed/BackupSeedStepBase.qml +++ b/ui/app/AppLayouts/Profile/popups/backupseed/BackupSeedStepBase.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 import StatusQ.Controls 0.1 import StatusQ.Controls.Validators 0.1 diff --git a/ui/app/AppLayouts/Profile/popups/backupseed/ConfirmSeedPhrasePanel.qml b/ui/app/AppLayouts/Profile/popups/backupseed/ConfirmSeedPhrasePanel.qml index 19ee9d15bd..3f236eb7e1 100644 --- a/ui/app/AppLayouts/Profile/popups/backupseed/ConfirmSeedPhrasePanel.qml +++ b/ui/app/AppLayouts/Profile/popups/backupseed/ConfirmSeedPhrasePanel.qml @@ -18,7 +18,7 @@ BackupSeedStepBase { property bool hideSeed: true property ProfileStores.PrivacyStore privacyStore - titleText: qsTr("Write down your 12-word seed phrase to keep offline") + titleText: qsTr("Write down your 12-word recovery phrase to keep offline") Item { implicitHeight: 304 @@ -69,7 +69,7 @@ BackupSeedStepBase { anchors.centerIn: parent visible: hideSeed icon.name: "view" - text: qsTr("Reveal seed phrase") + text: qsTr("Reveal recovery phrase") onClicked: { privacyStore.mnemonicWasShown(); hideSeed = false; @@ -86,7 +86,7 @@ BackupSeedStepBase { wrapMode: Text.WordWrap textFormat: Text.RichText color: Theme.palette.dangerColor1 - text: qsTr("The next screen contains your seed phrase.\nAnyone who sees it can use it to access to your funds.") + text: qsTr("The next screen contains your recovery phrase.\nAnyone who sees it can use it to access to your funds.") Layout.fillWidth: true } } diff --git a/ui/app/AppLayouts/Profile/popups/backupseed/ConfirmStoringSeedPhrasePanel.qml b/ui/app/AppLayouts/Profile/popups/backupseed/ConfirmStoringSeedPhrasePanel.qml index 3a9566626d..ff1650481a 100644 --- a/ui/app/AppLayouts/Profile/popups/backupseed/ConfirmStoringSeedPhrasePanel.qml +++ b/ui/app/AppLayouts/Profile/popups/backupseed/ConfirmStoringSeedPhrasePanel.qml @@ -38,7 +38,7 @@ BackupSeedStepBase { wrapMode: Text.WordWrap font.pixelSize: Theme.primaryTextFontSize lineHeight: 1.2 - text: qsTr("By completing this process, you will remove your seed phrase from this application’s storage. This makes your funds more secure.") + text: qsTr("By completing this process, you will remove your recovery phrase from this application’s storage. This makes your funds more secure.") Layout.fillWidth: true } @@ -48,7 +48,7 @@ BackupSeedStepBase { wrapMode: Text.WordWrap font.pixelSize: Theme.primaryTextFontSize lineHeight: 1.2 - text: qsTr("You will remain logged in, and your seed phrase will be entirely in your hands.") + text: qsTr("You will remain logged in, and your recovery phrase will be entirely in your hands.") Layout.fillWidth: true } @@ -57,7 +57,7 @@ BackupSeedStepBase { objectName: "ConfirmStoringSeedPhrasePanel_storeCheck" spacing: Theme.padding font.pixelSize: Theme.primaryTextFontSize - text: qsTr("I acknowledge that Status will not be able to show me my seed phrase again.") + text: qsTr("I acknowledge that Status will not be able to show me my recovery phrase again.") Layout.fillWidth: true Layout.topMargin: Theme.bigPadding } diff --git a/ui/app/AppLayouts/Profile/views/SettingsLeftTabView.qml b/ui/app/AppLayouts/Profile/views/SettingsLeftTabView.qml index e1b915ec53..c366593660 100644 --- a/ui/app/AppLayouts/Profile/views/SettingsLeftTabView.qml +++ b/ui/app/AppLayouts/Profile/views/SettingsLeftTabView.qml @@ -65,7 +65,7 @@ Item { id: confirmDialog confirmButtonObjectName: "signOutConfirmation" headerSettings.title: qsTr("Sign out") - confirmationText: qsTr("Make sure you have your account password and seed phrase stored. Without them you can lock yourself out of your account and lose funds.") + confirmationText: qsTr("Make sure you have your account password and recovery phrase stored. Without them you can lock yourself out of your account and lose funds.") confirmButtonLabel: qsTr("Sign out & Quit") onConfirmButtonClicked: Qt.quit() } diff --git a/ui/app/AppLayouts/Profile/views/SyncingView.qml b/ui/app/AppLayouts/Profile/views/SyncingView.qml index 81370cccae..8fca6312d6 100644 --- a/ui/app/AppLayouts/Profile/views/SyncingView.qml +++ b/ui/app/AppLayouts/Profile/views/SyncingView.qml @@ -187,7 +187,7 @@ SettingsContentBase { anchors.left: parent.right anchors.leftMargin: 8 anchors.verticalCenter: parent.verticalCenter - tooltipText: qsTr("Connection problems can happen.
If they do, please use the Enter a Seed Phrase feature instead.") + tooltipText: qsTr("Connection problems can happen.
If they do, please use the Enter a Recovery Phrase feature instead.") } } } diff --git a/ui/app/AppLayouts/Profile/views/keycard/MainView.qml b/ui/app/AppLayouts/Profile/views/keycard/MainView.qml index fad9c1f529..8aa69932f2 100644 --- a/ui/app/AppLayouts/Profile/views/keycard/MainView.qml +++ b/ui/app/AppLayouts/Profile/views/keycard/MainView.qml @@ -120,7 +120,7 @@ ColumnLayout { StatusListItem { Layout.fillWidth: true - title: qsTr("Create a new Keycard account with a new seed phrase") + title: qsTr("Create a new Keycard account with a new recovery phrase") objectName: "createNewKeycardAccount" components: [ StatusIcon { @@ -143,7 +143,7 @@ ColumnLayout { StatusListItem { Layout.fillWidth: true - title: qsTr("Import or restore via a seed phrase") + title: qsTr("Import or restore via a recovery phrase") objectName: "importRestoreKeycard" components: [ StatusIcon { diff --git a/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml b/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml index d2e6177cf5..87d3876116 100644 --- a/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml +++ b/ui/app/AppLayouts/Profile/views/wallet/AccountView.qml @@ -167,7 +167,7 @@ ColumnLayout { case Constants.keypair.type.profile: return qsTr("Derived from your default Status key pair") case Constants.keypair.type.seedImport: - return qsTr("Imported from seed phrase") + return qsTr("Imported from recovery phrase") case Constants.keypair.type.privateKeyImport: return qsTr("Imported from private key") case Constants.keypair.type.watchOnly: diff --git a/ui/app/AppLayouts/Wallet/panels/SeedPhraseBackupWarning.qml b/ui/app/AppLayouts/Wallet/panels/SeedPhraseBackupWarning.qml index 75023b3108..ea94788358 100644 --- a/ui/app/AppLayouts/Wallet/panels/SeedPhraseBackupWarning.qml +++ b/ui/app/AppLayouts/Wallet/panels/SeedPhraseBackupWarning.qml @@ -22,7 +22,7 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter StyledText { - text: qsTr("Back up your seed phrase") + text: qsTr("Back up your recovery phrase") font.pixelSize: 13 anchors.verticalCenter: parent.verticalCenter color: Theme.palette.white diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 59e6cd97a1..5fab2c8ffe 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -1249,7 +1249,7 @@ Item { active: !appMain.rootStore.profileSectionStore.profileStore.userDeclinedBackupBanner && !appMain.rootStore.profileSectionStore.profileStore.privacyStore.mnemonicBackedUp type: ModuleWarning.Danger - text: qsTr("Secure your seed phrase") + text: qsTr("Secure your recovery phrase") buttonText: qsTr("Back up now") delay: false onClicked: popups.openBackUpSeedPopup() diff --git a/ui/app/mainui/Popups.qml b/ui/app/mainui/Popups.qml index fe106fce16..6916498622 100644 --- a/ui/app/mainui/Popups.qml +++ b/ui/app/mainui/Popups.qml @@ -111,6 +111,7 @@ QtObject { Global.openBuyCryptoModalRequested.connect(openBuyCryptoModal) Global.privacyPolicyRequested.connect(() => openPopup(privacyPolicyPopupComponent)) Global.openPaymentRequestModalRequested.connect(openPaymentRequestModal) + Global.termsOfUseRequested.connect(() => openPopup(termsOfUsePopupComponent)) } property var currentPopup @@ -1278,23 +1279,25 @@ QtObject { }, Component { id: privacyPolicyPopupComponent - StatusDialog { - width: 600 - padding: 0 + StatusSimpleTextPopup { title: qsTr("Status Software Privacy Policy") - StatusScrollView { - id: privacyDialogScrollView - anchors.fill: parent - contentWidth: availableWidth - StatusBaseText { - width: privacyDialogScrollView.availableWidth - wrapMode: Text.Wrap - textFormat: Text.MarkdownText - text: SQUtils.StringUtils.readTextFile(":/imports/assets/docs/privacy.mdwn") - onLinkActivated: Global.openLinkWithConfirmation(link, SQUtils.StringUtils.extractDomainFromLink(link)) - } + content { + textFormat: Text.MarkdownText + text: SQUtils.StringUtils.readTextFile(":/imports/assets/docs/privacy.mdwn") + onLinkActivated: Global.openLinkWithConfirmation(link, SQUtils.StringUtils.extractDomainFromLink(link)) + } + destroyOnClose: true + } + }, + Component { + id: termsOfUsePopupComponent + StatusSimpleTextPopup { + title: qsTr("Status Software Terms of Use") + content { + textFormat: Text.MarkdownText + text: SQUtils.StringUtils.readTextFile(":/imports/assets/docs/terms-of-use.mdwn") + onLinkActivated: Global.openLinkWithConfirmation(link, SQUtils.StringUtils.extractDomainFromLink(link)) } - standardButtons: Dialog.Ok destroyOnClose: true } }, diff --git a/ui/app/mainui/SplashScreen.qml b/ui/app/mainui/SplashScreen.qml index 8117947cdc..d6513c3491 100644 --- a/ui/app/mainui/SplashScreen.qml +++ b/ui/app/mainui/SplashScreen.qml @@ -3,26 +3,49 @@ import QtQuick.Layouts 1.15 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 import utils 1.0 -import shared 1.0 Item { property alias text: loadingText.text + property alias secondaryText: secondaryText.text + property alias progress: progressBar.value + ColumnLayout { anchors.centerIn: parent - LoadingAnimation { + spacing: 20 + Image { objectName: "loadingAnimation" Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: 128 - Layout.preferredHeight: 128 - source: Theme.png("status-logo") + Layout.preferredWidth: 270 + Layout.preferredHeight: 260 + source: Theme.png("status-preparing") } StatusBaseText { id: loadingText - Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 12 + Layout.fillWidth: true horizontalAlignment: Qt.AlignHCenter - text: qsTr("Loading Status...") + font.pixelSize: 22 + font.bold: true + text: qsTr("Preparing Status for you") + } + StatusBaseText { + id: secondaryText + Layout.fillWidth: true + horizontalAlignment: Qt.AlignHCenter + color: Theme.palette.baseColor1 + text: qsTr("Hang in there! Just a few more seconds!") + } + StatusProgressBar { + id: progressBar + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 200 + Layout.preferredHeight: 4 + Layout.bottomMargin: 100 + fillColor: Theme.palette.primaryColor1 + opacity: progress > 0 ? 1 : 0 } } } diff --git a/ui/imports/shared/controls/GetSyncCodeDesktopInstructions.qml b/ui/imports/shared/controls/GetSyncCodeDesktopInstructions.qml index 3284528764..60ab2dfb77 100644 --- a/ui/imports/shared/controls/GetSyncCodeDesktopInstructions.qml +++ b/ui/imports/shared/controls/GetSyncCodeDesktopInstructions.qml @@ -71,7 +71,7 @@ Column { if (root.type === SyncingCodeInstructions.Type.EncryptedKey) { return qsTr("Copy the") } - return qsTr("Enable camera") + return qsTr("Enable camera access") } return qsTr("Click") } @@ -122,7 +122,7 @@ Column { } return "" } - return qsTr("Enable camera") + return qsTr("Enable camera access") } text2Color: Theme.palette.directColor1 text3: { diff --git a/ui/imports/shared/controls/StatusSyncCodeInput.qml b/ui/imports/shared/controls/StatusSyncCodeInput.qml index 18d145e07f..07fe63910e 100644 --- a/ui/imports/shared/controls/StatusSyncCodeInput.qml +++ b/ui/imports/shared/controls/StatusSyncCodeInput.qml @@ -1,5 +1,6 @@ import QtQuick 2.15 +import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Controls 0.1 @@ -25,11 +26,12 @@ StatusInput { switch (root.mode) { case StatusSyncCodeInput.Mode.WriteMode: return root.valid ? validCodeIconComponent - : pasteButtonComponent + : ClipboardUtils.hasText ? pasteButtonComponent : null case StatusSyncCodeInput.Mode.ReadMode: return copyButtonComponent } } + rightPadding: 12 Component { id: copyButtonComponent @@ -38,11 +40,7 @@ StatusInput { objectName: "syncCodeCopyButton" size: StatusBaseButton.Size.Tiny text: qsTr("Copy") - onClicked: { - root.input.edit.selectAll(); - root.input.edit.copy(); - root.input.edit.deselect(); - } + onClicked: ClipboardUtils.setText(root.text) } } @@ -52,12 +50,9 @@ StatusInput { StatusButton { objectName: "syncCodePasteButton" size: StatusBaseButton.Size.Tiny - enabled: !root.readOnly && root.input.edit.canPaste + enabled: !root.readOnly && ClipboardUtils.hasText text: qsTr("Paste") - onClicked: { - root.input.edit.selectAll(); - root.input.edit.paste(); - } + onClicked: root.input.text = ClipboardUtils.text } } diff --git a/ui/imports/shared/controls/StatusSyncCodeScan.qml b/ui/imports/shared/controls/StatusSyncCodeScan.qml index 2266a29d7e..2e840bb7e6 100644 --- a/ui/imports/shared/controls/StatusSyncCodeScan.qml +++ b/ui/imports/shared/controls/StatusSyncCodeScan.qml @@ -158,17 +158,6 @@ Column { text: d.errorMessage } - StatusBaseText { - visible: !d.showCamera - width: parent.width - height: visible ? implicitHeight : 0 - wrapMode: Text.WordWrap - color: Theme.palette.baseColor1 - font.pixelSize: Theme.tertiaryTextFontSize - horizontalAlignment: Text.AlignHCenter - text: qsTr("Ensure both devices are on the same network") - } - StatusBaseText { visible: d.showCamera && cameraLoader.item.camera ? true : false width: parent.width diff --git a/ui/imports/shared/panels/DidYouKnowSplashScreen.qml b/ui/imports/shared/panels/DidYouKnowSplashScreen.qml index bd3319bcee..7a20faae44 100644 --- a/ui/imports/shared/panels/DidYouKnowSplashScreen.qml +++ b/ui/imports/shared/panels/DidYouKnowSplashScreen.qml @@ -1,7 +1,6 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 -import QtQml 2.15 import StatusQ.Core.Theme 0.1 import StatusQ.Core 0.1 @@ -13,79 +12,68 @@ import shared.panels.private 1.0 Pane { id: root - property alias progress: progressBar.value + + property alias progress: splashScreen.progress property alias splashScreenText: splashScreen.text + property alias splashScreenSecondaryText: splashScreen.secondaryText + property bool messagesEnabled contentItem: Item { - SplashScreen { - id: splashScreen - objectName: "didYouKnowSplashScreen" - anchors.centerIn: parent - width: 128 - height: 128 + SplashScreen { + id: splashScreen + objectName: "didYouKnowSplashScreen" + anchors.centerIn: parent + } + ColumnLayout { + id: content + anchors.bottom: parent.bottom + width: parent.width + visible: root.progress !== 0 && root.messagesEnabled + Behavior on visible { + SequentialAnimation { + PropertyAction { target: content; property: "opacity"; value: visible ? 0 : didYouKnowText.opacity } //set opacity to 0 if the visible property changed to true + PropertyAction { } //set visible property + NumberAnimation { target: content; property: "opacity"; duration: 1000; to: visible ? 1 : didYouKnowText.opacity } //fade in + } } - ColumnLayout { - id: content - anchors.top: splashScreen.bottom - anchors.bottom: parent.bottom - width: parent.width - visible: root.progress !== 0 - Behavior on visible { - SequentialAnimation { - PropertyAction { target: content; property: "opacity"; value: visible ? 0 : didYouKnowText.opacity } //set opacity to 0 if the visible property changed to true - PropertyAction { } //set visible property - NumberAnimation { target: content; property: "opacity"; duration: 1000; to: visible ? 1 : didYouKnowText.opacity } //fade in - } - } - Item { - Layout.fillHeight: true - } - StatusBaseText { - id: didYouKnow - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: 12 - color: Theme.palette.primaryColor1 - font.weight: Font.DemiBold - font.pixelSize: Theme.asideTextFontSize - text: qsTr("DID YOU KNOW?") - } - StatusBaseText { - id: didYouKnowText - Layout.alignment: Qt.AlignHCenter - Layout.preferredHeight: 60 - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - color: Theme.palette.directColor1 - font.pixelSize: Theme.additionalTextSize - text: didYouKnowMessages.iterator.next() - Behavior on text { - SequentialAnimation { - NumberAnimation { target: didYouKnowText; properties: "opacity"; duration: 150; to: 0 } //fade out - PropertyAction { } //change text - NumberAnimation { target: didYouKnowText; properties: "opacity"; duration: 150; to: 1; }//fade in - } - } - DidYouKnowMessages { - id: didYouKnowMessages - } - Timer { - id: didYouKnowTimer - interval: 7000 - repeat: true - running: didYouKnowText.visible - onTriggered: didYouKnowText.text = didYouKnowMessages.iterator.next() - } - } + StatusBaseText { + id: didYouKnow + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: 12 + color: Theme.palette.primaryColor1 + font.weight: Font.DemiBold + font.pixelSize: Theme.asideTextFontSize + text: qsTr("DID YOU KNOW?") - StatusProgressBar { - id: progressBar - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: 200 - Layout.preferredHeight: 4 - Layout.bottomMargin: 100 - fillColor: Theme.palette.primaryColor1 + } + StatusBaseText { + id: didYouKnowText + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: 60 + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + color: Theme.palette.directColor1 + font.pixelSize: Theme.additionalTextSize + text: didYouKnowMessages.iterator.next() + Behavior on text { + SequentialAnimation { + NumberAnimation { target: didYouKnowText; properties: "opacity"; duration: 150; to: 0 } //fade out + PropertyAction { } //change text + NumberAnimation { target: didYouKnowText; properties: "opacity"; duration: 150; to: 1; }//fade in + } } + DidYouKnowMessages { + id: didYouKnowMessages + } + Timer { + id: didYouKnowTimer + interval: 7000 + repeat: true + running: didYouKnowText.visible + onTriggered: didYouKnowText.text = didYouKnowMessages.iterator.next() + } + } } } background: Rectangle { diff --git a/ui/imports/shared/panels/EnterSeedPhrase.qml b/ui/imports/shared/panels/EnterSeedPhrase.qml index 5e42aaaef8..4c0b6bebb9 100644 --- a/ui/imports/shared/panels/EnterSeedPhrase.qml +++ b/ui/imports/shared/panels/EnterSeedPhrase.qml @@ -19,7 +19,6 @@ ColumnLayout { //* This component is not refactored, just pulled out to a shared location * //************************************************************************** spacing: Theme.padding - clip: true readonly property bool seedPhraseIsValid: d.allEntriesValid && invalidSeedTxt.text === "" property var isSeedPhraseValid: function (mnemonic) { return false } @@ -30,8 +29,8 @@ ColumnLayout { function setWrongSeedPhraseMessage(message) { invalidSeedTxt.text = message - // Validate again the seed phrase - // This is needed because the message can be set to empty and the seed phrase is still invalid + // Validate again the recovery phrase + // This is needed because the message can be set to empty and the recovery phrase is still invalid if (message === "") d.validate() } @@ -45,7 +44,7 @@ ColumnLayout { property bool allEntriesValid: false property var mnemonicInput: [] - property var incorrectWordAtIndex: [] + property var incorrectWordAtIndex: [] // 1-based readonly property var tabs: [12, 18, 24] readonly property alias seedPhrases_en: root.dictionary @@ -57,7 +56,7 @@ ColumnLayout { if (d.allEntriesValid) { mnemonicString = buildMnemonicString() if (!Utils.isMnemonic(mnemonicString) || !root.isSeedPhraseValid(mnemonicString)) { - root.setWrongSeedPhraseMessage(qsTr("Invalid seed phrase")) + root.setWrongSeedPhraseMessage(qsTr("Invalid recovery phrase")) d.allEntriesValid = false } } @@ -86,7 +85,7 @@ ColumnLayout { if (word !== "" && !ModelUtils.contains(d.seedPhrases_en, "seedWord", word)) { const incorrectWordAtIndex = d.incorrectWordAtIndex incorrectWordAtIndex.push(pos) - d.incorrectWordAtIndex = incorrectWordAtIndex + d.incorrectWordAtIndex = [...new Set(incorrectWordAtIndex)] // remove dupes return } @@ -151,7 +150,8 @@ ColumnLayout { Repeater { model: d.tabs StatusSwitchTabButton { - text: qsTr("%n word(s)", "", modelData) + readonly property int wordCount: modelData + text: qsTr("%n word(s)", "", wordCount) id: seedPhraseWords objectName: `${modelData}SeedButton` } @@ -180,12 +180,13 @@ ColumnLayout { objectName: "enterSeedPhraseGridView" Layout.fillWidth: true Layout.preferredHeight: 312 - clip: false + Layout.topMargin: Theme.halfPadding + Layout.alignment: Qt.AlignHCenter flow: GridView.FlowTopToBottom cellWidth: (parent.width/(count/6)) cellHeight: 52 interactive: false - model: switchTabBar.currentItem.text.substring(0,2) + model: switchTabBar.currentItem.wordCount function addWord(pos, word, ignoreGoingNext = false) { @@ -240,7 +241,7 @@ ColumnLayout { textEdit.input.edit.objectName: `enterSeedPhraseInputField${seedWordInput.leftComponentText}` width: (grid.cellWidth - 8) height: (grid.cellHeight - 8) - Behavior on width { NumberAnimation { duration: 180 } } + Behavior on width { NumberAnimation { duration: 150 } } textEdit.text: { const pos = seedWordInput.mnemonicIndex for (let i in d.mnemonicInput) { @@ -252,12 +253,13 @@ ColumnLayout { return "" } + readonly property int itemIndex: index readonly property int mnemonicIndex: grid.wordIndex[(grid.count / 6) - 2][index] leftComponentText: mnemonicIndex + isError: d.incorrectWordAtIndex.includes(mnemonicIndex) & !!text inputList: d.seedPhrases_en - property int itemIndex: index onDoneInsertingWord: { grid.addWord(mnemonicIndex, word) } @@ -270,7 +272,6 @@ ColumnLayout { } onEditClicked: { grid.currentIndex = index - grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus() } onKeyPressed: { grid.currentIndex = index diff --git a/ui/imports/shared/panels/SeedPhrase.qml b/ui/imports/shared/panels/SeedPhrase.qml index af4bd958ef..d8d0640255 100644 --- a/ui/imports/shared/panels/SeedPhrase.qml +++ b/ui/imports/shared/panels/SeedPhrase.qml @@ -53,7 +53,7 @@ Item { visible: !root.seedPhraseRevealed type: StatusBaseButton.Type.Primary icon.name: "view" - text: qsTr("Reveal seed phrase") + text: qsTr("Reveal recovery phrase") onClicked: { root.seedPhraseRevealed = true } diff --git a/ui/imports/shared/popups/GetSyncCodeInstructionsPopup.qml b/ui/imports/shared/popups/GetSyncCodeInstructionsPopup.qml index 80e5e95c8e..1807f3f6bc 100644 --- a/ui/imports/shared/popups/GetSyncCodeInstructionsPopup.qml +++ b/ui/imports/shared/popups/GetSyncCodeInstructionsPopup.qml @@ -1,4 +1,4 @@ -import QtQuick 2.14 +import QtQuick 2.15 import StatusQ.Popups.Dialog 0.1 @@ -7,7 +7,7 @@ import shared.views 1.0 StatusDialog { id: root - title: qsTr("How to get a sync code on...") + title: qsTr("How to get a pairing code on...") horizontalPadding: 24 verticalPadding: 32 footer: null diff --git a/ui/imports/shared/popups/addaccount/AddAccountPopup.qml b/ui/imports/shared/popups/addaccount/AddAccountPopup.qml index 3afc8bc9fb..88349ea325 100644 --- a/ui/imports/shared/popups/addaccount/AddAccountPopup.qml +++ b/ui/imports/shared/popups/addaccount/AddAccountPopup.qml @@ -325,9 +325,9 @@ StatusModal { case Constants.addAccountPopup.state.enterKeypairName: return qsTr("Continue") case Constants.addAccountPopup.state.confirmAddingNewMasterKey: - return qsTr("Reveal seed phrase") + return qsTr("Reveal recovery phrase") case Constants.addAccountPopup.state.displaySeedPhrase: - return qsTr("Confirm seed phrase") + return qsTr("Confirm recovery phrase") } return "" diff --git a/ui/imports/shared/popups/addaccount/panels/SelectOrigin.qml b/ui/imports/shared/popups/addaccount/panels/SelectOrigin.qml index 87ac5e341f..dfd85b6c74 100644 --- a/ui/imports/shared/popups/addaccount/panels/SelectOrigin.qml +++ b/ui/imports/shared/popups/addaccount/panels/SelectOrigin.qml @@ -86,7 +86,7 @@ StatusSelect { subTitle: { if (menu.isOption) { if (model.keyPair.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey) - return qsTr("From Keycard, private key or seed phrase") + return qsTr("From Keycard, private key or recovery phrase") if (model.keyPair.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc) return qsTr("Any ETH address") } diff --git a/ui/imports/shared/popups/addaccount/panels/WatchOnlyAddressSection.qml b/ui/imports/shared/popups/addaccount/panels/WatchOnlyAddressSection.qml index cb53786cff..f1ab663ed3 100644 --- a/ui/imports/shared/popups/addaccount/panels/WatchOnlyAddressSection.qml +++ b/ui/imports/shared/popups/addaccount/panels/WatchOnlyAddressSection.qml @@ -119,7 +119,7 @@ Column { AddressDetails { width: parent.width addressDetailsItem: root.store.watchOnlyAccAddress - defaultMessage: qsTr("You will need to import your seed phrase or use your Keycard to transact with this account") + defaultMessage: qsTr("You will need to import your recovery phrase or use your Keycard to transact with this account") defaultMessageCondition: addressInput.text === "" || !addressInput.valid } } diff --git a/ui/imports/shared/popups/addaccount/states/ConfirmAddingNewMasterKey.qml b/ui/imports/shared/popups/addaccount/states/ConfirmAddingNewMasterKey.qml index c8bdd3da87..40f88a2b20 100644 --- a/ui/imports/shared/popups/addaccount/states/ConfirmAddingNewMasterKey.qml +++ b/ui/imports/shared/popups/addaccount/states/ConfirmAddingNewMasterKey.qml @@ -73,7 +73,7 @@ Item { textFormat: Text.RichText font.pixelSize: Theme.primaryTextFontSize lineHeight: d.lineHeight - text: qsTr("Your seed phrase is a 12-word passcode to your funds.

Your seed phrase cannot be recovered if lost. Therefore, you must back it up. The simplest way is to write it down offline and store it somewhere secure.") + text: qsTr("Your recovery phrase is a 12-word passcode to your funds.

Your recovery phrase cannot be recovered if lost. Therefore, you must back it up. The simplest way is to write it down offline and store it somewhere secure.") } StatusCheckBox { @@ -96,7 +96,7 @@ Item { Layout.alignment: Qt.AlignHCenter spacing: Theme.padding font.pixelSize: Theme.primaryTextFontSize - text: qsTr("I am ready to write down my seed phrase") + text: qsTr("I am ready to write down my recovery phrase") } StatusCheckBox { @@ -126,7 +126,7 @@ Item { wrapMode: Text.WordWrap color: Theme.palette.dangerColor1 lineHeight: d.lineHeight - text: qsTr("You can only complete this process once. Status will not store your seed phrase and can never help you recover it.") + text: qsTr("You can only complete this process once. Status will not store your recovery phrase and can never help you recover it.") } } } diff --git a/ui/imports/shared/popups/addaccount/states/ConfirmSeedPhraseBackup.qml b/ui/imports/shared/popups/addaccount/states/ConfirmSeedPhraseBackup.qml index 0d5529e116..f077644dda 100644 --- a/ui/imports/shared/popups/addaccount/states/ConfirmSeedPhraseBackup.qml +++ b/ui/imports/shared/popups/addaccount/states/ConfirmSeedPhraseBackup.qml @@ -73,7 +73,7 @@ Item { font.pixelSize: Theme.primaryTextFontSize lineHeight: 1.2 color: Theme.palette.directColor1 - text: qsTr("By completing this process, you will remove your seed phrase from this application’s storage. This makes your funds more secure.\n\nYou will remain logged in, and your seed phrase will be entirely in your hands.") + text: qsTr("By completing this process, you will remove your recovery phrase from this application’s storage. This makes your funds more secure.\n\nYou will remain logged in, and your recovery phrase will be entirely in your hands.") } StatusCheckBox { @@ -84,7 +84,7 @@ Item { Layout.alignment: Qt.AlignHCenter spacing: Theme.padding font.pixelSize: Theme.primaryTextFontSize - text: qsTr("I aknowledge that Status will not be able to show me my seed phrase again.") + text: qsTr("I acknowledge that Status will not be able to show me my recovery phrase again.") onToggled: { root.store.seedPhraseBackupConfirmed = checked } diff --git a/ui/imports/shared/popups/addaccount/states/DisplaySeedPhrase.qml b/ui/imports/shared/popups/addaccount/states/DisplaySeedPhrase.qml index 64a6ec86a0..02493128dc 100644 --- a/ui/imports/shared/popups/addaccount/states/DisplaySeedPhrase.qml +++ b/ui/imports/shared/popups/addaccount/states/DisplaySeedPhrase.qml @@ -43,7 +43,7 @@ Item { horizontalAlignment: Text.AlignHCenter font.pixelSize: Constants.addAccountPopup.labelFontSize1 color: Theme.palette.directColor1 - text: qsTr("Write down your 12-word seed phrase to keep offline") + text: qsTr("Write down your 12-word recovery phrase to keep offline") } SeedPhrase { @@ -69,7 +69,7 @@ Item { textFormat: Text.RichText wrapMode: Text.WordWrap color: Theme.palette.dangerColor1 - text: qsTr("The next screen contains your seed phrase.
Anyone who sees it can use it to access to your funds.") + text: qsTr("The next screen contains your recovery phrase.
Anyone who sees it can use it to access to your funds.") } } } diff --git a/ui/imports/shared/popups/addaccount/states/EnterSeedPhraseWord.qml b/ui/imports/shared/popups/addaccount/states/EnterSeedPhraseWord.qml index 3d9355b4e5..91d5370854 100644 --- a/ui/imports/shared/popups/addaccount/states/EnterSeedPhraseWord.qml +++ b/ui/imports/shared/popups/addaccount/states/EnterSeedPhraseWord.qml @@ -111,7 +111,7 @@ Item { horizontalAlignment: Text.AlignHCenter font.pixelSize: Constants.addAccountPopup.labelFontSize1 color: Theme.palette.directColor1 - text: qsTr("Confirm word #%1 of your seed phrase").arg(root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1? + text: qsTr("Confirm word #%1 of your recovery phrase").arg(root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1? root.store.seedPhraseWord1WordNumber + 1 : root.store.seedPhraseWord2WordNumber + 1) } diff --git a/ui/imports/shared/popups/addaccount/states/SelectMasterKey.qml b/ui/imports/shared/popups/addaccount/states/SelectMasterKey.qml index d7898cdea2..69297f8742 100644 --- a/ui/imports/shared/popups/addaccount/states/SelectMasterKey.qml +++ b/ui/imports/shared/popups/addaccount/states/SelectMasterKey.qml @@ -29,7 +29,7 @@ Item { StatusListItem { objectName: "AddAccountPopup-ImportUsingSeedPhrase" - title: qsTr("Import using seed phrase") + title: qsTr("Import using recovery phrase") asset { name: "key_pair_seed_phrase" color: Theme.palette.primaryColor1 diff --git a/ui/imports/shared/popups/common/EnterPrivateKey.qml b/ui/imports/shared/popups/common/EnterPrivateKey.qml index 99df8272fb..e4491aa16d 100644 --- a/ui/imports/shared/popups/common/EnterPrivateKey.qml +++ b/ui/imports/shared/popups/common/EnterPrivateKey.qml @@ -37,7 +37,7 @@ Item { StatusBaseText { width: parent.width - text: root.store.isAddAccountPopup? qsTr("Private key") : qsTr("Enter seed phrase for %1 key pair").arg(root.store.selectedKeypair.name) + text: root.store.isAddAccountPopup? qsTr("Private key") : qsTr("Enter recovery phrase for %1 key pair").arg(root.store.selectedKeypair.name) font.pixelSize: Constants.addAccountPopup.labelFontSize1 elide: Text.ElideRight } @@ -120,7 +120,7 @@ Item { multiline: true leftPadding: Theme.padding font.pixelSize: Constants.addAccountPopup.labelFontSize2 - text: qsTr("New addresses cannot be derived from an account imported from a private key. Import using a seed phrase if you wish to derive addresses.") + text: qsTr("New addresses cannot be derived from an account imported from a private key. Import using a recovery phrase if you wish to derive addresses.") input.edit.enabled: false input.enabled: false input.background.color: "transparent" diff --git a/ui/imports/shared/popups/common/EnterSeedPhrase.qml b/ui/imports/shared/popups/common/EnterSeedPhrase.qml index 697fff190b..4949d6e5da 100644 --- a/ui/imports/shared/popups/common/EnterSeedPhrase.qml +++ b/ui/imports/shared/popups/common/EnterSeedPhrase.qml @@ -27,7 +27,7 @@ Item { StatusBaseText { width: parent.width - text: root.store.isAddAccountPopup? qsTr("Enter seed phrase") : qsTr("Enter private key for %1 key pair").arg(root.store.selectedKeypair.name) + text: root.store.isAddAccountPopup? qsTr("Enter recovery phrase") : qsTr("Enter private key for %1 key pair").arg(root.store.selectedKeypair.name) font.pixelSize: Constants.addAccountPopup.labelFontSize1 elide: Text.ElideRight } @@ -46,9 +46,9 @@ Item { } root.store.enteredSeedPhraseIsValid = valid if (!enterSeedPhrase.isSeedPhraseValid(seedPhrase)) { - let err = qsTr("The entered seed phrase is already added") + let err = qsTr("The entered recovery phrase is already added") if (!root.store.isAddAccountPopup) { - err = qsTr("This is not the correct seed phrase for %1 key").arg(root.store.selectedKeypair.name) + err = qsTr("This is not the correct recovery phrase for %1 key").arg(root.store.selectedKeypair.name) } enterSeedPhrase.setWrongSeedPhraseMessage(err) } diff --git a/ui/imports/shared/popups/keycard/KeycardPopup.qml b/ui/imports/shared/popups/keycard/KeycardPopup.qml index 9ea4a9f342..be365535ee 100644 --- a/ui/imports/shared/popups/keycard/KeycardPopup.qml +++ b/ui/imports/shared/popups/keycard/KeycardPopup.qml @@ -28,9 +28,9 @@ StatusModal { case Constants.keycardSharedFlow.setupNewKeycard: return qsTr("Set up a new Keycard with an existing account") case Constants.keycardSharedFlow.setupNewKeycardNewSeedPhrase: - return qsTr("Create a new Keycard account with a new seed phrase") + return qsTr("Create a new Keycard account with a new recovery phrase") case Constants.keycardSharedFlow.setupNewKeycardOldSeedPhrase: - return qsTr("Import or restore a Keycard via a seed phrase") + return qsTr("Import or restore a Keycard via a recovery phrase") case Constants.keycardSharedFlow.importFromKeycard: return qsTr("Migrate account from Keycard to Status") case Constants.keycardSharedFlow.factoryReset: diff --git a/ui/imports/shared/popups/keycard/KeycardPopupDetails.qml b/ui/imports/shared/popups/keycard/KeycardPopupDetails.qml index ea78f3fd55..b179bdd42a 100644 --- a/ui/imports/shared/popups/keycard/KeycardPopupDetails.qml +++ b/ui/imports/shared/popups/keycard/KeycardPopupDetails.qml @@ -659,7 +659,7 @@ QtObject { case Constants.keycardSharedState.createPin: case Constants.keycardSharedState.repeatPin: case Constants.keycardSharedState.pinSet: - return qsTr("Input seed phrase") + return qsTr("Input recovery phrase") case Constants.keycardSharedState.seedPhraseEnterWords: return qsTr("Yes, migrate key pair to this Keycard") @@ -668,7 +668,7 @@ QtObject { return qsTr("Yes, migrate key pair to Keycard") case Constants.keycardSharedState.wrongSeedPhrase: - return qsTr("Try entering seed phrase again") + return qsTr("Try entering recovery phrase again") case Constants.keycardSharedState.keycardNotEmpty: return qsTr("Check what is stored on this Keycard") @@ -950,7 +950,7 @@ QtObject { // to run unlock flow directly. return "" } - return qsTr("Unlock using seed phrase") + return qsTr("Unlock using recovery phrase") case Constants.keycardSharedState.createPin: case Constants.keycardSharedState.repeatPin: @@ -961,7 +961,7 @@ QtObject { return qsTr("Next") case Constants.keycardSharedState.wrongSeedPhrase: - return qsTr("Try entering seed phrase again") + return qsTr("Try entering recovery phrase again") case Constants.keycardSharedState.maxPukRetriesReached: if (root.sharedKeycardModule.keycardData & Constants.predefinedKeycardData.disableSeedPhraseForUnlock) { @@ -1123,7 +1123,7 @@ QtObject { return qsTr("Done") case Constants.keycardSharedState.wrongSeedPhrase: - return qsTr("Try entering seed phrase again") + return qsTr("Try entering recovery phrase again") case Constants.keycardSharedState.maxPinRetriesReached: case Constants.keycardSharedState.maxPukRetriesReached: diff --git a/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml b/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml index 9c862ccc3e..dcc91ff32b 100644 --- a/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml +++ b/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml @@ -18,7 +18,7 @@ Item { property bool wrongSeedPhrase: root.sharedKeycardModule.keycardData & Constants.predefinedKeycardData.wrongSeedPhrase onWrongSeedPhraseChanged: { - seedPhrase.setWrongSeedPhraseMessage(wrongSeedPhrase? qsTr("The phrase you’ve entered does not match this Keycard’s seed phrase") : "") + seedPhrase.setWrongSeedPhraseMessage(wrongSeedPhrase? qsTr("The phrase you’ve entered does not match this Keycard’s recovery phrase") : "") } } @@ -74,7 +74,7 @@ Item { text: { switch (root.sharedKeycardModule.currentState.flowType) { case Constants.keycardSharedFlow.migrateFromKeycardToApp: - return qsTr("Enter seed phrase for %1 key pair").arg(root.sharedKeycardModule.keyPairForProcessing.name) + return qsTr("Enter recovery phrase for %1 key pair").arg(root.sharedKeycardModule.keyPairForProcessing.name) } return "" @@ -89,7 +89,7 @@ Item { text: { switch (root.sharedKeycardModule.currentState.flowType) { case Constants.keycardSharedFlow.migrateFromKeycardToApp: - return qsTr("Enter seed phrase for %1 key pair").arg(root.sharedKeycardModule.keyPairForProcessing.name) + return qsTr("Enter recovery phrase for %1 key pair").arg(root.sharedKeycardModule.keyPairForProcessing.name) } return "" diff --git a/ui/imports/shared/popups/keycard/states/EnterSeedPhraseWords.qml b/ui/imports/shared/popups/keycard/states/EnterSeedPhraseWords.qml index 0686cca146..b341aff4e7 100644 --- a/ui/imports/shared/popups/keycard/states/EnterSeedPhraseWords.qml +++ b/ui/imports/shared/popups/keycard/states/EnterSeedPhraseWords.qml @@ -62,7 +62,7 @@ Item { id: title Layout.preferredHeight: Constants.keycard.general.titleHeight Layout.alignment: Qt.AlignHCenter - text: qsTr("Confirm seed phrase words") + text: qsTr("Confirm recovery phrase words") font.pixelSize: Constants.keycard.general.fontSize1 font.weight: Font.Bold color: Theme.palette.directColor1 diff --git a/ui/imports/shared/popups/keycard/states/KeycardInit.qml b/ui/imports/shared/popups/keycard/states/KeycardInit.qml index 2bf7da129a..e2240df2ed 100644 --- a/ui/imports/shared/popups/keycard/states/KeycardInit.qml +++ b/ui/imports/shared/popups/keycard/states/KeycardInit.qml @@ -1401,7 +1401,7 @@ Item { target: title text: { if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycardOldSeedPhrase) { - return qsTr("This seed phrase has already been imported") + return qsTr("This recovery phrase has already been imported") } if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.importFromKeycard) { return qsTr("This keycard has already been imported") @@ -1459,7 +1459,7 @@ Item { if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.migrateKeypairToApp) { if (root.sharedKeycardModule.keyPairForProcessing.pairType === Constants.keycard.keyPairType.profile) { if (root.sharedKeycardModule.forceFlow) { - return qsTr("In order to continue using this profile on this device, you need to enter the key pairs seed phrase and create a new password to log in with on this device.") + return qsTr("In order to continue using this profile on this device, you need to enter the key pairs recovery phrase and create a new password to log in with on this device.") } let t = qsTr("%1 is your default Status key pair.").arg(root.sharedKeycardModule.keyPairForProcessing.name) diff --git a/ui/imports/shared/popups/keycard/states/SeedPhrase.qml b/ui/imports/shared/popups/keycard/states/SeedPhrase.qml index 40211437f8..22b0cacfb6 100644 --- a/ui/imports/shared/popups/keycard/states/SeedPhrase.qml +++ b/ui/imports/shared/popups/keycard/states/SeedPhrase.qml @@ -45,7 +45,7 @@ Item { Layout.preferredWidth: parent.width Layout.fillHeight: true - property var seedPhrase: root.sharedKeycardModule.getMnemonic().split(" ") + seedPhrase: root.sharedKeycardModule.getMnemonic().split(" ") } } @@ -55,14 +55,14 @@ Item { when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseDisplay PropertyChanges { target: title - text: qsTr("Write down your seed phrase") + text: qsTr("Write down your recovery phrase") font.pixelSize: Constants.keycard.general.fontSize1 font.weight: Font.Bold color: Theme.palette.directColor1 } PropertyChanges { target: message - text: qsTr("The next screen contains your seed phrase.
Anyone who sees it can use it to access to your funds.") + text: qsTr("The next screen contains your recovery phrase.
Anyone who sees it can use it to access to your funds.") font.pixelSize: Constants.keycard.general.fontSize2 wrapMode: Text.WordWrap textFormat: Text.RichText diff --git a/ui/imports/shared/popups/keypairimport/states/SelectImportMethod.qml b/ui/imports/shared/popups/keypairimport/states/SelectImportMethod.qml index e7759d033a..e5dc10bee3 100644 --- a/ui/imports/shared/popups/keypairimport/states/SelectImportMethod.qml +++ b/ui/imports/shared/popups/keypairimport/states/SelectImportMethod.qml @@ -62,7 +62,7 @@ Item { StatusListItem { title: root.store.selectedKeypair.pairType === Constants.keypair.type.seedImport? - qsTr("Import via entering seed phrase") : + qsTr("Import via entering recovery phrase") : qsTr("Import via entering private key") asset { diff --git a/ui/imports/shared/stores/BIP39_en.qml b/ui/imports/shared/stores/BIP39_en.qml index 7fb49b05b2..8d312b43aa 100644 --- a/ui/imports/shared/stores/BIP39_en.qml +++ b/ui/imports/shared/stores/BIP39_en.qml @@ -1,4 +1,4 @@ -import QtQuick 2.13 +import QtQuick 2.15 import StatusQ.Core.Utils 0.1 @@ -6,11 +6,11 @@ ListModel { id: root Component.onCompleted: { - const words = StringUtils.readTextFile(":/imports/shared/stores/english.txt").split(/\r?\n|\r/); + const words = StringUtils.readTextFile(":/imports/shared/stores/english.txt").split(/\r?\n|\r/) for (var i = 0; i < words.length; i++) { let word = words[i] if (word !== "") { - insert(count, {"seedWord": word}); + append({"seedWord": word}) } } } diff --git a/ui/imports/shared/views/PasswordComponentIndicator.qml b/ui/imports/shared/views/PasswordComponentIndicator.qml new file mode 100644 index 0000000000..9962c96c1b --- /dev/null +++ b/ui/imports/shared/views/PasswordComponentIndicator.qml @@ -0,0 +1,13 @@ +import QtQuick 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +StatusBaseText { + property bool checked + property string caption + + text: "%1 %2".arg(checked ? "✓" : "+").arg(caption) + font.pixelSize: Theme.tertiaryTextFontSize + color: checked ? Theme.palette.successColor1 : Theme.palette.baseColor1 +} diff --git a/ui/imports/shared/views/PasswordView.qml b/ui/imports/shared/views/PasswordView.qml index ed5fc8b875..05823ba5a8 100644 --- a/ui/imports/shared/views/PasswordView.qml +++ b/ui/imports/shared/views/PasswordView.qml @@ -32,7 +32,7 @@ ColumnLayout { property int contentAlignment: Qt.AlignHCenter - property var passwordStrengthScoreFunction: function () {} + property var passwordStrengthScoreFunction: (password) => { console.error("passwordStrengthScoreFunction: IMPLEMENT ME") } readonly property int zBehind: 1 readonly property int zFront: 100 @@ -79,11 +79,6 @@ ColumnLayout { QtObject { id: d - property bool containsLower: false - property bool containsUpper: false - property bool containsNumbers: false - property bool containsSymbols: false - readonly property var validatorRegexp: /^[!-~]+$/ readonly property string validatorErrMessage: qsTr("Only ASCII letters, numbers, and symbols are allowed") readonly property string passTooLongErrMessage: qsTr("Maximum %n character(s)", "", Constants.maxPasswordLength) @@ -244,7 +239,7 @@ ColumnLayout { Layout.alignment: root.contentAlignment StatusBaseText { - text: qsTr("New password") + text: qsTr("Choose password") } StatusPasswordInput { @@ -255,7 +250,7 @@ ColumnLayout { Layout.alignment: root.contentAlignment Layout.fillWidth: true - placeholderText: qsTr("Enter new password") + placeholderText: qsTr("Type password") echoMode: showPassword ? TextInput.Normal : TextInput.Password rightPadding: showHideNewIcon.width + showHideNewIcon.anchors.rightMargin + Theme.padding / 2 @@ -265,11 +260,6 @@ ColumnLayout { // Update strength indicator: strengthInditactor.strength = d.convertStrength(root.passwordStrengthScoreFunction(newPswInput.text)) - d.containsLower = d.lowerCaseValidator(text) - d.containsUpper = d.upperCaseValidator(text) - d.containsNumbers = d.numbersValidator(text) - d.containsSymbols = d.symbolsValidator(text) - if(!d.validateCharacterSet(text)) return if (text.length === confirmPswInput.text.length) { @@ -292,87 +282,11 @@ ColumnLayout { onClicked: newPswInput.showPassword = !newPswInput.showPassword } } - - StatusPasswordStrengthIndicator { - id: strengthInditactor - Layout.fillWidth: true - value: Math.min(Constants.minPasswordLength, newPswInput.text.length) - from: 0 - to: Constants.minPasswordLength - labelVeryWeak: qsTr("Very weak") - labelWeak: qsTr("Weak") - labelSoso: qsTr("So-so") - labelGood: qsTr("Good") - labelGreat: qsTr("Great") - } - } - - Rectangle { - Layout.fillWidth: true - Layout.minimumHeight: 80 - border.color: Theme.palette.baseColor2 - border.width: 1 - color: "transparent" - radius: Theme.radius - implicitHeight: strengthColumn.implicitHeight - implicitWidth: strengthColumn.implicitWidth - - ColumnLayout { - id: strengthColumn - anchors.fill: parent - anchors.margins: Theme.padding - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.padding - - StatusBaseText { - id: strengthenTxt - Layout.fillHeight: true - Layout.alignment: Qt.AlignHCenter - wrapMode: Text.WordWrap - text: root.strengthenText - font.pixelSize: 12 - color: Theme.palette.baseColor1 - clip: true - } - - RowLayout { - spacing: Theme.padding - Layout.alignment: Qt.AlignHCenter - - StatusBaseText { - id: lowerCaseTxt - text: "• " + qsTr("Lower case") - font.pixelSize: 12 - color: d.containsLower ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } - - StatusBaseText { - id: upperCaseTxt - text: "• " + qsTr("Upper case") - font.pixelSize: 12 - color: d.containsUpper ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } - - StatusBaseText { - id: numbersTxt - text: "• " + qsTr("Numbers") - font.pixelSize: 12 - color: d.containsNumbers ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } - - StatusBaseText { - id: symbolsTxt - text: "• " + qsTr("Symbols") - font.pixelSize: 12 - color: d.containsSymbols ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } - } - } } ColumnLayout { StatusBaseText { - text: qsTr("Confirm new password") + text: qsTr("Repeat password") } StatusPasswordInput { @@ -384,7 +298,7 @@ ColumnLayout { z: root.zFront Layout.fillWidth: true Layout.alignment: root.contentAlignment - placeholderText: qsTr("Enter new password") + placeholderText: qsTr("Type password") echoMode: showPassword ? TextInput.Normal : TextInput.Password rightPadding: showHideConfirmIcon.width + showHideConfirmIcon.anchors.rightMargin + Theme.padding / 2 @@ -427,11 +341,44 @@ ColumnLayout { } } + StatusPasswordStrengthIndicator { + id: strengthInditactor + Layout.fillWidth: true + value: Math.min(Constants.minPasswordLength, newPswInput.text.length) + from: 0 + to: Constants.minPasswordLength + } + + RowLayout { + Layout.fillWidth: true + spacing: Theme.padding + Layout.alignment: Qt.AlignHCenter + + PasswordComponentIndicator { + caption: qsTr("Lower case") + checked: d.lowerCaseValidator(newPswInput.text) + } + + PasswordComponentIndicator { + caption: qsTr("Upper case") + checked: d.upperCaseValidator(newPswInput.text) + } + + PasswordComponentIndicator { + caption: qsTr("Numbers") + checked: d.numbersValidator(newPswInput.text) + } + + PasswordComponentIndicator { + caption: qsTr("Symbols") + checked: d.symbolsValidator(newPswInput.text) + } + } + StatusBaseText { id: errorTxt Layout.alignment: root.contentAlignment - Layout.fillHeight: true - font.pixelSize: 12 + font.pixelSize: Theme.tertiaryTextFontSize color: Theme.palette.dangerColor1 } } diff --git a/ui/imports/shared/views/SyncingEnterCode.qml b/ui/imports/shared/views/SyncingEnterCode.qml index aae94bf460..050d60b164 100644 --- a/ui/imports/shared/views/SyncingEnterCode.qml +++ b/ui/imports/shared/views/SyncingEnterCode.qml @@ -1,5 +1,5 @@ -import QtQuick 2.14 -import QtQuick.Layouts 1.14 +import QtQuick 2.15 +import QtQuick.Layouts 1.15 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 @@ -13,48 +13,56 @@ ColumnLayout { id: root property string firstTabName: qsTr("Scan QR code") - property string secondTabName: qsTr("Enter sync code") - property string firstInstructionButtonName: qsTr("How to get a sync code") - property string secondInstructionButtonName: qsTr("How to get a sync code") - property string syncQrErrorMessage: qsTr("This does not look like a sync QR code") - property string syncCodeErrorMessage: qsTr("This does not look like a sync code") - property string syncCodeLabel: qsTr("Paste sync code") + property string secondTabName: qsTr("Enter code") + property string firstInstructionButtonName: qsTr("How to get a pairing code") + property string secondInstructionButtonName: qsTr("How to get a pairing code") + property string syncQrErrorMessage: qsTr("This does not look like a pairing QR code") + property string syncCodeErrorMessage: qsTr("This does not look like a pairing code") + property string syncCodeLabel: qsTr("Type or paste pairing code") + property alias showBetaTag: betaTag.visible - property var validateConnectionString: function(stringValue) { return true } + property var validateConnectionString: function(stringValue) { return false } - readonly property bool syncViaQr: !switchTabBar.currentIndex + readonly property bool syncViaQr: !switchTabBar.currentIndex signal displayInstructions() signal proceed(string connectionString) - spacing: 8 + spacing: Theme.halfPadding - StatusSwitchTabBar { - id: switchTabBar + RowLayout { + spacing: root.spacing Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - currentIndex: 0 + Layout.leftMargin: Theme.bigPadding + Layout.rightMargin: Theme.bigPadding - StatusSwitchTabButton { - text: root.firstTabName + StatusSwitchTabBar { + Layout.fillWidth: true + Layout.leftMargin: betaTag.visible ? betaTag.width : 0 + id: switchTabBar + + currentIndex: 0 + + StatusSwitchTabButton { + objectName: "firstTab_StatusSwitchTabButton" + text: root.firstTabName + } + + StatusSwitchTabButton { + objectName: "secondTab_StatusSwitchTabButton" + text: root.secondTabName + } } - StatusSwitchTabButton { - text: root.secondTabName + StatusBetaTag { + id: betaTag } } - StatusBetaTag { - anchors.left: switchTabBar.right - anchors.leftMargin: 8 - anchors.verticalCenter: switchTabBar.verticalCenter - } - StackLayout { Layout.fillWidth: true Layout.preferredHeight: Math.max(syncQr.implicitHeight, syncCode.implicitHeight) - Layout.topMargin: 24 + Layout.topMargin: Theme.bigPadding currentIndex: switchTabBar.currentIndex // StackLayout doesn't support alignment, so we create an `Item` wrappers @@ -80,11 +88,13 @@ ColumnLayout { } ColumnLayout { - spacing: 20 + Layout.topMargin: Theme.padding + spacing: Theme.padding StatusSyncCodeInput { + objectName: "syncCodeInput" id: syncCode Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: 424 + Layout.preferredWidth: 440 mode: StatusSyncCodeInput.Mode.WriteMode label: root.syncCodeLabel @@ -97,30 +107,37 @@ ColumnLayout { validate: root.validateConnectionString } ] - input.onValidChanged: { - if (!input.valid) - return - root.proceed(syncCode.text) - } } StatusBaseText { Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter horizontalAlignment: Text.AlignHCenter color: Theme.palette.baseColor1 font.pixelSize: Theme.tertiaryTextFontSize - text: qsTr("Ensure both devices are on the same network") + text: qsTr("Ensure both devices are on the same local network") + } + StatusButton { + objectName: "continue_StatusButton" + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Theme.padding + text: qsTr("Continue") + enabled: syncCode.input.valid + onClicked: root.proceed(syncCode.text) } } } StatusFlatButton { + Layout.topMargin: Theme.xlPadding Layout.alignment: Qt.AlignHCenter visible: switchTabBar.currentIndex == 0 && !!root.firstInstructionButtonName || switchTabBar.currentIndex == 1 && !!root.secondInstructionButtonName text: switchTabBar.currentIndex == 0? root.firstInstructionButtonName : root.secondInstructionButtonName + font.pixelSize: Theme.additionalTextSize + normalColor: "transparent" + borderWidth: 1 + borderColor: Theme.palette.baseColor2 onClicked: { root.displayInstructions() } diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 2694af97c9..7a6cd3e138 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -1349,6 +1349,7 @@ QtObject { readonly property string welcome: "welcome_view" readonly property string privacyAndSecurity: "privacy_and_security_view" readonly property string startApp: "start_app_after_upgrade" + readonly property string onboarding: "onboarding" } enum MutingVariations { diff --git a/ui/imports/utils/Global.qml b/ui/imports/utils/Global.qml index 072ef21846..61a478ac91 100644 --- a/ui/imports/utils/Global.qml +++ b/ui/imports/utils/Global.qml @@ -86,6 +86,7 @@ QtObject { signal openTestnetPopup() signal privacyPolicyRequested() + signal termsOfUseRequested() signal openPaymentRequestModalRequested(var callback) diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index 6ca427e217..e6daa6d68d 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -78,11 +78,11 @@ QtObject { `${link}` } - function getStyledLink(linkText, linkUrl, hoveredLink, textColor = Theme.palette.directColor1, linkColor = Theme.palette.primaryColor1) { + function getStyledLink(linkText, linkUrl, hoveredLink, textColor = Theme.palette.directColor1, linkColor = Theme.palette.primaryColor1, underlineLink = true) { return `` +