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
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)

View File

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

View File

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

View File

@ -8349,6 +8349,7 @@
<file>assets/png/onboarding/status_keycard.png</file>
<file>assets/png/onboarding/status_keycard_multiple.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/keycard/empty.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.views 1.0

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Core 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
ListItemButton 1.0 ListItemButton.qml
MaybeOutlineButton 1.0 MaybeOutlineButton.qml

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,6 +21,8 @@ OnboardingPage {
signal createProfileWithSeedphraseRequested()
signal createProfileWithEmptyKeycardRequested()
pageClassName: "CreateProfilePage"
contentItem: Item {
ColumnLayout {
anchors.centerIn: parent

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ KeycardBasePage {
signal keycardPinCreated(string pin)
pageClassName: "KeycardCreatePinPage"
image.source: Theme.png("onboarding/keycard/reading")
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.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)
})()
}
}
},

View File

@ -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("Ive inserted a Keycard")
visible: false
text: qsTr("Ive 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 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")
}
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 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 {
name: "locked"
extend: "error"
when: root.keycardState === Constants.startupState.keycardLocked
PropertyChanges {
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")
}
},
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")
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()
}
}
]
}

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
Page {
required property string pageClassName
signal openLink(string link)
signal openLinkWithConfirmation(string link, string domain)

View File

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

View File

@ -15,6 +15,7 @@ import utils 1.0
OnboardingPage {
id: root
pageClassName: "WelcomePage"
title: qsTr("Welcome to Status")
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
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

View File

@ -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: {

View File

@ -166,7 +166,7 @@ Column {
color: Theme.palette.baseColor1
font.pixelSize: Theme.tertiaryTextFontSize
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 {

View File

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

View File

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