feat(Onboarding): Login flows

- implement the Login flows (seed, sync, keycard)
- amend the keycard flow sequences with separate (non) empty page
This commit is contained in:
Lukáš Tinkl 2024-11-19 12:14:14 +01:00
parent 69cc5d52f5
commit 71ca20a9f3
No known key found for this signature in database
GPG Key ID: 4ABB993B9382F296
37 changed files with 672 additions and 198 deletions

View File

@ -47,6 +47,8 @@ SplitView {
// create keycard profile // create keycard profile
Constants.startupState.keycardEmpty Constants.startupState.keycardEmpty
] ]
readonly property string mnemonic: "dog dog dog dog dog dog dog dog dog dog dog dog"
} }
OnboardingLayout { OnboardingLayout {
@ -59,14 +61,30 @@ SplitView {
} }
function getPasswordStrengthScore(password) { function getPasswordStrengthScore(password) {
logs.logEvent("StartupStore.getPasswordStrengthScore", ["password"], arguments)
return Math.min(password.length-1, 4) return Math.min(password.length-1, 4)
} }
function validMnemonic(mnemonic) { function validMnemonic(mnemonic) {
return true logs.logEvent("StartupStore.validMnemonic", ["mnemonic"], arguments)
return mnemonic === keycardMock.mnemonic
} }
function getPin() { function getPin() {
logs.logEvent("StartupStore.getPin()")
return ctrlPin.text 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 { readonly property var startupModuleInst: QtObject {
property int remainingAttempts: 5 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"] readonly property var words: ["apple", "banana", "cat", "cow", "catalog", "catch", "category", "cattle", "dog", "elephant", "fish", "grape"]
function getMnemonic() { function getMnemonic() {
logs.logEvent("PrivacyStore.getMnemonic()")
return words.join(" ") return words.join(" ")
} }
function mnemonicWasShown() { function mnemonicWasShown() {
console.warn("!!! MNEMONIC SHOWN") 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 property bool metricsPopupSeen
} }
onFinished: (success, primaryPath, secondaryPath) => { onFinished: (primaryPath, secondaryPath, data) => {
console.warn("!!! ONBOARDING FINISHED; success:", success, "; primary path:", primaryPath, "; secondary:", secondaryPath) console.warn("!!! ONBOARDING FINISHED; primary path:", primaryPath, "; secondary:", secondaryPath, "; data:", JSON.stringify(data))
logs.logEvent("onFinished", ["success", "primaryPath", "secondaryPath"], arguments) logs.logEvent("onFinished", ["primaryPath", "secondaryPath", "data"], arguments)
console.warn("!!! RESTARTING FLOW") console.warn("!!! RESTARTING FLOW")
restartFlow() restartFlow()
@ -151,7 +175,7 @@ SplitView {
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Label { 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 { Label {
text: `Current path: ${onboarding.primaryPath} -> ${onboarding.secondaryPath}` text: `Current path: ${onboarding.primaryPath} -> ${onboarding.secondaryPath}`
@ -177,7 +201,7 @@ SplitView {
Button { Button {
text: "Copy seedphrase" text: "Copy seedphrase"
focusPolicy: Qt.NoFocus focusPolicy: Qt.NoFocus
onClicked: ClipboardUtils.setText("dog dog dog dog dog dog dog dog dog dog dog dog") onClicked: ClipboardUtils.setText(keycardMock.mnemonic)
} }
Button { Button {
text: "Copy PIN (\"%1\")".arg(ctrlPin.text) text: "Copy PIN (\"%1\")".arg(ctrlPin.text)

View File

@ -7,6 +7,7 @@ import Storybook 1.0
import mainui 1.0 import mainui 1.0
import shared.views 1.0 import shared.views 1.0
import shared.stores 1.0 as SharedStores import shared.stores 1.0 as SharedStores
import shared.popups 1.0
import AppLayouts.stores 1.0 as AppLayoutStores import AppLayouts.stores 1.0 as AppLayoutStores
@ -33,9 +34,19 @@ SplitView {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
validateConnectionString: (stringValue) => !Number.isNaN(parseInt(stringValue)) 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) onProceed: (connectionString) => logs.logEvent("SyncingEnterCode::proceed", ["connectionString"], arguments)
} }
Component {
id: instructionsPopup
GetSyncCodeInstructionsPopup {
destroyOnClose: true
}
}
} }
LogsAndControlsPanel { LogsAndControlsPanel {

View File

@ -8,12 +8,10 @@ StatusAnimatedImage 0.1 StatusAnimatedImage.qml
StatusBadge 0.1 StatusBadge.qml StatusBadge 0.1 StatusBadge.qml
StatusBetaTag 0.1 StatusBetaTag.qml StatusBetaTag 0.1 StatusBetaTag.qml
StatusCard 0.1 StatusCard.qml StatusCard 0.1 StatusCard.qml
StatusChart 0.1 StatusChart.qml
StatusChartPanel 0.1 StatusChartPanel.qml StatusChartPanel 0.1 StatusChartPanel.qml
StatusChatInfoToolBar 0.1 StatusChatInfoToolBar.qml StatusChatInfoToolBar 0.1 StatusChatInfoToolBar.qml
StatusChatList 0.1 StatusChatList.qml StatusChatList 0.1 StatusChatList.qml
StatusChatListAndCategories 0.1 StatusChatListAndCategories.qml StatusChatListAndCategories 0.1 StatusChatListAndCategories.qml
StatusChatListCategory 0.1 StatusChatListCategory.qml
StatusChatListCategoryItem 0.1 StatusChatListCategoryItem.qml StatusChatListCategoryItem 0.1 StatusChatListCategoryItem.qml
StatusChatListItem 0.1 StatusChatListItem.qml StatusChatListItem 0.1 StatusChatListItem.qml
StatusColorSpace 0.0 StatusColorSpace.qml StatusColorSpace 0.0 StatusColorSpace.qml
@ -23,7 +21,6 @@ StatusContactRequestsIndicatorListItem 0.1 StatusContactRequestsIndicatorListIte
StatusContactVerificationIcons 0.1 StatusContactVerificationIcons.qml StatusContactVerificationIcons 0.1 StatusContactVerificationIcons.qml
StatusCursorDelegate 0.1 StatusCursorDelegate.qml StatusCursorDelegate 0.1 StatusCursorDelegate.qml
StatusDateGroupLabel 0.1 StatusDateGroupLabel.qml StatusDateGroupLabel 0.1 StatusDateGroupLabel.qml
StatusDateInput 0.1 StatusDateInput.qml
StatusDatePicker 0.1 StatusDatePicker.qml StatusDatePicker 0.1 StatusDatePicker.qml
StatusDescriptionListItem 0.1 StatusDescriptionListItem.qml StatusDescriptionListItem 0.1 StatusDescriptionListItem.qml
StatusDotsLoadingIndicator 0.1 StatusDotsLoadingIndicator.qml StatusDotsLoadingIndicator 0.1 StatusDotsLoadingIndicator.qml

View File

@ -8349,6 +8349,7 @@
<file>assets/png/onboarding/status_keycard.png</file> <file>assets/png/onboarding/status_keycard.png</file>
<file>assets/png/onboarding/status_keycard_multiple.png</file> <file>assets/png/onboarding/status_keycard_multiple.png</file>
<file>assets/png/onboarding/status_seedphrase.png</file> <file>assets/png/onboarding/status_seedphrase.png</file>
<file>assets/png/onboarding/status_sync.png</file>
<file>assets/png/onboarding/enable_biometrics.png</file> <file>assets/png/onboarding/enable_biometrics.png</file>
<file>assets/png/onboarding/keycard/empty.png</file> <file>assets/png/onboarding/keycard/empty.png</file>
<file>assets/png/onboarding/keycard/insert.png</file> <file>assets/png/onboarding/keycard/insert.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -1,4 +1,4 @@
import QtQuick 2.13 import QtQuick 2.15
import shared.popups 1.0 import shared.popups 1.0
import shared.views 1.0 import shared.views 1.0

View File

@ -31,7 +31,7 @@ Page {
readonly property alias primaryPath: d.primaryPath readonly property alias primaryPath: d.primaryPath
readonly property alias secondaryPath: d.secondaryPath 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 keycardFactoryResetRequested() // TODO integrate/switch to an external flow
signal keycardReloaded() signal keycardReloaded()
@ -57,8 +57,9 @@ Page {
// state collected // state collected
property string password property string password
property bool enableBiometrics
property string keycardPin property string keycardPin
property bool enableBiometrics
property string syncConnectionString
function resetState() { function resetState() {
d.primaryPath = OnboardingLayout.PrimaryPath.Unknown d.primaryPath = OnboardingLayout.PrimaryPath.Unknown
@ -66,16 +67,14 @@ Page {
d.password = "" d.password = ""
d.keycardPin = "" d.keycardPin = ""
d.enableBiometrics = false d.enableBiometrics = false
d.settings.seedphraseRevealed = false d.syncConnectionString = ""
} }
readonly property Settings settings: Settings { readonly property Settings settings: Settings {
property bool keycardPromoShown // whether we've seen the keycard promo banner on KeycardIntroPage property bool keycardPromoShown // whether we've seen the keycard promo banner on KeycardIntroPage
property bool seedphraseRevealed
function reset() { function reset() {
keycardPromoShown = false keycardPromoShown = false
seedphraseRevealed = false
} }
} }
} }
@ -88,12 +87,16 @@ Page {
enum SecondaryPath { enum SecondaryPath {
Unknown, Unknown,
CreateProfileWithPassword, CreateProfileWithPassword,
CreateProfileWithSeedphrase, CreateProfileWithSeedphrase,
CreateProfileWithKeycard, CreateProfileWithKeycard,
CreateProfileWithKeycardNewSeedphrase, CreateProfileWithKeycardNewSeedphrase,
CreateProfileWithKeycardExistingSeedphrase CreateProfileWithKeycardExistingSeedphrase,
// TODO secondary Login paths
LoginWithSeedphrase,
LoginWithSyncing,
LoginWithKeycard
} }
// page stack // page stack
@ -104,17 +107,17 @@ Page {
pushEnter: Transition { pushEnter: Transition {
ParallelAnimation { ParallelAnimation {
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 50; easing.type: Easing.InQuint } 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: 400; easing.type: Easing.OutCubic } NumberAnimation { property: "x"; from: (stack.mirrored ? -0.3 : 0.3) * stack.width; to: 0; duration: d.swipeDuration; easing.type: Easing.OutCubic }
} }
} }
pushExit: Transition { 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 { popEnter: Transition {
ParallelAnimation { ParallelAnimation {
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 50; easing.type: Easing.InQuint } 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: 400; easing.type: Easing.OutCubic } NumberAnimation { property: "x"; from: (stack.mirrored ? -0.3 : 0.3) * -stack.width; to: 0; duration: d.swipeDuration; easing.type: Easing.OutCubic }
} }
} }
popExit: pushExit popExit: pushExit
@ -130,17 +133,13 @@ Page {
onClicked: stack.pop() onClicked: stack.pop()
} }
// back button StatusBackButton {
StatusButton {
objectName: "onboardingBackButton"
isRoundIcon: true
width: 44 width: 44
height: 44 height: 44
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.padding anchors.leftMargin: Theme.padding
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.padding anchors.bottomMargin: Theme.padding
icon.name: "arrow-left"
visible: stack.depth > 1 && !stack.busy visible: stack.depth > 1 && !stack.busy
onClicked: stack.pop() onClicked: stack.pop()
} }
@ -175,6 +174,7 @@ Page {
function onLoginRequested() { function onLoginRequested() {
console.warn("!!! PRIMARY: LOG IN") console.warn("!!! PRIMARY: LOG IN")
d.primaryPath = OnboardingLayout.PrimaryPath.Login d.primaryPath = OnboardingLayout.PrimaryPath.Login
stack.push(helpUsImproveStatusPage)
} }
// help us improve page // help us improve page
@ -187,7 +187,7 @@ Page {
if (d.primaryPath === OnboardingLayout.PrimaryPath.CreateProfile) if (d.primaryPath === OnboardingLayout.PrimaryPath.CreateProfile)
stack.push(createProfilePage) stack.push(createProfilePage)
else if (d.primaryPath === OnboardingLayout.PrimaryPath.Login) else if (d.primaryPath === OnboardingLayout.PrimaryPath.Login)
; // TODO Login path stack.push(loginPage)
} }
// create profile page // create profile page
@ -199,7 +199,7 @@ Page {
function onCreateProfileWithSeedphraseRequested() { function onCreateProfileWithSeedphraseRequested() {
console.warn("!!! SECONDARY: CREATE PROFILE WITH SEEDPHRASE") console.warn("!!! SECONDARY: CREATE PROFILE WITH SEEDPHRASE")
d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithSeedphrase 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() { function onCreateProfileWithEmptyKeycardRequested() {
console.warn("!!! SECONDARY: CREATE PROFILE WITH KEYCARD") console.warn("!!! SECONDARY: CREATE PROFILE WITH KEYCARD")
@ -207,10 +207,28 @@ Page {
stack.push(keycardIntroPage) 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 // create password page
function onSetPasswordRequested(password: string) { function onSetPasswordRequested(password: string) {
console.warn("!!! SET PASSWORD REQUESTED") console.warn("!!! SET PASSWORD REQUESTED")
d.password = password d.password = password
// TODO set the password immediately?
stack.clear() stack.clear()
stack.push(enableBiometricsPage, {subtitle: qsTr("Use biometrics to fill in your password?")}) // FIXME make optional on unsupported platforms 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 // seedphrase page
function onSeedphraseValidated() { function onSeedphraseValidated() {
console.warn("!!! SEEDPHRASE VALIDATED") 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") console.warn("!!! AFTER SEEDPHRASE -> PASSWORD PAGE")
stack.push(createPasswordPage) stack.push(createPasswordPage)
} else if (d.secondaryPath === OnboardingLayout.SecondaryPath.CreateProfileWithKeycardExistingSeedphrase) { } else if (d.secondaryPath === OnboardingLayout.SecondaryPath.CreateProfileWithKeycardExistingSeedphrase) {
@ -238,15 +256,31 @@ Page {
} }
function onKeycardFactoryResetRequested() { function onKeycardFactoryResetRequested() {
console.warn("!!! KEYCARD FACTORY RESET REQUESTED") console.warn("!!! KEYCARD FACTORY RESET REQUESTED")
// TODO start keycard factory reset in a popup here
root.keycardFactoryResetRequested() root.keycardFactoryResetRequested()
} }
function onLoginWithKeycardRequested() { function onLoginWithThisKeycardRequested() {
console.warn("!!! LOGIN WITH KEYCARD REQUESTED") console.warn("!!! LOGIN WITH THIS KEYCARD REQUESTED")
stack.push(keycardEnterPinPage) d.primaryPath = OnboardingLayout.PrimaryPath.Login
d.secondaryPath = OnboardingLayout.SecondaryPath.LoginWithKeycard
if (root.startupStore.getPin() !== "")
stack.push(keycardEnterPinPage)
else
stack.push(keycardCreatePinPage)
} }
function onEmptyKeycardDetected() { function onEmptyKeycardDetected() {
console.warn("!!! EMPTY KEYCARD DETECTED") 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() { function onCreateKeycardProfileWithNewSeedphrase() {
@ -267,6 +301,7 @@ Page {
function onKeycardPinCreated(pin) { function onKeycardPinCreated(pin) {
console.warn("!!! KEYCARD PIN CREATED:", pin) console.warn("!!! KEYCARD PIN CREATED:", pin)
d.keycardPin = pin d.keycardPin = pin
// TODO set the PIN immediately?
Backpressure.debounce(root, 2000, function() { Backpressure.debounce(root, 2000, function() {
stack.clear() stack.clear()
stack.push(enableBiometricsPage, // FIXME make optional on unsupported platforms stack.push(enableBiometricsPage, // FIXME make optional on unsupported platforms
@ -277,6 +312,7 @@ Page {
function onKeycardPinEntered(pin) { function onKeycardPinEntered(pin) {
console.warn("!!! KEYCARD PIN ENTERED:", pin) console.warn("!!! KEYCARD PIN ENTERED:", pin)
d.keycardPin = pin d.keycardPin = pin
// TODO set the PIN immediately?
stack.clear() stack.clear()
stack.push(enableBiometricsPage, // FIXME make optional on unsupported platforms 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.")}) {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() { function onBackupSeedphraseConfirmed() {
console.warn("!!! BACKUP SEED CONFIRMED") console.warn("!!! BACKUP SEED CONFIRMED")
d.settings.seedphraseRevealed = true
root.privacyStore.mnemonicWasShown() root.privacyStore.mnemonicWasShown()
stack.push(backupSeedVerifyPage) stack.push(backupSeedVerifyPage)
} }
@ -311,6 +346,19 @@ Page {
stack.replace(splashScreen, { runningProgressAnimation: true }) 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 // enable biometrics page
function onEnableBiometricsRequested(enabled: bool) { function onEnableBiometricsRequested(enabled: bool) {
console.warn("!!! ENABLE BIOMETRICS:", enabled) console.warn("!!! ENABLE BIOMETRICS:", enabled)
@ -346,30 +394,26 @@ Page {
id: createPasswordPage id: createPasswordPage
CreatePasswordPage { CreatePasswordPage {
passwordStrengthScoreFunction: root.startupStore.getPasswordStrengthScore passwordStrengthScoreFunction: root.startupStore.getPasswordStrengthScore
StackView.onRemoved: {
d.password = ""
}
} }
} }
Component { Component {
id: enableBiometricsPage id: enableBiometricsPage
EnableBiometricsPage { EnableBiometricsPage {}
StackView.onRemoved: d.enableBiometrics = false
}
} }
Component { Component {
id: splashScreen id: splashScreen
DidYouKnowSplashScreen { DidYouKnowSplashScreen {
readonly property string title: "Splash" readonly property string pageClassName: "Splash"
property bool runningProgressAnimation property bool runningProgressAnimation
NumberAnimation on progress { NumberAnimation on progress {
from: 0.0 from: 0.0
to: 1 to: 1
duration: root.splashScreenDurationMs duration: root.splashScreenDurationMs
running: runningProgressAnimation 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 { Component {
id: createKeycardProfilePage id: createKeycardProfilePage
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 // 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) if (keycardState === Constants.startupState.keycardEmpty)
emptyKeycardDetected() emptyKeycardDetected()
else if (keycardState === Constants.startupState.keycardNotEmpty)
notEmptyKeycardDetected()
} }
} }
} }
Component {
id: keycardEmptyPage
KeycardEmptyPage {}
}
Component {
id: keycardNotEmptyPage
KeycardNotEmptyPage {}
}
Component { Component {
id: keycardCreatePinPage id: keycardCreatePinPage
KeycardCreatePinPage {} KeycardCreatePinPage {}
@ -427,7 +486,6 @@ Page {
Component { Component {
id: backupSeedRevealPage id: backupSeedRevealPage
BackupSeedphraseReveal { BackupSeedphraseReveal {
seedphraseRevealed: d.settings.seedphraseRevealed
seedWords: d.seedWords seedWords: d.seedWords
} }
} }
@ -451,6 +509,20 @@ Page {
BackupSeedphraseOutro {} 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 // common popups
Component { Component {
id: privacyPolicyPopup id: privacyPolicyPopup

View File

@ -46,25 +46,18 @@ StatusTextField {
switch (event.key) { switch (event.key) {
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: { case Qt.Key_Enter: {
if (!!text && filteredModel.count > 0) { if (filteredModel.count > 0) {
root.text = filteredModel.get(suggestionsList.currentIndex).seedWord root.text = filteredModel.get(suggestionsList.currentIndex).seedWord
} }
break break
} }
case Qt.Key_Down: {
suggestionsList.incrementCurrentIndex()
break
}
case Qt.Key_Up: {
suggestionsList.decrementCurrentIndex()
break
}
case Qt.Key_Space: { case Qt.Key_Space: {
event.accepted = !event.text.match(/^[a-zA-Z]$/) event.accepted = !event.text.match(/^[a-zA-Z]$/)
break break
} }
} }
} }
Keys.forwardTo: [suggestionsList]
StatusDropdown { StatusDropdown {
x: 0 x: 0

View File

@ -1,5 +1,4 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1

View File

@ -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
}
}

View File

@ -1,2 +1,3 @@
OnboardingFrame 1.0 OnboardingFrame.qml OnboardingFrame 1.0 OnboardingFrame.qml
ListItemButton 1.0 ListItemButton.qml ListItemButton 1.0 ListItemButton.qml
MaybeOutlineButton 1.0 MaybeOutlineButton.qml

View File

@ -12,6 +12,8 @@ OnboardingPage {
signal backupSeedphraseContinue() signal backupSeedphraseContinue()
pageClassName: "BackupSeedphraseAcks"
contentItem: Item { contentItem: Item {
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent

View File

@ -12,6 +12,8 @@ OnboardingPage {
signal backupSeedphraseRequested() signal backupSeedphraseRequested()
pageClassName: "BackupSeedphraseIntro"
contentItem: Item { contentItem: Item {
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent

View File

@ -14,6 +14,8 @@ OnboardingPage {
signal backupSeedphraseRemovalConfirmed() signal backupSeedphraseRemovalConfirmed()
pageClassName: "BackupSeedphraseOutro"
contentItem: Item { contentItem: Item {
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent

View File

@ -14,10 +14,16 @@ OnboardingPage {
id: root id: root
required property var seedWords required property var seedWords
property bool seedphraseRevealed
signal backupSeedphraseConfirmed() signal backupSeedphraseConfirmed()
pageClassName: "BackupSeedphraseReveal"
QtObject {
id: d
property bool seedphraseRevealed
}
contentItem: Item { contentItem: Item {
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
@ -78,7 +84,7 @@ OnboardingPage {
} }
} }
} }
layer.enabled: !root.seedphraseRevealed layer.enabled: !d.seedphraseRevealed
layer.effect: GaussianBlur { layer.effect: GaussianBlur {
radius: 16 radius: 16
samples: 33 samples: 33
@ -91,8 +97,8 @@ OnboardingPage {
text: qsTr("Reveal recovery phrase") text: qsTr("Reveal recovery phrase")
icon.name: "show" icon.name: "show"
type: StatusBaseButton.Type.Primary type: StatusBaseButton.Type.Primary
visible: !root.seedphraseRevealed visible: !d.seedphraseRevealed
onClicked: root.seedphraseRevealed = true onClicked: d.seedphraseRevealed = true
} }
} }
@ -107,7 +113,7 @@ OnboardingPage {
StatusButton { StatusButton {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: qsTr("Confirm recovery phrase") text: qsTr("Confirm recovery phrase")
enabled: root.seedphraseRevealed enabled: d.seedphraseRevealed
onClicked: root.backupSeedphraseConfirmed() onClicked: root.backupSeedphraseConfirmed()
} }
} }

View File

@ -21,6 +21,8 @@ OnboardingPage {
signal backupSeedphraseVerified() signal backupSeedphraseVerified()
pageClassName: "BackupSeedphraseVerify"
QtObject { QtObject {
id: d id: d
readonly property var seedSuggestions: BIP39_en {} // [{seedWord:string}, ...] readonly property var seedSuggestions: BIP39_en {} // [{seedWord:string}, ...]
@ -87,9 +89,13 @@ OnboardingPage {
seedSuggestions: d.seedSuggestions seedSuggestions: d.seedSuggestions
Component.onCompleted: if (index === 0) forceActiveFocus() Component.onCompleted: if (index === 0) forceActiveFocus()
onAccepted: { onAccepted: {
const nextItem = seedRepeater.itemAt(index + 1) ?? seedRepeater.itemAt(0) if (seedRepeater.allValid) { /// move to next page
if (!!nextItem) { root.backupSeedphraseVerified()
nextItem.input.forceActiveFocus() } else { // move to next field
const nextItem = seedRepeater.itemAt(index + 1) ?? seedRepeater.itemAt(0)
if (!!nextItem) {
nextItem.input.forceActiveFocus()
}
} }
} }
} }

View File

@ -19,6 +19,8 @@ OnboardingPage {
signal createKeycardProfileWithNewSeedphrase() signal createKeycardProfileWithNewSeedphrase()
signal createKeycardProfileWithExistingSeedphrase() signal createKeycardProfileWithExistingSeedphrase()
pageClassName: "CreateKeycardProfilePage"
contentItem: Item { contentItem: Item {
ColumnLayout { ColumnLayout {
width: parent.width width: parent.width

View File

@ -19,6 +19,8 @@ OnboardingPage {
title: qsTr("Create profile password") title: qsTr("Create profile password")
pageClassName: "CreatePasswordPage"
QtObject { QtObject {
id: d id: d

View File

@ -1,7 +1,6 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
@ -22,6 +21,8 @@ OnboardingPage {
signal createProfileWithSeedphraseRequested() signal createProfileWithSeedphraseRequested()
signal createProfileWithEmptyKeycardRequested() signal createProfileWithEmptyKeycardRequested()
pageClassName: "CreateProfilePage"
contentItem: Item { contentItem: Item {
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent

View File

@ -16,6 +16,8 @@ OnboardingPage {
signal enableBiometricsRequested(bool enable) signal enableBiometricsRequested(bool enable)
pageClassName: "EnableBiometricsPage"
contentItem: Item { contentItem: Item {
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent

View File

@ -20,6 +20,8 @@ OnboardingPage {
signal shareUsageDataRequested(bool enabled) signal shareUsageDataRequested(bool enabled)
signal privacyPolicyRequested() signal privacyPolicyRequested()
pageClassName: "HelpUsImproveStatusPage"
contentItem: Item { contentItem: Item {
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent

View File

@ -8,8 +8,6 @@ import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils import StatusQ.Core.Utils 0.1 as SQUtils
import utils 1.0
OnboardingPage { OnboardingPage {
id: root id: root

View File

@ -17,6 +17,7 @@ KeycardBasePage {
signal keycardPinCreated(string pin) signal keycardPinCreated(string pin)
pageClassName: "KeycardCreatePinPage"
image.source: Theme.png("onboarding/keycard/reading") image.source: Theme.png("onboarding/keycard/reading")
QtObject { QtObject {

View File

@ -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()
}
]
}

View File

@ -7,6 +7,7 @@ import StatusQ.Components 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1 import StatusQ.Controls.Validators 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Core.Backpressure 0.1
import AppLayouts.Onboarding2.controls 1.0 import AppLayouts.Onboarding2.controls 1.0
@ -23,6 +24,7 @@ KeycardBasePage {
signal keycardFactoryResetRequested() signal keycardFactoryResetRequested()
signal keycardLocked() signal keycardLocked()
pageClassName: "KeycardEnterPinPage"
image.source: Theme.png("onboarding/keycard/reading") image.source: Theme.png("onboarding/keycard/reading")
QtObject { QtObject {
@ -134,7 +136,9 @@ KeycardBasePage {
} }
StateChangeScript { StateChangeScript {
script: { script: {
root.keycardPinEntered(pinInput.pinInput) Backpressure.debounce(root, 2000, function() {
root.keycardPinEntered(pinInput.pinInput)
})()
} }
} }
}, },

View File

@ -17,11 +17,12 @@ KeycardBasePage {
required property string keycardState // Constants.startupState.keycardXXX required property string keycardState // Constants.startupState.keycardXXX
property bool displayPromoBanner property bool displayPromoBanner
signal reloadKeycardRequested()
signal keycardFactoryResetRequested() signal keycardFactoryResetRequested()
signal loginWithKeycardRequested() signal reloadKeycardRequested()
signal emptyKeycardDetected() signal emptyKeycardDetected()
signal notEmptyKeycardDetected()
pageClassName: "KeycardIntroPage"
OnboardingFrame { OnboardingFrame {
id: promoBanner id: promoBanner
@ -77,46 +78,22 @@ KeycardBasePage {
} }
buttons: [ buttons: [
MaybeOutlineButton {
id: btnLogin
text: qsTr("Log in with this Keycard")
onClicked: root.loginWithKeycardRequested()
},
MaybeOutlineButton { MaybeOutlineButton {
id: btnFactoryReset id: btnFactoryReset
visible: false
text: qsTr("Factory reset Keycard") text: qsTr("Factory reset Keycard")
anchors.horizontalCenter: parent.horizontalCenter
onClicked: root.keycardFactoryResetRequested() onClicked: root.keycardFactoryResetRequested()
}, },
MaybeOutlineButton { MaybeOutlineButton {
id: btnReload id: btnReload
text: qsTr("Ive inserted a Keycard") visible: false
text: qsTr("Ive inserted a different Keycard")
anchors.horizontalCenter: parent.horizontalCenter
onClicked: root.reloadKeycardRequested() 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: [ states: [
// normal/intro states // normal/intro states
State { State {
@ -139,15 +116,18 @@ KeycardBasePage {
PropertyChanges { PropertyChanges {
target: root target: root
title: qsTr("Insert your Keycard") title: qsTr("Insert your Keycard")
infoText.text: qsTr("Need a little %1?").arg(Utils.getStyledLink(qsTr("help"), "https://keycard.tech/docs/", infoText.hoveredLink, infoText.text: qsTr("Need a little %1?").arg(Utils.getStyledLink(qsTr("help"), "https://keycard.tech/docs/",
Theme.palette.baseColor1, Theme.palette.primaryColor1)) infoText.hoveredLink,
Theme.palette.baseColor1,
Theme.palette.primaryColor1))
image.source: Theme.png("onboarding/keycard/insert") image.source: Theme.png("onboarding/keycard/insert")
} }
}, },
State { State {
name: "reading" name: "reading"
when: root.keycardState === Constants.startupState.keycardReadingKeycard || when: root.keycardState === Constants.startupState.keycardReadingKeycard ||
root.keycardState === Constants.startupState.keycardInsertedKeycard root.keycardState === Constants.startupState.keycardInsertedKeycard ||
root.keycardState === Constants.startupState.keycardRecognizedKeycard
PropertyChanges { PropertyChanges {
target: root target: root
title: qsTr("Reading Keycard...") title: qsTr("Reading Keycard...")
@ -156,9 +136,42 @@ KeycardBasePage {
}, },
// error states // error states
State { State {
name: "error" name: "notKeycard"
when: root.keycardState === Constants.startupState.keycardWrongKeycard ||
root.keycardState === Constants.startupState.keycardNotKeycard
PropertyChanges { PropertyChanges {
target: root target: root
title: qsTr("Oops this isnt 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") image.source: Theme.png("onboarding/keycard/error")
} }
PropertyChanges { PropertyChanges {
@ -170,63 +183,38 @@ KeycardBasePage {
visible: true 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 isnt 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 { State {
name: "locked" name: "locked"
extend: "error"
when: root.keycardState === Constants.startupState.keycardLocked when: root.keycardState === Constants.startupState.keycardLocked
PropertyChanges { PropertyChanges {
target: root target: root
title: qsTr("Keycard locked") title: "<font color='%1'>".arg(Theme.palette.dangerColor1) + qsTr("Keycard locked") + "</font>"
subtitle: qsTr("The Keycard you have inserted is locked, you will need to factory reset it or insert a different one") 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")
},
State {
name: "notEmpty"
extend: "error"
when: root.keycardState === Constants.startupState.keycardNotEmpty
PropertyChanges {
target: root
title: qsTr("Keycard is not empty")
subtitle: qsTr("You cant use it to store new keys right now")
} }
PropertyChanges { PropertyChanges {
target: btnLogin target: btnFactoryReset
visible: true
}
PropertyChanges {
target: btnReload
visible: true visible: true
} }
}, },
// success/exit state // exit states
State { State {
name: "emptyDetected" name: "empty"
when: root.keycardState === Constants.startupState.keycardEmpty when: root.keycardState === Constants.startupState.keycardEmpty
StateChangeScript { StateChangeScript {
script: root.emptyKeycardDetected() script: root.emptyKeycardDetected()
} }
},
State {
name: "notEmpty"
when: root.keycardState === Constants.startupState.keycardNotEmpty
StateChangeScript {
script: root.notEmptyKeycardDetected()
}
} }
] ]
} }

View File

@ -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 cant use it to store new keys right now")
image.source: Theme.png("onboarding/keycard/error")
pageClassName: "KeycardNotEmptyPage"
buttons: [
MaybeOutlineButton {
id: btnReload
text: qsTr("Ive 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()
}
]
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,174 @@
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: 20
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("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(268, parent.width)
Layout.preferredHeight: Math.min(164, 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()
}
}
}
OnboardingFrame {
id: buttonFrame
Layout.fillWidth: true
padding: 1
dropShadow: false
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()
}
}
}
}
}
}
}

View File

@ -4,6 +4,8 @@ import QtQuick.Controls 2.15
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
Page { Page {
required property string pageClassName
signal openLink(string link) signal openLink(string link)
signal openLinkWithConfirmation(string link, string domain) signal openLinkWithConfirmation(string link, string domain)

View File

@ -18,6 +18,8 @@ OnboardingPage {
signal seedphraseValidated() signal seedphraseValidated()
pageClassName: "SeedphrasePage"
contentItem: Item { contentItem: Item {
ColumnLayout { ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent

View File

@ -15,6 +15,7 @@ import utils 1.0
OnboardingPage { OnboardingPage {
id: root id: root
pageClassName: "WelcomePage"
title: qsTr("Welcome to Status") title: qsTr("Welcome to Status")
signal createProfileRequested() signal createProfileRequested()

View File

@ -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 BackupSeedphraseAcks 1.0 BackupSeedphraseAcks.qml
BackupSeedphraseIntro 1.0 BackupSeedphraseIntro.qml
BackupSeedphraseOutro 1.0 BackupSeedphraseOutro.qml
BackupSeedphraseReveal 1.0 BackupSeedphraseReveal.qml BackupSeedphraseReveal 1.0 BackupSeedphraseReveal.qml
BackupSeedphraseVerify 1.0 BackupSeedphraseVerify.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

View File

@ -71,7 +71,7 @@ Column {
if (root.type === SyncingCodeInstructions.Type.EncryptedKey) { if (root.type === SyncingCodeInstructions.Type.EncryptedKey) {
return qsTr("Copy the") return qsTr("Copy the")
} }
return qsTr("Enable camera") return qsTr("Enable camera access")
} }
return qsTr("Click") return qsTr("Click")
} }
@ -122,7 +122,7 @@ Column {
} }
return "" return ""
} }
return qsTr("Enable camera") return qsTr("Enable camera access")
} }
text2Color: Theme.palette.directColor1 text2Color: Theme.palette.directColor1
text3: { text3: {

View File

@ -166,7 +166,7 @@ Column {
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
font.pixelSize: Theme.tertiaryTextFontSize font.pixelSize: Theme.tertiaryTextFontSize
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: qsTr("Ensure both devices are on the same network") text: qsTr("Ensure both devices are on the same local network")
} }
StatusBaseText { StatusBaseText {

View File

@ -1,4 +1,4 @@
import QtQuick 2.14 import QtQuick 2.15
import StatusQ.Popups.Dialog 0.1 import StatusQ.Popups.Dialog 0.1
@ -7,7 +7,7 @@ import shared.views 1.0
StatusDialog { StatusDialog {
id: root id: root
title: qsTr("How to get a sync code on...") title: qsTr("How to get a pairing code on...")
horizontalPadding: 24 horizontalPadding: 24
verticalPadding: 32 verticalPadding: 32
footer: null footer: null

View File

@ -1,5 +1,5 @@
import QtQuick 2.14 import QtQuick 2.15
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.15
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
@ -13,48 +13,54 @@ ColumnLayout {
id: root id: root
property string firstTabName: qsTr("Scan QR code") property string firstTabName: qsTr("Scan QR code")
property string secondTabName: qsTr("Enter sync code") property string secondTabName: qsTr("Enter code")
property string firstInstructionButtonName: qsTr("How to get a sync code") property string firstInstructionButtonName: qsTr("How to get a pairing code")
property string secondInstructionButtonName: qsTr("How to get a sync code") property string secondInstructionButtonName: qsTr("How to get a pairing code")
property string syncQrErrorMessage: qsTr("This does not look like a sync QR code") property string syncQrErrorMessage: qsTr("This does not look like a pairing QR code")
property string syncCodeErrorMessage: qsTr("This does not look like a sync code") property string syncCodeErrorMessage: qsTr("This does not look like a pairing code")
property string syncCodeLabel: qsTr("Paste sync 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 true }
readonly property bool syncViaQr: !switchTabBar.currentIndex readonly property bool syncViaQr: !switchTabBar.currentIndex
signal displayInstructions() signal displayInstructions()
signal proceed(string connectionString) signal proceed(string connectionString)
spacing: 8 spacing: Theme.halfPadding
StatusSwitchTabBar { RowLayout {
id: switchTabBar spacing: root.spacing
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: Theme.bigPadding
Layout.rightMargin: 16 Layout.rightMargin: Theme.bigPadding
currentIndex: 0
StatusSwitchTabButton { StatusSwitchTabBar {
text: root.firstTabName Layout.fillWidth: true
Layout.leftMargin: betaTag.visible ? betaTag.width : 0
id: switchTabBar
currentIndex: 0
StatusSwitchTabButton {
text: root.firstTabName
}
StatusSwitchTabButton {
text: root.secondTabName
}
} }
StatusSwitchTabButton { StatusBetaTag {
text: root.secondTabName id: betaTag
} }
} }
StatusBetaTag {
anchors.left: switchTabBar.right
anchors.leftMargin: 8
anchors.verticalCenter: switchTabBar.verticalCenter
}
StackLayout { StackLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Math.max(syncQr.implicitHeight, syncCode.implicitHeight) Layout.preferredHeight: Math.max(syncQr.implicitHeight, syncCode.implicitHeight)
Layout.topMargin: 24 Layout.topMargin: Theme.bigPadding
currentIndex: switchTabBar.currentIndex currentIndex: switchTabBar.currentIndex
// StackLayout doesn't support alignment, so we create an `Item` wrappers // StackLayout doesn't support alignment, so we create an `Item` wrappers
@ -80,11 +86,12 @@ ColumnLayout {
} }
ColumnLayout { ColumnLayout {
spacing: 20 Layout.topMargin: Theme.padding
spacing: Theme.padding
StatusSyncCodeInput { StatusSyncCodeInput {
id: syncCode id: syncCode
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 424 Layout.preferredWidth: 440
mode: StatusSyncCodeInput.Mode.WriteMode mode: StatusSyncCodeInput.Mode.WriteMode
label: root.syncCodeLabel label: root.syncCodeLabel
@ -97,30 +104,36 @@ ColumnLayout {
validate: root.validateConnectionString validate: root.validateConnectionString
} }
] ]
input.onValidChanged: {
if (!input.valid)
return
root.proceed(syncCode.text)
}
} }
StatusBaseText { StatusBaseText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
font.pixelSize: Theme.tertiaryTextFontSize 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 { StatusFlatButton {
Layout.topMargin: Theme.xlPadding
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
visible: switchTabBar.currentIndex == 0 && !!root.firstInstructionButtonName || visible: switchTabBar.currentIndex == 0 && !!root.firstInstructionButtonName ||
switchTabBar.currentIndex == 1 && !!root.secondInstructionButtonName switchTabBar.currentIndex == 1 && !!root.secondInstructionButtonName
text: switchTabBar.currentIndex == 0? text: switchTabBar.currentIndex == 0?
root.firstInstructionButtonName : root.firstInstructionButtonName :
root.secondInstructionButtonName root.secondInstructionButtonName
font.pixelSize: Theme.additionalTextSize
normalColor: "transparent"
borderWidth: 1
borderColor: Theme.palette.baseColor2
onClicked: { onClicked: {
root.displayInstructions() root.displayInstructions()
} }