diff --git a/storybook/pages/OnboardingLayoutPage.qml b/storybook/pages/OnboardingLayoutPage.qml index 93fc82aee9..369117c67a 100644 --- a/storybook/pages/OnboardingLayoutPage.qml +++ b/storybook/pages/OnboardingLayoutPage.qml @@ -47,6 +47,8 @@ SplitView { // create keycard profile Constants.startupState.keycardEmpty ] + + readonly property string mnemonic: "dog dog dog dog dog dog dog dog dog dog dog dog" } OnboardingLayout { @@ -59,14 +61,30 @@ SplitView { } function getPasswordStrengthScore(password) { + logs.logEvent("StartupStore.getPasswordStrengthScore", ["password"], arguments) return Math.min(password.length-1, 4) } function validMnemonic(mnemonic) { - return true + logs.logEvent("StartupStore.validMnemonic", ["mnemonic"], arguments) + return mnemonic === keycardMock.mnemonic } function getPin() { + logs.logEvent("StartupStore.getPin()") return ctrlPin.text } + function getSeedPhrase() { + logs.logEvent("StartupStore.getSeedPhrase()") + // FIXME needed? cf getMnemonic() + } + + function validateLocalPairingConnectionString(connectionString) { + logs.logEvent("StartupStore.validateLocalPairingConnectionString", ["connectionString"], arguments) + return !Number.isNaN(parseInt(connectionString)) + } + function setConnectionString(connectionString) { + logs.logEvent("StartupStore.setConnectionString", ["connectionString"], arguments) + } + readonly property var startupModuleInst: QtObject { property int remainingAttempts: 5 } @@ -89,12 +107,18 @@ SplitView { readonly property var words: ["apple", "banana", "cat", "cow", "catalog", "catch", "category", "cattle", "dog", "elephant", "fish", "grape"] function getMnemonic() { + logs.logEvent("PrivacyStore.getMnemonic()") return words.join(" ") } function mnemonicWasShown() { console.warn("!!! MNEMONIC SHOWN") - logs.logEvent("mnemonicWasShown") + logs.logEvent("PrivacyStore.mnemonicWasShown()") + } + + function removeMnemonic() { + console.warn("!!! REMOVE MNEMONIC") + logs.logEvent("PrivacyStore.removeMnemonic()") } } @@ -105,9 +129,9 @@ SplitView { property bool metricsPopupSeen } - onFinished: (success, primaryPath, secondaryPath) => { - console.warn("!!! ONBOARDING FINISHED; success:", success, "; primary path:", primaryPath, "; secondary:", secondaryPath) - logs.logEvent("onFinished", ["success", "primaryPath", "secondaryPath"], arguments) + onFinished: (primaryPath, secondaryPath, data) => { + console.warn("!!! ONBOARDING FINISHED; primary path:", primaryPath, "; secondary:", secondaryPath, "; data:", JSON.stringify(data)) + logs.logEvent("onFinished", ["primaryPath", "secondaryPath", "data"], arguments) console.warn("!!! RESTARTING FLOW") restartFlow() @@ -151,7 +175,7 @@ SplitView { ColumnLayout { Layout.fillWidth: true Label { - text: "Current page: %1".arg(onboarding.stack.currentItem ? onboarding.stack.currentItem.title : "") + text: "Current page: %1".arg(onboarding.stack.currentItem ? onboarding.stack.currentItem.pageClassName : "") } Label { text: `Current path: ${onboarding.primaryPath} -> ${onboarding.secondaryPath}` @@ -177,7 +201,7 @@ SplitView { Button { text: "Copy seedphrase" focusPolicy: Qt.NoFocus - onClicked: ClipboardUtils.setText("dog dog dog dog dog dog dog dog dog dog dog dog") + onClicked: ClipboardUtils.setText(keycardMock.mnemonic) } Button { text: "Copy PIN (\"%1\")".arg(ctrlPin.text) 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/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/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/assets.qrc b/ui/StatusQ/src/assets.qrc index d6610e3a52..00b9aa9541 100644 --- a/ui/StatusQ/src/assets.qrc +++ b/ui/StatusQ/src/assets.qrc @@ -8349,6 +8349,7 @@ assets/png/onboarding/status_keycard.png assets/png/onboarding/status_keycard_multiple.png assets/png/onboarding/status_seedphrase.png + assets/png/onboarding/status_sync.png assets/png/onboarding/enable_biometrics.png assets/png/onboarding/keycard/empty.png assets/png/onboarding/keycard/insert.png diff --git a/ui/StatusQ/src/assets/png/onboarding/enable_biometrics.png b/ui/StatusQ/src/assets/png/onboarding/enable_biometrics.png index 83538879b0..6191df3b28 100644 Binary files a/ui/StatusQ/src/assets/png/onboarding/enable_biometrics.png and b/ui/StatusQ/src/assets/png/onboarding/enable_biometrics.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/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 index 5fecbb8a8f..bb1517c954 100644 --- a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml +++ b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml @@ -31,7 +31,7 @@ Page { readonly property alias primaryPath: d.primaryPath readonly property alias secondaryPath: d.secondaryPath - signal finished(bool success, int primaryPath, int secondaryPath) + signal finished(int primaryPath, int secondaryPath, var data) signal keycardFactoryResetRequested() // TODO integrate/switch to an external flow signal keycardReloaded() @@ -57,8 +57,9 @@ Page { // state collected property string password - property bool enableBiometrics property string keycardPin + property bool enableBiometrics + property string syncConnectionString function resetState() { d.primaryPath = OnboardingLayout.PrimaryPath.Unknown @@ -66,16 +67,14 @@ Page { d.password = "" d.keycardPin = "" d.enableBiometrics = false - d.settings.seedphraseRevealed = false + d.syncConnectionString = "" } readonly property Settings settings: Settings { property bool keycardPromoShown // whether we've seen the keycard promo banner on KeycardIntroPage - property bool seedphraseRevealed function reset() { keycardPromoShown = false - seedphraseRevealed = false } } } @@ -88,12 +87,16 @@ Page { enum SecondaryPath { Unknown, + CreateProfileWithPassword, CreateProfileWithSeedphrase, CreateProfileWithKeycard, CreateProfileWithKeycardNewSeedphrase, - CreateProfileWithKeycardExistingSeedphrase - // TODO secondary Login paths + CreateProfileWithKeycardExistingSeedphrase, + + LoginWithSeedphrase, + LoginWithSyncing, + LoginWithKeycard } // page stack @@ -104,17 +107,17 @@ Page { pushEnter: Transition { ParallelAnimation { - NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 50; easing.type: Easing.InQuint } - NumberAnimation { property: "x"; from: (stack.mirrored ? -0.3 : 0.3) * stack.width; to: 0; duration: 400; easing.type: Easing.OutCubic } + 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: 50; easing.type: Easing.OutQuint } + 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: 50; easing.type: Easing.InQuint } - NumberAnimation { property: "x"; from: (stack.mirrored ? -0.3 : 0.3) * -stack.width; to: 0; duration: 400; easing.type: Easing.OutCubic } + 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 @@ -130,17 +133,13 @@ Page { onClicked: stack.pop() } - // back button - StatusButton { - objectName: "onboardingBackButton" - isRoundIcon: true + StatusBackButton { width: 44 height: 44 anchors.left: parent.left anchors.leftMargin: Theme.padding anchors.bottom: parent.bottom anchors.bottomMargin: Theme.padding - icon.name: "arrow-left" visible: stack.depth > 1 && !stack.busy onClicked: stack.pop() } @@ -175,6 +174,7 @@ Page { function onLoginRequested() { console.warn("!!! PRIMARY: LOG IN") d.primaryPath = OnboardingLayout.PrimaryPath.Login + stack.push(helpUsImproveStatusPage) } // help us improve page @@ -187,7 +187,7 @@ Page { if (d.primaryPath === OnboardingLayout.PrimaryPath.CreateProfile) stack.push(createProfilePage) else if (d.primaryPath === OnboardingLayout.PrimaryPath.Login) - ; // TODO Login path + stack.push(loginPage) } // create profile page @@ -199,7 +199,7 @@ Page { function onCreateProfileWithSeedphraseRequested() { console.warn("!!! SECONDARY: CREATE PROFILE WITH SEEDPHRASE") d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithSeedphrase - stack.push(seedphrasePage, { title: qsTr("Create profile with a recovery phrase"), subtitle: qsTr("Enter your 12, 18 or 24 word recovery phrase")}) + stack.push(seedphrasePage, { title: qsTr("Create profile using a recovery phrase"), subtitle: qsTr("Enter your 12, 18 or 24 word recovery phrase")}) } function onCreateProfileWithEmptyKeycardRequested() { console.warn("!!! SECONDARY: CREATE PROFILE WITH KEYCARD") @@ -207,10 +207,28 @@ Page { stack.push(keycardIntroPage) } + // login page + function onLoginWithSeedphraseRequested() { + console.warn("!!! SECONDARY: LOGIN WITH SEEDPHRASE") + d.secondaryPath = OnboardingLayout.SecondaryPath.LoginWithSeedphrase + stack.push(seedphrasePage, { title: qsTr("Sign in with your Status recovery phrase"), subtitle: qsTr("Enter your 12, 18 or 24 word recovery phrase")}) + } + function onLoginWithSyncingRequested() { + console.warn("!!! SECONDARY: LOGIN WITH SYNCING") + d.secondaryPath = OnboardingLayout.SecondaryPath.LoginWithSyncing + stack.push(loginBySyncPage) + } + function onLoginWithKeycardRequested() { + console.warn("!!! SECONDARY: LOGIN WITH KEYCARD") + d.secondaryPath = OnboardingLayout.SecondaryPath.LoginWithKeycard + stack.push(keycardIntroPage) + } + // create password page function onSetPasswordRequested(password: string) { console.warn("!!! SET PASSWORD REQUESTED") d.password = password + // TODO set the password immediately? stack.clear() stack.push(enableBiometricsPage, {subtitle: qsTr("Use biometrics to fill in your password?")}) // FIXME make optional on unsupported platforms } @@ -218,7 +236,7 @@ Page { // seedphrase page function onSeedphraseValidated() { console.warn("!!! SEEDPHRASE VALIDATED") - if (d.secondaryPath === OnboardingLayout.SecondaryPath.CreateProfileWithSeedphrase) { + if (d.secondaryPath === OnboardingLayout.SecondaryPath.CreateProfileWithSeedphrase || d.secondaryPath === OnboardingLayout.SecondaryPath.LoginWithSeedphrase) { console.warn("!!! AFTER SEEDPHRASE -> PASSWORD PAGE") stack.push(createPasswordPage) } else if (d.secondaryPath === OnboardingLayout.SecondaryPath.CreateProfileWithKeycardExistingSeedphrase) { @@ -238,15 +256,31 @@ Page { } function onKeycardFactoryResetRequested() { console.warn("!!! KEYCARD FACTORY RESET REQUESTED") + // TODO start keycard factory reset in a popup here root.keycardFactoryResetRequested() } - function onLoginWithKeycardRequested() { - console.warn("!!! LOGIN WITH KEYCARD REQUESTED") - stack.push(keycardEnterPinPage) + function onLoginWithThisKeycardRequested() { + console.warn("!!! LOGIN WITH THIS KEYCARD REQUESTED") + d.primaryPath = OnboardingLayout.PrimaryPath.Login + d.secondaryPath = OnboardingLayout.SecondaryPath.LoginWithKeycard + if (root.startupStore.getPin() !== "") + stack.push(keycardEnterPinPage) + else + stack.push(keycardCreatePinPage) } function onEmptyKeycardDetected() { console.warn("!!! EMPTY KEYCARD DETECTED") - stack.replace(createKeycardProfilePage) // NB: replacing the keycardIntroPage + if (d.secondaryPath === OnboardingLayout.SecondaryPath.LoginWithKeycard) + stack.replace(keycardEmptyPage) // NB: replacing the loginPage + else + stack.replace(createKeycardProfilePage) // NB: replacing the keycardIntroPage + } + function onNotEmptyKeycardDetected() { + console.warn("!!! NOT EMPTY KEYCARD DETECTED") + if (d.secondaryPath === OnboardingLayout.SecondaryPath.LoginWithKeycard) + stack.push(keycardEnterPinPage) + else + stack.push(keycardNotEmptyPage) } function onCreateKeycardProfileWithNewSeedphrase() { @@ -267,6 +301,7 @@ Page { function onKeycardPinCreated(pin) { console.warn("!!! KEYCARD PIN CREATED:", pin) d.keycardPin = pin + // TODO set the PIN immediately? Backpressure.debounce(root, 2000, function() { stack.clear() stack.push(enableBiometricsPage, // FIXME make optional on unsupported platforms @@ -277,6 +312,7 @@ Page { function onKeycardPinEntered(pin) { console.warn("!!! KEYCARD PIN ENTERED:", pin) d.keycardPin = pin + // TODO set the PIN immediately? stack.clear() stack.push(enableBiometricsPage, // FIXME make optional on unsupported platforms {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.")}) @@ -295,7 +331,6 @@ Page { function onBackupSeedphraseConfirmed() { console.warn("!!! BACKUP SEED CONFIRMED") - d.settings.seedphraseRevealed = true root.privacyStore.mnemonicWasShown() stack.push(backupSeedVerifyPage) } @@ -311,6 +346,19 @@ Page { stack.replace(splashScreen, { runningProgressAnimation: true }) } + // login with sync pages + function onSyncProceedWithConnectionString(connectionString) { + console.warn("!!! SYNC PROCEED WITH CONNECTION STRING:", connectionString) + d.syncConnectionString = connectionString + root.startupStore.setConnectionString(connectionString) + // TODO backend: start the sync + Backpressure.debounce(root, 1000, function() { + stack.clear() + // TODO show the sync in progress screen instead of the final splash page? + stack.replace(splashScreen, { runningProgressAnimation: true }) + })() + } + // enable biometrics page function onEnableBiometricsRequested(enabled: bool) { console.warn("!!! ENABLE BIOMETRICS:", enabled) @@ -346,30 +394,26 @@ Page { id: createPasswordPage CreatePasswordPage { passwordStrengthScoreFunction: root.startupStore.getPasswordStrengthScore - StackView.onRemoved: { - d.password = "" - } } } Component { id: enableBiometricsPage - EnableBiometricsPage { - StackView.onRemoved: d.enableBiometrics = false - } + EnableBiometricsPage {} } Component { id: splashScreen DidYouKnowSplashScreen { - readonly property string title: "Splash" + readonly property string pageClassName: "Splash" property bool runningProgressAnimation NumberAnimation on progress { from: 0.0 to: 1 duration: root.splashScreenDurationMs running: runningProgressAnimation - onStopped: root.finished(true, d.primaryPath, d.secondaryPath) + onStopped: root.finished(d.primaryPath, d.secondaryPath, + {"password": d.password, "keycardPin": d.keycardPin, "enableBiometrics": d.enableBiometrics, "syncConnectionString": d.syncConnectionString}) } } } @@ -384,7 +428,10 @@ Page { Component { id: createKeycardProfilePage CreateKeycardProfilePage { - StackView.onActivated: d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithKeycard + StackView.onActivated: { + d.primaryPath = OnboardingLayout.PrimaryPath.CreateProfile + d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithKeycard + } } } @@ -397,10 +444,22 @@ Page { // NB just to make sure we don't miss the signal when we (re)load the page in the final state already if (keycardState === Constants.startupState.keycardEmpty) emptyKeycardDetected() + else if (keycardState === Constants.startupState.keycardNotEmpty) + notEmptyKeycardDetected() } } } + Component { + id: keycardEmptyPage + KeycardEmptyPage {} + } + + Component { + id: keycardNotEmptyPage + KeycardNotEmptyPage {} + } + Component { id: keycardCreatePinPage KeycardCreatePinPage {} @@ -427,7 +486,6 @@ Page { Component { id: backupSeedRevealPage BackupSeedphraseReveal { - seedphraseRevealed: d.settings.seedphraseRevealed seedWords: d.seedWords } } @@ -451,6 +509,20 @@ Page { BackupSeedphraseOutro {} } + Component { + id: loginPage + LoginPage { + StackView.onActivated: d.secondaryPath = OnboardingLayout.SecondaryPath.Unknown // reset when we get back here + } + } + + Component { + id: loginBySyncPage + LoginBySyncingPage { + validateConnectionString: root.startupStore.validateLocalPairingConnectionString + } + } + // common popups Component { id: privacyPolicyPopup diff --git a/ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml b/ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml index 78b4c44a71..b6e22a4cf4 100644 --- a/ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml +++ b/ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml @@ -90,7 +90,7 @@ Control { NumberAnimation on width { from: 0 to: pageIndicatorDelegate.availableWidth - duration: 2000 + duration: 3000 running: pageIndicatorDelegate.isCurrentPage onStopped: { if (pageIndicatorDelegate.isCurrentPage) diff --git a/ui/app/AppLayouts/Onboarding2/components/SeedphraseVerifyInput.qml b/ui/app/AppLayouts/Onboarding2/components/SeedphraseVerifyInput.qml index 0e08511d77..0b042fc9c9 100644 --- a/ui/app/AppLayouts/Onboarding2/components/SeedphraseVerifyInput.qml +++ b/ui/app/AppLayouts/Onboarding2/components/SeedphraseVerifyInput.qml @@ -46,25 +46,18 @@ StatusTextField { switch (event.key) { case Qt.Key_Return: case Qt.Key_Enter: { - if (!!text && filteredModel.count > 0) { + if (filteredModel.count > 0) { root.text = filteredModel.get(suggestionsList.currentIndex).seedWord } break } - case Qt.Key_Down: { - suggestionsList.incrementCurrentIndex() - break - } - case Qt.Key_Up: { - suggestionsList.decrementCurrentIndex() - break - } case Qt.Key_Space: { event.accepted = !event.text.match(/^[a-zA-Z]$/) break } } } + Keys.forwardTo: [suggestionsList] StatusDropdown { x: 0 diff --git a/ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml b/ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml index c28b16b703..93a2b6d31b 100644 --- a/ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml +++ b/ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml @@ -1,12 +1,11 @@ import QtQuick 2.15 -import QtQuick.Controls 2.15 import StatusQ.Core 0.1 import StatusQ.Components 0.1 import StatusQ.Core.Theme 0.1 StatusListItem { - radius: 20 + radius: 12 asset.width: 32 asset.height: 32 asset.bgRadius: 0 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..4f06442afd --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/controls/OnboardingButtonFrame.qml @@ -0,0 +1,15 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core.Theme 0.1 + +Frame { + padding: 1 + + background: Rectangle { + border.width: 1 + border.color: Theme.palette.baseColor2 + radius: 12 + color: Theme.palette.background + } +} diff --git a/ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml b/ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml index abd22549a5..012c7905a6 100644 --- a/ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml +++ b/ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml @@ -1,13 +1,10 @@ import QtQuick 2.15 -import QtQuick.Layouts 1.15 import QtQuick.Controls 2.15 import QtGraphicalEffects 1.15 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 -import utils 1.0 - Frame { id: root @@ -30,7 +27,6 @@ Frame { radius: 7 samples: 15 cached: true - color: Theme.palette.name === Constants.darkThemeName ? Theme.palette.dropShadow - : Qt.rgba(0, 34/255, 51/255, 0.03) + color: Theme.palette.dropShadow } } diff --git a/ui/app/AppLayouts/Onboarding2/controls/qmldir b/ui/app/AppLayouts/Onboarding2/controls/qmldir index b513ee4081..44f3c9781c 100644 --- a/ui/app/AppLayouts/Onboarding2/controls/qmldir +++ b/ui/app/AppLayouts/Onboarding2/controls/qmldir @@ -1,2 +1,4 @@ +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 index 9597d83dc1..6f475458e0 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseAcks.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseAcks.qml @@ -12,6 +12,8 @@ OnboardingPage { signal backupSeedphraseContinue() + pageClassName: "BackupSeedphraseAcks" + contentItem: Item { ColumnLayout { anchors.centerIn: parent diff --git a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseIntro.qml b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseIntro.qml index 94fdc77c4d..f14cbeaa99 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseIntro.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseIntro.qml @@ -12,6 +12,8 @@ OnboardingPage { signal backupSeedphraseRequested() + pageClassName: "BackupSeedphraseIntro" + contentItem: Item { ColumnLayout { anchors.centerIn: parent diff --git a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseOutro.qml b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseOutro.qml index 11c6c359d3..307de382a3 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseOutro.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseOutro.qml @@ -14,6 +14,8 @@ OnboardingPage { signal backupSeedphraseRemovalConfirmed() + pageClassName: "BackupSeedphraseOutro" + contentItem: Item { ColumnLayout { anchors.centerIn: parent diff --git a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseReveal.qml b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseReveal.qml index 414506344a..f174aa44c8 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseReveal.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseReveal.qml @@ -14,10 +14,16 @@ OnboardingPage { id: root required property var seedWords - property bool seedphraseRevealed signal backupSeedphraseConfirmed() + pageClassName: "BackupSeedphraseReveal" + + QtObject { + id: d + property bool seedphraseRevealed + } + contentItem: Item { ColumnLayout { anchors.centerIn: parent @@ -78,7 +84,7 @@ OnboardingPage { } } } - layer.enabled: !root.seedphraseRevealed + layer.enabled: !d.seedphraseRevealed layer.effect: GaussianBlur { radius: 16 samples: 33 @@ -91,8 +97,8 @@ OnboardingPage { text: qsTr("Reveal recovery phrase") icon.name: "show" type: StatusBaseButton.Type.Primary - visible: !root.seedphraseRevealed - onClicked: root.seedphraseRevealed = true + visible: !d.seedphraseRevealed + onClicked: d.seedphraseRevealed = true } } @@ -107,7 +113,7 @@ OnboardingPage { StatusButton { Layout.alignment: Qt.AlignHCenter text: qsTr("Confirm recovery phrase") - enabled: root.seedphraseRevealed + enabled: d.seedphraseRevealed onClicked: root.backupSeedphraseConfirmed() } } diff --git a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseVerify.qml b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseVerify.qml index 9a7f53ab78..e19302200b 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseVerify.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/BackupSeedphraseVerify.qml @@ -21,6 +21,8 @@ OnboardingPage { signal backupSeedphraseVerified() + pageClassName: "BackupSeedphraseVerify" + QtObject { id: d readonly property var seedSuggestions: BIP39_en {} // [{seedWord:string}, ...] @@ -87,9 +89,13 @@ OnboardingPage { seedSuggestions: d.seedSuggestions Component.onCompleted: if (index === 0) forceActiveFocus() onAccepted: { - const nextItem = seedRepeater.itemAt(index + 1) ?? seedRepeater.itemAt(0) - if (!!nextItem) { - nextItem.input.forceActiveFocus() + 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() + } } } } diff --git a/ui/app/AppLayouts/Onboarding2/pages/CreateKeycardProfilePage.qml b/ui/app/AppLayouts/Onboarding2/pages/CreateKeycardProfilePage.qml index 94750192bc..c937945fb9 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/CreateKeycardProfilePage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/CreateKeycardProfilePage.qml @@ -19,6 +19,8 @@ OnboardingPage { signal createKeycardProfileWithNewSeedphrase() signal createKeycardProfileWithExistingSeedphrase() + pageClassName: "CreateKeycardProfilePage" + contentItem: Item { ColumnLayout { width: parent.width @@ -44,7 +46,7 @@ OnboardingPage { Layout.maximumWidth: Math.min(380, root.availableWidth) Layout.alignment: Qt.AlignHCenter Layout.topMargin: 56 - spacing: 20 + spacing: Theme.bigPadding OnboardingFrame { Layout.fillWidth: true @@ -83,10 +85,8 @@ OnboardingPage { } } - OnboardingFrame { + OnboardingButtonFrame { Layout.fillWidth: true - padding: 1 - dropShadow: false contentItem: ColumnLayout { spacing: 0 ListItemButton { diff --git a/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml b/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml index 1041eea266..3eaec9eabd 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml @@ -19,6 +19,8 @@ OnboardingPage { title: qsTr("Create profile password") + pageClassName: "CreatePasswordPage" + QtObject { id: d diff --git a/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml b/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml index 879f2023a0..e1680736e8 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml @@ -1,7 +1,6 @@ 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 @@ -22,11 +21,13 @@ OnboardingPage { signal createProfileWithSeedphraseRequested() signal createProfileWithEmptyKeycardRequested() + pageClassName: "CreateProfilePage" + contentItem: Item { ColumnLayout { anchors.centerIn: parent width: Math.min(380, root.availableWidth) - spacing: 20 + spacing: Theme.bigPadding StatusBaseText { Layout.fillWidth: true @@ -38,7 +39,7 @@ OnboardingPage { } StatusBaseText { Layout.fillWidth: true - Layout.topMargin: -12 + Layout.topMargin: -Theme.padding text: qsTr("How would you like to start using Status?") color: Theme.palette.baseColor1 wrapMode: Text.WordWrap @@ -82,11 +83,9 @@ OnboardingPage { } } - OnboardingFrame { - id: buttonFrame + OnboardingButtonFrame { Layout.fillWidth: true - padding: 1 - dropShadow: false + id: buttonFrame contentItem: ColumnLayout { spacing: 0 ListItemButton { diff --git a/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml b/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml index 5a21d96a0c..d693ed8291 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml @@ -16,12 +16,23 @@ OnboardingPage { signal enableBiometricsRequested(bool enable) + pageClassName: "EnableBiometricsPage" + contentItem: Item { ColumnLayout { anchors.centerIn: parent - spacing: 20 + 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 @@ -39,23 +50,15 @@ OnboardingPage { horizontalAlignment: Text.AlignHCenter } - StatusImage { - Layout.preferredWidth: 260 - Layout.preferredHeight: 260 - Layout.topMargin: 20 - Layout.bottomMargin: 20 - Layout.alignment: Qt.AlignHCenter - mipmap: true - source: Theme.png("onboarding/enable_biometrics") - } - StatusButton { + Layout.topMargin: Theme.halfPadding Layout.alignment: Qt.AlignHCenter text: qsTr("Yes, use biometrics") onClicked: root.enableBiometricsRequested(true) } StatusFlatButton { + 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 index 4adb4d4a18..77d8734022 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/HelpUsImproveStatusPage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/HelpUsImproveStatusPage.qml @@ -20,6 +20,8 @@ OnboardingPage { signal shareUsageDataRequested(bool enabled) signal privacyPolicyRequested() + pageClassName: "HelpUsImproveStatusPage" + contentItem: Item { ColumnLayout { anchors.centerIn: parent @@ -42,8 +44,8 @@ OnboardingPage { } StatusImage { - Layout.preferredWidth: 300 - Layout.preferredHeight: 300 + Layout.preferredWidth: 320 + Layout.preferredHeight: 320 Layout.topMargin: 36 Layout.bottomMargin: 36 Layout.alignment: Qt.AlignHCenter diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardBasePage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardBasePage.qml index 866f1367ba..2ffd2b72b0 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/KeycardBasePage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardBasePage.qml @@ -8,8 +8,6 @@ import StatusQ.Controls 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 as SQUtils -import utils 1.0 - OnboardingPage { id: root diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardCreatePinPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardCreatePinPage.qml index ad13826e07..ce3481e00d 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/KeycardCreatePinPage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardCreatePinPage.qml @@ -17,6 +17,7 @@ KeycardBasePage { signal keycardPinCreated(string pin) + pageClassName: "KeycardCreatePinPage" image.source: Theme.png("onboarding/keycard/reading") QtObject { diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardEmptyPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardEmptyPage.qml new file mode 100644 index 0000000000..f4233370c5 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardEmptyPage.qml @@ -0,0 +1,25 @@ +import QtQuick 2.15 + +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +KeycardBasePage { + id: root + + signal createProfileWithEmptyKeycardRequested() + + 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() + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml index 1d1c417ac2..260e82f326 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardEnterPinPage.qml @@ -7,6 +7,7 @@ import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import StatusQ.Controls.Validators 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Backpressure 0.1 import AppLayouts.Onboarding2.controls 1.0 @@ -23,6 +24,7 @@ KeycardBasePage { signal keycardFactoryResetRequested() signal keycardLocked() + pageClassName: "KeycardEnterPinPage" image.source: Theme.png("onboarding/keycard/reading") QtObject { @@ -134,7 +136,9 @@ KeycardBasePage { } StateChangeScript { script: { - root.keycardPinEntered(pinInput.pinInput) + Backpressure.debounce(root, 2000, function() { + root.keycardPinEntered(pinInput.pinInput) + })() } } }, diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml index a2a74b2084..d9922241e3 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml @@ -17,11 +17,12 @@ KeycardBasePage { required property string keycardState // Constants.startupState.keycardXXX property bool displayPromoBanner - signal reloadKeycardRequested() signal keycardFactoryResetRequested() - signal loginWithKeycardRequested() - + signal reloadKeycardRequested() signal emptyKeycardDetected() + signal notEmptyKeycardDetected() + + pageClassName: "KeycardIntroPage" OnboardingFrame { id: promoBanner @@ -77,46 +78,22 @@ KeycardBasePage { } buttons: [ - MaybeOutlineButton { - id: btnLogin - text: qsTr("Log in with this Keycard") - onClicked: root.loginWithKeycardRequested() - }, MaybeOutlineButton { id: btnFactoryReset + visible: false text: qsTr("Factory reset Keycard") + anchors.horizontalCenter: parent.horizontalCenter onClicked: root.keycardFactoryResetRequested() }, MaybeOutlineButton { id: btnReload - text: qsTr("I’ve inserted a Keycard") + visible: false + text: qsTr("I’ve inserted a different Keycard") + anchors.horizontalCenter: parent.horizontalCenter onClicked: root.reloadKeycardRequested() } ] - // inside a Column (or another Positioner), make all but the first button outline - component MaybeOutlineButton: StatusButton { - id: maybeOutlineButton - width: 320 - anchors.horizontalCenter: parent.horizontalCenter - visible: false - Binding on normalColor { - value: "transparent" - when: !maybeOutlineButton.Positioner.isFirstItem - restoreMode: Binding.RestoreBindingOrValue - } - Binding on borderWidth { - value: 1 - when: !maybeOutlineButton.Positioner.isFirstItem - restoreMode: Binding.RestoreBindingOrValue - } - Binding on borderColor { - value: Theme.palette.baseColor2 - when: !maybeOutlineButton.Positioner.isFirstItem - restoreMode: Binding.RestoreBindingOrValue - } - } - states: [ // normal/intro states State { @@ -139,15 +116,18 @@ KeycardBasePage { PropertyChanges { target: root title: qsTr("Insert your Keycard") - infoText.text: qsTr("Need a little %1?").arg(Utils.getStyledLink(qsTr("help"), "https://keycard.tech/docs/", infoText.hoveredLink, - Theme.palette.baseColor1, Theme.palette.primaryColor1)) + infoText.text: qsTr("Need a little %1?").arg(Utils.getStyledLink(qsTr("help"), "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 === Constants.startupState.keycardReadingKeycard || - root.keycardState === Constants.startupState.keycardInsertedKeycard + root.keycardState === Constants.startupState.keycardInsertedKeycard || + root.keycardState === Constants.startupState.keycardRecognizedKeycard PropertyChanges { target: root title: qsTr("Reading Keycard...") @@ -156,9 +136,42 @@ KeycardBasePage { }, // error states State { - name: "error" + name: "notKeycard" + when: root.keycardState === Constants.startupState.keycardWrongKeycard || + root.keycardState === Constants.startupState.keycardNotKeycard 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 + } + }, + State { + name: "noService" + when: root.keycardState === Constants.startupState.keycardNoPCSCService + 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 === Constants.startupState.keycardMaxPairingSlotsReached + 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 { @@ -170,63 +183,38 @@ KeycardBasePage { visible: true } }, - State { - name: "notKeycard" - extend: "error" - when: root.keycardState === Constants.startupState.keycardWrongKeycard || - root.keycardState === Constants.startupState.keycardNotKeycard - 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: btnFactoryReset - visible: false - } - }, - State { - name: "occupied" - extend: "error" - when: root.keycardState === Constants.startupState.keycardMaxPairingSlotsReached - PropertyChanges { - target: root - title: qsTr("All pairing slots occupied") - subtitle: qsTr("Factory reset this Keycard or insert a different one") - } - }, State { name: "locked" - extend: "error" when: root.keycardState === Constants.startupState.keycardLocked PropertyChanges { target: root - title: qsTr("Keycard locked") + 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") - } - }, - State { - name: "notEmpty" - extend: "error" - when: root.keycardState === Constants.startupState.keycardNotEmpty - PropertyChanges { - target: root - 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") } PropertyChanges { - target: btnLogin + target: btnFactoryReset + visible: true + } + PropertyChanges { + target: btnReload visible: true } }, - // success/exit state + // exit states State { - name: "emptyDetected" + name: "empty" when: root.keycardState === Constants.startupState.keycardEmpty StateChangeScript { script: root.emptyKeycardDetected() } + }, + State { + name: "notEmpty" + when: root.keycardState === Constants.startupState.keycardNotEmpty + 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..8e0f800c89 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardNotEmptyPage.qml @@ -0,0 +1,40 @@ +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 { + id: btnReload + text: qsTr("I’ve inserted a Keycard") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.reloadKeycardRequested() + }, + MaybeOutlineButton { + id: btnLogin + text: qsTr("Log in with this Keycard") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.loginWithThisKeycardRequested() + }, + MaybeOutlineButton { + id: btnFactoryReset + 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..166f98f8aa --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/LoginBySyncingPage.qml @@ -0,0 +1,68 @@ +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 { + id: syncView + 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..a3c41aef42 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/LoginPage.qml @@ -0,0 +1,172 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml.Models 2.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 StatusQ.Popups.Dialog 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +import utils 1.0 + +OnboardingPage { + id: root + + 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(245, parent.width) + Layout.preferredHeight: Math.min(200, height) + source: Theme.png("onboarding/status_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 { + 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 { + Layout.fillWidth: true + title: qsTr("Log in by syncing") + subTitle: qsTr("If you have Status on another device") + asset.name: Theme.svg("mobile-sync") // FIXME correct icon + 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 { + Layout.fillWidth: true + title: qsTr("Log in with Keycard") + subTitle: qsTr("If your profile keys are stored on a Keycard") + asset.name: Theme.png("onboarding/create_profile_keycard") + onClicked: root.loginWithKeycardRequested() + } + } + } + } + } + + Component { + id: loginWithSyncAck + StatusDialog { + title: qsTr("Log in by syncing") + width: 480 + padding: 20 + destroyOnClose: true + 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 { + Layout.fillWidth: true + id: ack1 + text: qsTr("Connect both devices to the same network") + } + StatusCheckBox { + Layout.fillWidth: true + id: ack2 + text: qsTr("Make sure you are logged in on the other device") + } + StatusCheckBox { + 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 { + text: qsTr("Continue") + enabled: ack1.checked && ack2.checked && ack3.checked + onClicked: { + root.loginWithSyncingRequested() + close() + } + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml b/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml index 5a6a81f04f..6a9efcbc0f 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml @@ -4,6 +4,8 @@ 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) diff --git a/ui/app/AppLayouts/Onboarding2/pages/SeedphrasePage.qml b/ui/app/AppLayouts/Onboarding2/pages/SeedphrasePage.qml index 13b7923b5a..ba9d9eeb24 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/SeedphrasePage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/SeedphrasePage.qml @@ -18,6 +18,8 @@ OnboardingPage { signal seedphraseValidated() + pageClassName: "SeedphrasePage" + contentItem: Item { ColumnLayout { anchors.centerIn: parent diff --git a/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml b/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml index 740394e393..7c58b71c5f 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml @@ -15,6 +15,7 @@ import utils 1.0 OnboardingPage { id: root + pageClassName: "WelcomePage" title: qsTr("Welcome to Status") signal createProfileRequested() diff --git a/ui/app/AppLayouts/Onboarding2/pages/qmldir b/ui/app/AppLayouts/Onboarding2/pages/qmldir index 5cc71f3440..58f98e41dc 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/qmldir +++ b/ui/app/AppLayouts/Onboarding2/pages/qmldir @@ -1,15 +1,19 @@ -WelcomePage 1.0 WelcomePage.qml -HelpUsImproveStatusPage 1.0 HelpUsImproveStatusPage.qml -CreateProfilePage 1.0 CreateProfilePage.qml -CreatePasswordPage 1.0 CreatePasswordPage.qml -EnableBiometricsPage 1.0 EnableBiometricsPage.qml -SeedphrasePage 1.0 SeedphrasePage.qml -KeycardIntroPage 1.0 KeycardIntroPage.qml -CreateKeycardProfilePage 1.0 CreateKeycardProfilePage.qml -KeycardCreatePinPage 1.0 KeycardCreatePinPage.qml -KeycardEnterPinPage 1.0 KeycardEnterPinPage.qml -BackupSeedphraseIntro 1.0 BackupSeedphraseIntro.qml 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 -BackupSeedphraseOutro 1.0 BackupSeedphraseOutro.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 +LoginPage 1.0 LoginPage.qml +LoginBySyncingPage 1.0 LoginBySyncingPage.qml +SeedphrasePage 1.0 SeedphrasePage.qml +WelcomePage 1.0 WelcomePage.qml 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..72cdc421c2 100644 --- a/ui/imports/shared/controls/StatusSyncCodeInput.qml +++ b/ui/imports/shared/controls/StatusSyncCodeInput.qml @@ -25,11 +25,12 @@ StatusInput { switch (root.mode) { case StatusSyncCodeInput.Mode.WriteMode: return root.valid ? validCodeIconComponent - : pasteButtonComponent + : input.edit.canPaste ? pasteButtonComponent : null case StatusSyncCodeInput.Mode.ReadMode: return copyButtonComponent } } + rightPadding: 12 Component { id: copyButtonComponent 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/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/views/SyncingEnterCode.qml b/ui/imports/shared/views/SyncingEnterCode.qml index aae94bf460..71e15e11b9 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,54 @@ 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 } - 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 { + text: root.firstTabName + } + + 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 +86,12 @@ ColumnLayout { } ColumnLayout { - spacing: 20 + Layout.topMargin: Theme.padding + spacing: Theme.padding StatusSyncCodeInput { id: syncCode Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: 424 + Layout.preferredWidth: 440 mode: StatusSyncCodeInput.Mode.WriteMode label: root.syncCodeLabel @@ -97,30 +104,36 @@ 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 { + 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() }