diff --git a/libs/StatusQ/qml/Status/Core/StatusBaseText.qml b/libs/StatusQ/qml/Status/Core/StatusBaseText.qml index bc128ebfac2..5cc724774aa 100644 --- a/libs/StatusQ/qml/Status/Core/StatusBaseText.qml +++ b/libs/StatusQ/qml/Status/Core/StatusBaseText.qml @@ -17,7 +17,7 @@ import Status.Core.Theme width: 240 text: qsTr("Hello World!") font.pixelSize: 24 - color: Theme.pallete.directColor1 + color: Theme.palette.directColor1 } \endqml diff --git a/storybook/pages/ColorsPage.qml b/storybook/pages/ColorsPage.qml index c8d4b686394..fc05a4c3e5c 100644 --- a/storybook/pages/ColorsPage.qml +++ b/storybook/pages/ColorsPage.qml @@ -146,6 +146,10 @@ SplitView { enabled: searchField.searchText !== "" onClicked: searchField.clear() } + Label { + text: "INFO: Reload the page after selecting 'Dark mode'" + font.weight: Font.Medium + } } ColorFlow { diff --git a/storybook/pages/OnboardingLayoutPage.qml b/storybook/pages/OnboardingLayoutPage.qml new file mode 100644 index 00000000000..4975758fae8 --- /dev/null +++ b/storybook/pages/OnboardingLayoutPage.qml @@ -0,0 +1,173 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml 2.15 + +import StatusQ 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Theme 0.1 + +import Models 1.0 +import Storybook 1.0 + +import utils 1.0 + +import AppLayouts.Onboarding2 1.0 + +import shared.stores 1.0 as SharedStores + +// compat +import AppLayouts.Onboarding.stores 1.0 as OOBS + +SplitView { + id: root + orientation: Qt.Vertical + + Logs { id: logs } + + QtObject { + id: keycardMock + property string stateType: ctrlKeycardState.currentValue + + readonly property var keycardStates: [ + // initial + //Constants.startupState.keycardNoPCSCService, + Constants.startupState.keycardPluginReader, + Constants.startupState.keycardInsertKeycard, + Constants.startupState.keycardInsertedKeycard, Constants.startupState.keycardReadingKeycard, + // initial errors + Constants.startupState.keycardWrongKeycard, Constants.startupState.keycardNotKeycard, + Constants.startupState.keycardMaxPairingSlotsReached, + Constants.startupState.keycardLocked, + Constants.startupState.keycardNotEmpty, + // create keycard profile + Constants.startupState.keycardEmpty + ] + } + + OnboardingLayout { + id: onboarding + SplitView.fillWidth: true + SplitView.fillHeight: true + startupStore: OOBS.StartupStore { + property var currentStartupState: QtObject { + property string stateType: keycardMock.stateType + } + + function getPasswordStrengthScore(password) { + return Math.min(password.length-1, 4) + } + function validMnemonic(mnemonic) { + return true + } + function getPin() { + return "" // FIXME make configurable + } + } + metricsStore: SharedStores.MetricsStore { + readonly property var d: QtObject { + id: d + property bool isCentralizedMetricsEnabled + } + + function toggleCentralizedMetrics(enabled) { + d.isCentralizedMetricsEnabled = enabled + } + + function addCentralizedMetricIfEnabled(eventName, eventValue = null) {} + + readonly property bool isCentralizedMetricsEnabled : d.isCentralizedMetricsEnabled + } + splashScreenDurationMs: 3000 + + QtObject { + id: localAppSettings + 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) + + console.warn("!!! RESTARTING FLOW") + restartFlow() + ctrlKeycardState.currentIndex = 0 + } + onKeycardFactoryResetRequested: { + logs.logEvent("onKeycardFactoryResetRequested") + console.warn("!!! FACTORY RESET; RESTARTING FLOW") + restartFlow() + ctrlKeycardState.currentIndex = 0 + } + onKeycardReloaded: { + logs.logEvent("onKeycardReloaded") + ctrlKeycardState.currentIndex = 0 + } + } + + Connections { + target: Global + function onOpenLink(link: string) { + console.debug("Opening link in an external web browser:", link) + Qt.openUrlExternally(link) + } + function onOpenLinkWithConfirmation(link: string, domain: string) { + console.debug("Opening link in an external web browser:", link, domain) + Qt.openUrlExternally(link) + } + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 150 + SplitView.preferredHeight: 150 + + logsView.logText: logs.logText + + RowLayout { + anchors.fill: parent + ColumnLayout { + Layout.fillWidth: true + Label { + text: "Current page: %1".arg(onboarding.stack.currentItem ? onboarding.stack.currentItem.title : "") + } + Label { + text: `Current path: ${onboarding.primaryPath} -> ${onboarding.secondaryPath}` + } + Label { + text: "Stack depth: %1".arg(onboarding.stack.depth) + } + } + Item { Layout.fillWidth: true } + Button { + text: "Restart" + focusPolicy: Qt.NoFocus + onClicked: onboarding.restartFlow() + } + Button { + text: "Copy password" + focusPolicy: Qt.NoFocus + onClicked: ClipboardUtils.setText("0123456789") + } + Button { + text: "Copy seedphrase" + focusPolicy: Qt.NoFocus + onClicked: ClipboardUtils.setText("dog dog dog dog dog dog dog dog dog dog dog dog") + } + ComboBox { + Layout.preferredWidth: 250 + id: ctrlKeycardState + focusPolicy: Qt.NoFocus + model: keycardMock.keycardStates + } + } + } +} + +// category: Onboarding +// status: good +// https://www.figma.com/design/Lw4nPYQcZOPOwTgETiiIYo/Desktop-Onboarding-Redesign?node-id=1-25&node-type=canvas&m=dev diff --git a/storybook/pages/StatusPinInputPage.qml b/storybook/pages/StatusPinInputPage.qml new file mode 100644 index 00000000000..b33b4e65b85 --- /dev/null +++ b/storybook/pages/StatusPinInputPage.qml @@ -0,0 +1,41 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 +import StatusQ.Core.Theme 0.1 + +Item { + id: root + + ColumnLayout { + anchors.centerIn: parent + spacing: 16 + StatusBaseText { + Layout.alignment: Qt.AlignHCenter + text: "ENTER NUMERIC PIN, EXPECTED LENGTH: %1".arg(pinInput.pinLen) + } + StatusPinInput { + Layout.alignment: Qt.AlignHCenter + id: pinInput + validator: StatusIntValidator { bottom: 0; top: 999999 } + Component.onCompleted: { + statesInitialization() + forceFocus() + } + } + StatusBaseText { + Layout.alignment: Qt.AlignHCenter + text: "ENTERED PIN: %1".arg(pinInput.pinInput || "[empty]") + } + StatusBaseText { + Layout.alignment: Qt.AlignHCenter + text: "VALID: %1".arg(pinInput.valid ? "true" : "false") + } + } +} + +// category: Controls +// status: good diff --git a/storybook/stubs/shared/stores/BIP39_en.qml b/storybook/stubs/shared/stores/BIP39_en.qml index b275a1e6852..084cecbf35f 100644 --- a/storybook/stubs/shared/stores/BIP39_en.qml +++ b/storybook/stubs/shared/stores/BIP39_en.qml @@ -5,7 +5,7 @@ ListModel { Component.onCompleted: { var englishWords = [ - "apple", "banana", "cat", "cow", "catalog", "catch", "category", "cattle", "dog", "elephant", "fish", "grape", "horse", "ice cream", "jellyfish", + "age", "agent", "apple", "banana", "cat", "cow", "catalog", "catch", "category", "cattle", "dog", "elephant", "fish", "grape", "horse", "ice cream", "jellyfish", "kiwi", "lemon", "mango", "nut", "orange", "pear", "quail", "rabbit", "strawberry", "turtle", "umbrella", "violet", "watermelon", "xylophone", "yogurt", "zebra" // Add more English words here... diff --git a/storybook/stubs/shared/stores/MetricsStore.qml b/storybook/stubs/shared/stores/MetricsStore.qml new file mode 100644 index 00000000000..2587cd419c7 --- /dev/null +++ b/storybook/stubs/shared/stores/MetricsStore.qml @@ -0,0 +1,3 @@ +import QtQml 2.15 + +QtObject {} diff --git a/storybook/stubs/shared/stores/qmldir b/storybook/stubs/shared/stores/qmldir index d7a633428cd..b907b1108bc 100644 --- a/storybook/stubs/shared/stores/qmldir +++ b/storybook/stubs/shared/stores/qmldir @@ -8,3 +8,4 @@ PermissionsStore 1.0 PermissionsStore.qml ProfileStore 1.0 ProfileStore.qml RootStore 1.0 RootStore.qml UtilsStore 1.0 UtilsStore.qml +MetricsStore 1.0 MetricsStore.qml diff --git a/ui/StatusQ/src/StatusQ/Components/StatusImage.qml b/ui/StatusQ/src/StatusQ/Components/StatusImage.qml index 608414d4ab1..c2b1915c426 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusImage.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusImage.qml @@ -1,5 +1,4 @@ -import QtQuick 2.13 -import QtQuick.Window 2.15 +import QtQuick 2.15 /*! \qmltype StatusImage diff --git a/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml b/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml index 90180869cad..1644ff7b13f 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusSmartIdenticon.qml @@ -53,6 +53,7 @@ Loader { objectName: "statusRoundImage" width: parent.width height: parent.height + radius: asset.bgRadius image.source: root.asset.isImage ? root.asset.name : "" showLoadingIndicator: true border.width: root.asset.imgIsIdenticon ? 1 : 0 diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusPasswordStrengthIndicator.qml b/ui/StatusQ/src/StatusQ/Controls/StatusPasswordStrengthIndicator.qml index d8c58f95e65..013de9caf7d 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusPasswordStrengthIndicator.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusPasswordStrengthIndicator.qml @@ -74,7 +74,7 @@ StatusProgressBar { Default value: "So-so" */ - property string labelSoso: qsTr("So-so") + property string labelSoso: qsTr("Okay") /*! \qmlproperty string StatusPasswordStrengthIndicator::labelGood This property holds the text shown when the strength is StatusPasswordStrengthIndicator.Strength.Good. @@ -88,7 +88,7 @@ StatusProgressBar { Default value: "Great" */ - property string labelGreat: qsTr("Great") + property string labelGreat: qsTr("Very strong") enum Strength { None, // 0 diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusPinInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusPinInput.qml index 2a7e16e9cc8..9de6ce9a8e0 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusPinInput.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusPinInput.qml @@ -42,7 +42,7 @@ Item { property alias pinInput: inputText.text /*! - \qmlproperty Validator StatusPinInput::validator + \qmlproperty StatusValidator StatusPinInput::validator This property allows you to set a validator on the StatusPinInput. When a validator is set the StatusPinInput will only accept input which leaves the pinInput property in an acceptable state. @@ -59,6 +59,13 @@ Item { */ property alias validator: d.statusValidator + /*! + \qmlproperty bool StatusPinInput::pinInput + This property holds whether the entered PIN is valid; PIN is considered valid when it passes the internal validator + and its length matches that of @p pinLen + */ + readonly property bool valid: inputText.acceptableInput && inputText.length === pinLen + /*! \qmlproperty int StatusPinInput::pinLen This property allows you to set a specific pin input length. The default value is 6. diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml index c252a2691fd..2e2ad3bd65a 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusSeedPhraseInput.qml @@ -117,9 +117,10 @@ Item { Component { id: seedInputLeftComponent StatusBaseText { - leftPadding: 4 - rightPadding: 6 + leftPadding: text.length == 1 ? 10 : 6 + rightPadding: 4 text: root.leftComponentText + font.family: Theme.monoFont.name color: seedWordInput.input.edit.activeFocus ? Theme.palette.primaryColor1 : Theme.palette.baseColor1 } @@ -197,7 +198,7 @@ Item { id: suggListContainer contentWidth: seedSuggestionsList.width contentHeight: ((seedSuggestionsList.count <= 5) ? seedSuggestionsList.count : 5) *34 - x: 16 + x: 0 y: seedWordInput.height + 4 topPadding: 8 bottomPadding: 8 diff --git a/ui/StatusQ/src/StatusQ/Controls/Validators/StatusIntValidator.qml b/ui/StatusQ/src/StatusQ/Controls/Validators/StatusIntValidator.qml index b3f1ba7e51e..1ee9d15577d 100644 --- a/ui/StatusQ/src/StatusQ/Controls/Validators/StatusIntValidator.qml +++ b/ui/StatusQ/src/StatusQ/Controls/Validators/StatusIntValidator.qml @@ -1,4 +1,4 @@ -import QtQuick 2.14 +import QtQuick 2.15 import StatusQ.Controls 0.1 diff --git a/ui/StatusQ/src/StatusQ/Core/Theme/StatusColors.qml b/ui/StatusQ/src/StatusQ/Core/Theme/StatusColors.qml index abb02f68d9c..b6ee9b6d1ff 100644 --- a/ui/StatusQ/src/StatusQ/Core/Theme/StatusColors.qml +++ b/ui/StatusQ/src/StatusQ/Core/Theme/StatusColors.qml @@ -86,5 +86,8 @@ QtObject { 'lightDesktopBlue10': '#ECEFFB', 'darkDesktopBlue10': '#273251', + + // new/mobile colors + 'neutral-95': '#0D1625' } } diff --git a/ui/StatusQ/src/StatusQ/Popups/StatusSimpleTextPopup.qml b/ui/StatusQ/src/StatusQ/Popups/StatusSimpleTextPopup.qml new file mode 100644 index 00000000000..a2a31c9c1bf --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Popups/StatusSimpleTextPopup.qml @@ -0,0 +1,24 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Popups.Dialog 0.1 + +StatusDialog { + width: 600 + padding: 0 + standardButtons: Dialog.Ok + + property alias content: contentText + + StatusScrollView { + id: scrollView + anchors.fill: parent + contentWidth: availableWidth + StatusBaseText { + id: contentText + width: scrollView.availableWidth + wrapMode: Text.Wrap + } + } +} diff --git a/ui/StatusQ/src/StatusQ/Popups/qmldir b/ui/StatusQ/src/StatusQ/Popups/qmldir index a5acf21cb0e..db0110b4a8c 100644 --- a/ui/StatusQ/src/StatusQ/Popups/qmldir +++ b/ui/StatusQ/src/StatusQ/Popups/qmldir @@ -14,5 +14,6 @@ StatusModalDivider 0.1 StatusModalDivider.qml StatusSearchLocationMenu 0.1 StatusSearchLocationMenu.qml StatusSearchPopup 0.1 StatusSearchPopup.qml StatusSearchPopupMenuItem 0.1 StatusSearchPopupMenuItem.qml +StatusSimpleTextPopup 0.1 StatusSimpleTextPopup.qml StatusStackModal 0.1 StatusStackModal.qml StatusSuccessAction 0.1 StatusSuccessAction.qml diff --git a/ui/StatusQ/src/assets.qrc b/ui/StatusQ/src/assets.qrc index 8f7d43516fa..8e5d505bfb0 100644 --- a/ui/StatusQ/src/assets.qrc +++ b/ui/StatusQ/src/assets.qrc @@ -8339,6 +8339,22 @@ assets/png/onboarding/profile_fetching_in_progress.png assets/png/onboarding/seed-phrase.png assets/png/onboarding/welcome.png + assets/png/onboarding/status_totebag_artwork_1.png + assets/png/onboarding/status_generate_keys.png + assets/png/onboarding/status_generate_keycard.png + assets/png/onboarding/create_profile_seed.png + assets/png/onboarding/create_profile_keycard.png + assets/png/onboarding/status_chat.png + assets/png/onboarding/status_key.png + assets/png/onboarding/status_keycard.png + assets/png/onboarding/status_keycard_multiple.png + assets/png/onboarding/enable_biometrics.png + assets/png/onboarding/keycard/empty.png + assets/png/onboarding/keycard/insert.png + assets/png/onboarding/keycard/invalid.png + assets/png/onboarding/keycard/reading.png + assets/png/onboarding/keycard/error.png + assets/png/onboarding/keycard/success.png assets/png/onRampProviders/latamex.png assets/png/onRampProviders/mercuryo.png assets/png/onRampProviders/moonPay.png diff --git a/ui/StatusQ/src/assets/png/onboarding/create_profile_keycard.png b/ui/StatusQ/src/assets/png/onboarding/create_profile_keycard.png new file mode 100644 index 00000000000..993e3bacf3d Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/create_profile_keycard.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/create_profile_seed.png b/ui/StatusQ/src/assets/png/onboarding/create_profile_seed.png new file mode 100644 index 00000000000..ba8cf2fc3ec Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/create_profile_seed.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/enable_biometrics.png b/ui/StatusQ/src/assets/png/onboarding/enable_biometrics.png new file mode 100644 index 00000000000..83538879b0f Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/enable_biometrics.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/empty.png b/ui/StatusQ/src/assets/png/onboarding/keycard/empty.png new file mode 100644 index 00000000000..aa50cf90987 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/empty.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/error.png b/ui/StatusQ/src/assets/png/onboarding/keycard/error.png new file mode 100644 index 00000000000..245ea6610b6 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/error.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/insert.png b/ui/StatusQ/src/assets/png/onboarding/keycard/insert.png new file mode 100644 index 00000000000..4ea58220923 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/insert.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/invalid.png b/ui/StatusQ/src/assets/png/onboarding/keycard/invalid.png new file mode 100644 index 00000000000..b9e1258c4c8 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/invalid.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/reading.png b/ui/StatusQ/src/assets/png/onboarding/keycard/reading.png new file mode 100644 index 00000000000..afcd019ee48 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/reading.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/keycard/success.png b/ui/StatusQ/src/assets/png/onboarding/keycard/success.png new file mode 100644 index 00000000000..80f41fc28c1 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/keycard/success.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_chat.png b/ui/StatusQ/src/assets/png/onboarding/status_chat.png new file mode 100644 index 00000000000..7757a05b6d0 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_chat.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_generate_keycard.png b/ui/StatusQ/src/assets/png/onboarding/status_generate_keycard.png new file mode 100644 index 00000000000..c56732d540d Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_generate_keycard.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_generate_keys.png b/ui/StatusQ/src/assets/png/onboarding/status_generate_keys.png new file mode 100644 index 00000000000..4c348579336 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_generate_keys.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_key.png b/ui/StatusQ/src/assets/png/onboarding/status_key.png new file mode 100644 index 00000000000..37d2b2ac4f6 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_key.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_keycard.png b/ui/StatusQ/src/assets/png/onboarding/status_keycard.png new file mode 100644 index 00000000000..68755a7c5a1 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_keycard.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_keycard_multiple.png b/ui/StatusQ/src/assets/png/onboarding/status_keycard_multiple.png new file mode 100644 index 00000000000..965e41663e9 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_keycard_multiple.png differ diff --git a/ui/StatusQ/src/assets/png/onboarding/status_totebag_artwork_1.png b/ui/StatusQ/src/assets/png/onboarding/status_totebag_artwork_1.png new file mode 100644 index 00000000000..ce8f1dd1d35 Binary files /dev/null and b/ui/StatusQ/src/assets/png/onboarding/status_totebag_artwork_1.png differ diff --git a/ui/StatusQ/src/statusq.qrc b/ui/StatusQ/src/statusq.qrc index b8adcf53540..b0e3df4a701 100644 --- a/ui/StatusQ/src/statusq.qrc +++ b/ui/StatusQ/src/statusq.qrc @@ -245,6 +245,7 @@ StatusQ/Popups/StatusSearchLocationMenu.qml StatusQ/Popups/StatusSearchPopup.qml StatusQ/Popups/StatusSearchPopupMenuItem.qml + StatusQ/Popups/StatusSimpleTextPopup.qml StatusQ/Popups/StatusStackModal.qml StatusQ/Popups/StatusSuccessAction.qml StatusQ/Popups/qmldir diff --git a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml new file mode 100644 index 00000000000..684d1ac1871 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml @@ -0,0 +1,377 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import Qt.labs.settings 1.1 + +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils +import StatusQ.Core.Backpressure 0.1 +import StatusQ.Popups 0.1 + +import AppLayouts.Onboarding2.pages 1.0 + +import shared.panels 1.0 +import shared.stores 1.0 as SharedStores +import utils 1.0 + +// compat +import AppLayouts.Onboarding.stores 1.0 as OOBS + +Page { + id: root + + property OOBS.StartupStore startupStore: OOBS.StartupStore {} // TODO replace with a new OnboardingStore, with just the needed props/functions? + required property SharedStores.MetricsStore metricsStore // TODO externalize the centralized metrics handling too? + + property int splashScreenDurationMs: 30000 + + readonly property alias stack: stack + readonly property alias primaryPath: d.primaryPath + readonly property alias secondaryPath: d.secondaryPath + + signal finished(bool success, int primaryPath, int secondaryPath) + signal keycardFactoryResetRequested() // TODO integrate/switch to an external flow + signal keycardReloaded() + + function restartFlow() { + stack.replace(welcomePage) // rewind to Welcome page + d.resetState() + } + + QtObject { + id: d + // logic + property int primaryPath: OnboardingLayout.PrimaryPath.Unknown + property int secondaryPath: OnboardingLayout.SecondaryPath.Unknown + readonly property string currentKeycardState: root.startupStore.currentStartupState.stateType + + // UI + readonly property int opacityDuration: 50 + readonly property int swipeDuration: 400 + + // state + property string password + property bool enableBiometrics + property string keycardPin + + function resetState() { + d.primaryPath = OnboardingLayout.PrimaryPath.Unknown + d.secondaryPath = OnboardingLayout.SecondaryPath.Unknown + d.password = "" + d.keycardPin = "" + d.enableBiometrics = false + } + + readonly property Settings settings: Settings { + property bool keycardPromoShown // whether we've seen the keycard promo banner on KeycardIntroPage + } + } + + enum PrimaryPath { + Unknown, + CreateProfile, + Login + } + + enum SecondaryPath { + Unknown, + CreateProfileWithPassword, + CreateProfileWithSeedphrase, + CreateProfileWithKeycard, + CreateProfileWithKeycardNewSeedphrase, + CreateProfileWithKeycardExistingSeedphrase + // TODO secondary Login paths + } + + // page stack + StackView { + id: stack + anchors.fill: parent + initialItem: welcomePage + + 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 } + } + } + pushExit: Transition { + NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 50; 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 } + } + } + popExit: pushExit + replaceEnter: pushEnter + replaceExit: pushExit + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.BackButton + enabled: stack.depth > 1 && !stack.busy + cursorShape: undefined // fall thru + onClicked: stack.pop() + } + + // back button + StatusButton { + objectName: "onboardingBackButton" + isRoundIcon: true + 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() + } + + // main signal handler + Connections { + target: stack.currentItem + ignoreUnknownSignals: true + + // common popups + function onPrivacyPolicyRequested() { + console.warn("!!! AUX: PRIVACY POLICY") + privacyPolicyPopup.createObject(root).open() + } + function onTermsOfUseRequested() { + console.warn("!!! AUX: TERMS OF USE") + termsOfUsePopup.createObject(root).open() + } + function onOpenLink(link: string) { + Global.openLink(link) + } + function onOpenLinkWithConfirmation(link: string, domain: string) { + Global.openLinkWithConfirmation(link, domain) + } + + // welcome page + function onCreateProfileRequested() { + console.warn("!!! PRIMARY: CREATE PROFILE") + d.primaryPath = OnboardingLayout.PrimaryPath.CreateProfile + stack.push(helpUsImproveStatusPage) + } + function onLoginRequested() { + console.warn("!!! PRIMARY: LOG IN") + d.primaryPath = OnboardingLayout.PrimaryPath.Login + } + + // help us improve page + function onShareUsageDataRequested(enabled: bool) { + console.warn("!!! SHARE USAGE DATA:", enabled) + metricsStore.toggleCentralizedMetrics(enabled) + Global.addCentralizedMetricIfEnabled("usage_data_shared", {placement: Constants.metricsEnablePlacement.onboarding}) + localAppSettings.metricsPopupSeen = true + + if (d.primaryPath === OnboardingLayout.PrimaryPath.CreateProfile) + stack.push(createProfilePage) + else if (d.primaryPath === OnboardingLayout.PrimaryPath.Login) + ; // TODO Login path + } + + // create profile page + function onCreateProfileWithPasswordRequested() { + console.warn("!!! SECONDARY: CREATE PROFILE WITH PASSWORD") + d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithPassword + stack.push(createPasswordPage) + } + function onCreateProfileWithSeedphraseRequested() { + console.warn("!!! SECONDARY: CREATE PROFILE WITH SEEDPHRASE") + d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithSeedphrase + stack.push(seedphrasePage, { title: qsTr("Create profile with a seed phrase"), subtitle: qsTr("Enter your 12, 18 or 24 word seed phrase")}) + } + function onCreateProfileWithEmptyKeycardRequested() { + console.warn("!!! SECONDARY: CREATE PROFILE WITH KEYCARD") + d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithKeycard + stack.push(keycardIntroPage) + } + + // create password page + function onSetPasswordRequested(password: string) { + console.warn("!!! SET PASSWORD REQUESTED") + d.password = password + stack.push(enableBiometricsPage, {subtitle: qsTr("Use biometrics to fill in your password?")}) // FIXME make optional on unsupported platforms + } + + // seedphrase page + function onSeedphraseValidated() { + console.warn("!!! SEEDPHRASE VALIDATED") + if (d.secondaryPath === OnboardingLayout.SecondaryPath.CreateProfileWithSeedphrase) { + console.warn("!!! AFTER SEEDPHRASE -> PASSWORD PAGE") + stack.push(createPasswordPage) + } + } + + // keycard pages + function onReloadKeycardRequested() { + console.warn("!!! RELOAD KEYCARD REQUESTED") + root.keycardReloaded() + stack.replace(keycardIntroPage) + } + function onKeycardFactoryResetRequested() { + console.warn("!!! KEYCARD FACTORY RESET REQUESTED") + root.keycardFactoryResetRequested() + } + function onLoginWithKeycardRequested() { + // TODO Login path + } + function onEmptyKeycardDetected() { + console.warn("!!! EMPTY KEYCARD DETECTED") + stack.replace(createKeycardProfilePage) // NB: replacing the keycardIntroPage + } + + function onCreateKeycardProfileWithNewSeedphrase() { + console.warn("!!! CREATE KEYCARD PROFILE WITH NEW SEEDPHRASE") + d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithKeycardNewSeedphrase + + if (root.startupStore.getPin()) + ; // TODO check for existing PIN; push keycardEnterPinPage instead + else + stack.push(keycardCreatePinPage) + } + function onCreateKeycardProfileWithExistingSeedphrase() { + console.warn("!!! CREATE KEYCARD PROFILE WITH EXISTING SEEDPHRASE") + d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithKeycardExistingSeedphrase + // TODO check for existing PIN; push keycardEnterPinPage instead + // TODO seedphrasePage + keycardCreatePinPage + // stack.push(keycardEnterPinPage) // TODO + } + + function onKeycardPinCreated(pin) { + console.warn("!!! KEYCARD PIN CREATED:", pin) + d.keycardPin = pin + stack.replace(null, stack.currentItem) // replace everything with "Keycard PIN set" page, clear history, no Back button at this point + Backpressure.debounce(root, 2000, function() { + stack.replace(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.")}) + })() + } + + // enable biometrics page + function onEnableBiometricsRequested(enabled: bool) { + console.warn("!!! ENABLE BIOMETRICS:", enabled) + d.enableBiometrics = enabled + if (d.secondaryPath === OnboardingLayout.SecondaryPath.CreateProfileWithKeycardNewSeedphrase) + ; // TODO backup seedphrase pages + else + stack.push(splashScreen, { runningProgressAnimation: true }) + } + } + + // pages + Component { + id: welcomePage + WelcomePage { + StackView.onActivated: d.resetState() + } + } + + Component { + id: helpUsImproveStatusPage + HelpUsImproveStatusPage {} + } + + Component { + id: createProfilePage + CreateProfilePage { + StackView.onActivated: d.secondaryPath = OnboardingLayout.SecondaryPath.Unknown // reset when we get back here + } + } + + Component { + id: createPasswordPage + CreatePasswordPage { + passwordStrengthScoreFunction: root.startupStore.getPasswordStrengthScore + StackView.onRemoved: { + d.password = "" + } + } + } + + Component { + id: enableBiometricsPage + EnableBiometricsPage { + StackView.onRemoved: d.enableBiometrics = false + } + } + + Component { + id: splashScreen + DidYouKnowSplashScreen { + readonly property string title: "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) + } + } + } + + Component { + id: seedphrasePage + SeedphrasePage { + isSeedPhraseValid: root.startupStore.validMnemonic + } + } + + Component { + id: createKeycardProfilePage + CreateKeycardProfilePage { + StackView.onActivated: d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithKeycard + } + } + + Component { + id: keycardIntroPage + KeycardIntroPage { + keycardState: d.currentKeycardState + displayPromoBanner: !d.settings.keycardPromoShown + StackView.onActivated: { + // NB just to make sure we don't miss the signal when we (re)load the page in the final state already + if (keycardState === Constants.startupState.keycardEmpty) + emptyKeycardDetected() + } + } + } + + Component { + id: keycardCreatePinPage + KeycardCreatePinPage {} + } + + // common popups + Component { + id: privacyPolicyPopup + StatusSimpleTextPopup { + title: qsTr("Status Software Privacy Policy") + content { + textFormat: Text.MarkdownText + text: SQUtils.StringUtils.readTextFile(Qt.resolvedUrl("../../../imports/assets/docs/privacy.mdwn")) + } + destroyOnClose: true + } + } + + Component { + id: termsOfUsePopup + StatusSimpleTextPopup { + title: qsTr("Status Software Terms of Use") + content { + textFormat: Text.MarkdownText + text: SQUtils.StringUtils.readTextFile(Qt.resolvedUrl("../../../imports/assets/docs/terms-of-use.mdwn")) + } + destroyOnClose: true + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml b/ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml new file mode 100644 index 00000000000..78b4c44a719 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/components/NewsCarousel.qml @@ -0,0 +1,110 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +Control { + id: root + + // [{primary:string, secondary:string, image:string}] + required property var newsModel + + background: Rectangle { + color: StatusColors.colors["neutral-95"] + radius: 20 + } + + contentItem: Item { + id: newsPage + readonly property string primaryText: root.newsModel.get(pageIndicator.currentIndex).primary + readonly property string secondaryText: root.newsModel.get(pageIndicator.currentIndex).secondary + + Image { + readonly property int size: Math.min(parent.width / 3 * 2, parent.height / 2, 370) + anchors.centerIn: parent + width: size + height: size + source: Theme.png(root.newsModel.get(pageIndicator.currentIndex).image) + } + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 48 - root.padding + width: Math.min(300, parent.width) + spacing: 4 + + StatusBaseText { + Layout.fillWidth: true + text: newsPage.primaryText + horizontalAlignment: Text.AlignHCenter + font.weight: Font.DemiBold + color: Theme.palette.white + } + + StatusBaseText { + Layout.fillWidth: true + text: newsPage.secondaryText + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.additionalTextSize + color: Theme.palette.white + wrapMode: Text.WordWrap + } + + PageIndicator { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Theme.halfPadding + id: pageIndicator + interactive: true + count: root.newsModel.count + currentIndex: -1 + Component.onCompleted: currentIndex = 0 // start switching pages + + function switchToNextOrFirstPage() { + if (currentIndex < count - 1) + currentIndex++ + else + currentIndex = 0 + } + + delegate: Control { + id: pageIndicatorDelegate + implicitWidth: 44 + implicitHeight: 8 + + readonly property bool isCurrentPage: index === pageIndicator.currentIndex + + background: Rectangle { + color: Qt.rgba(1, 1, 1, 0.1) + radius: 4 + HoverHandler { + cursorShape: hovered ? Qt.PointingHandCursor : undefined + } + } + contentItem: Item { + Rectangle { + NumberAnimation on width { + from: 0 + to: pageIndicatorDelegate.availableWidth + duration: 2000 + running: pageIndicatorDelegate.isCurrentPage + onStopped: { + if (pageIndicatorDelegate.isCurrentPage) + pageIndicator.switchToNextOrFirstPage() + } + } + + height: parent.height + color: pageIndicatorDelegate.isCurrentPage ? Theme.palette.white : "transparent" + radius: 4 + } + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/components/qmldir b/ui/app/AppLayouts/Onboarding2/components/qmldir new file mode 100644 index 00000000000..cfd038f2479 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/components/qmldir @@ -0,0 +1 @@ +NewsCarousel 1.0 NewsCarousel.qml diff --git a/ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml b/ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml new file mode 100644 index 00000000000..c28b16b7037 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/controls/ListItemButton.qml @@ -0,0 +1,26 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Theme 0.1 + +StatusListItem { + radius: 20 + asset.width: 32 + asset.height: 32 + asset.bgRadius: 0 + asset.bgColor: "transparent" + asset.isImage: true + statusListItemTitle.font.pixelSize: Theme.additionalTextSize + statusListItemTitle.font.weight: Font.Medium + statusListItemSubTitle.font.pixelSize: Theme.additionalTextSize + components: [ + StatusIcon { + icon: "next" + width: 16 + height: 16 + color: Theme.palette.baseColor1 + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml b/ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml new file mode 100644 index 00000000000..abd22549a5a --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/controls/OnboardingFrame.qml @@ -0,0 +1,36 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import utils 1.0 + +Frame { + id: root + + property bool dropShadow: true + property alias cornerRadius: background.radius + + padding: Theme.bigPadding + + background: Rectangle { + id: background + border.width: 1 + border.color: Theme.palette.baseColor2 + radius: 20 + color: Theme.palette.background + } + + layer.enabled: root.dropShadow + layer.effect: DropShadow { + verticalOffset: 4 + radius: 7 + samples: 15 + cached: true + color: Theme.palette.name === Constants.darkThemeName ? Theme.palette.dropShadow + : Qt.rgba(0, 34/255, 51/255, 0.03) + } +} diff --git a/ui/app/AppLayouts/Onboarding2/controls/qmldir b/ui/app/AppLayouts/Onboarding2/controls/qmldir new file mode 100644 index 00000000000..b513ee40814 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/controls/qmldir @@ -0,0 +1,2 @@ +OnboardingFrame 1.0 OnboardingFrame.qml +ListItemButton 1.0 ListItemButton.qml diff --git a/ui/app/AppLayouts/Onboarding2/pages/CreateKeycardProfilePage.qml b/ui/app/AppLayouts/Onboarding2/pages/CreateKeycardProfilePage.qml new file mode 100644 index 00000000000..0c4035b62a2 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/CreateKeycardProfilePage.qml @@ -0,0 +1,104 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +OnboardingPage { + id: root + + title: qsTr("Create profile on empty Keycard") + + signal createKeycardProfileWithNewSeedphrase() + signal createKeycardProfileWithExistingSeedphrase() + + contentItem: Item { + ColumnLayout { + width: parent.width + anchors.centerIn: parent + + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("You will require your Keycard to log in to Status and sign transactions") + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + ColumnLayout { + Layout.maximumWidth: Math.min(380, root.availableWidth) + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 56 + spacing: 20 + + OnboardingFrame { + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 24 + StatusImage { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(268, parent.width) + Layout.preferredHeight: Math.min(164, height) + source: Theme.png("onboarding/status_generate_keycard") + mipmap: true + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Use a new seed phrase") + font.pixelSize: Theme.secondaryAdditionalTextSize + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -Theme.padding + text: qsTr("To create your Keycard-stored profile ") + font.pixelSize: Theme.additionalTextSize + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: Theme.palette.baseColor1 + } + StatusButton { + Layout.fillWidth: true + text: qsTr("Let’s go!") + font.pixelSize: Theme.additionalTextSize + onClicked: root.createKeycardProfileWithNewSeedphrase() + } + } + } + + OnboardingFrame { + Layout.fillWidth: true + padding: 1 + dropShadow: false + contentItem: ColumnLayout { + spacing: 0 + ListItemButton { + Layout.fillWidth: true + title: qsTr("Use an existing seed phrase") + subTitle: qsTr("To create your Keycard-stored profile ") + asset.name: Theme.png("onboarding/create_profile_seed") + onClicked: root.createKeycardProfileWithExistingSeedphrase() + } + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml b/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml new file mode 100644 index 00000000000..1041eea2663 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml @@ -0,0 +1,87 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups 0.1 + +import utils 1.0 +import shared.views 1.0 + +OnboardingPage { + id: root + + property var passwordStrengthScoreFunction: (password) => { console.error("passwordStrengthScoreFunction: IMPLEMENT ME") } + + signal setPasswordRequested(string password) + + title: qsTr("Create profile password") + + QtObject { + id: d + + function submit() { + if (!passView.ready) + return + root.setPasswordRequested(passView.newPswText) + } + } + + Component.onCompleted: passView.forceNewPswInputFocus() + + contentItem: Item { + ColumnLayout { + spacing: Theme.padding + anchors.centerIn: parent + width: Math.min(400, root.availableWidth) + + PasswordView { + id: passView + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + highSizeIntro: true + title: root.title + introText: qsTr("This password can’t be recovered") + recoverText: "" + passwordStrengthScoreFunction: root.passwordStrengthScoreFunction + onReturnPressed: d.submit() + } + StatusButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Confirm password") + enabled: passView.ready + onClicked: d.submit() + } + } + } + + StatusButton { + width: 32 + height: 32 + icon.width: 20 + icon.height: 20 + icon.color: Theme.palette.directColor1 + normalColor: Theme.palette.baseColor2 + padding: 0 + anchors.right: parent.right + anchors.top: parent.top + icon.name: "info" + onClicked: passwordDetailsPopup.createObject(root).open() + } + + Component { + id: passwordDetailsPopup + StatusSimpleTextPopup { + title: qsTr("Create profile password") + width: 480 + destroyOnClose: true + content.text: qsTr("Your Status keys are the foundation of your self-sovereign identity in Web3. You have complete control over these keys, which you can use to sign transactions, access your data, and interact with Web3 services. + +Your keys are always securely stored on your device and protected by your Status profile password. Status doesn't know your password and can't reset it for you. If you forget your password, you may lose access to your Status profile and wallet funds. + +Remember your password and don't share it with anyone.") + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml b/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml new file mode 100644 index 00000000000..3d0b419b24f --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml @@ -0,0 +1,117 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +import utils 1.0 + +OnboardingPage { + id: root + + title: qsTr("Create your profile") + + signal createProfileWithPasswordRequested() + signal createProfileWithSeedphraseRequested() + signal createProfileWithEmptyKeycardRequested() + + 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 start using Status?") + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + OnboardingFrame { + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: 20 + StatusImage { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(268, parent.width) + Layout.preferredHeight: Math.min(164, height) + source: Theme.png("onboarding/status_generate_keys") + mipmap: true + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Start fresh") + font.pixelSize: Theme.secondaryAdditionalTextSize + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -Theme.padding + text: qsTr("Create a new profile from scratch") + font.pixelSize: Theme.additionalTextSize + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: Theme.palette.baseColor1 + } + StatusButton { + Layout.fillWidth: true + text: qsTr("Let’s go!") + font.pixelSize: Theme.additionalTextSize + onClicked: root.createProfileWithPasswordRequested() + } + } + } + + OnboardingFrame { + id: buttonFrame + Layout.fillWidth: true + padding: 1 + dropShadow: false + contentItem: ColumnLayout { + spacing: 0 + ListItemButton { + Layout.fillWidth: true + title: qsTr("Use a seed phrase") + subTitle: qsTr("If you already have an Ethereum wallet") + asset.name: Theme.png("onboarding/create_profile_seed") + onClicked: root.createProfileWithSeedphraseRequested() + } + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: -buttonFrame.padding + Layout.rightMargin: -buttonFrame.padding + Layout.preferredHeight: 1 + color: Theme.palette.statusMenu.separatorColor + } + ListItemButton { + Layout.fillWidth: true + title: qsTr("Use an empty Keycard") + subTitle: qsTr("Store your new profile keys on Keycard") + asset.name: Theme.png("onboarding/create_profile_keycard") + onClicked: root.createProfileWithEmptyKeycardRequested() + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml b/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml new file mode 100644 index 00000000000..5a21d96a0c1 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml @@ -0,0 +1,65 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Theme 0.1 + +OnboardingPage { + id: root + + title: qsTr("Enable biometrics") + + property string subtitle + + signal enableBiometricsRequested(bool enable) + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + spacing: 20 + width: Math.min(400, root.availableWidth) + + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: -12 + text: root.subtitle + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StatusImage { + Layout.preferredWidth: 260 + Layout.preferredHeight: 260 + Layout.topMargin: 20 + Layout.bottomMargin: 20 + Layout.alignment: Qt.AlignHCenter + mipmap: true + source: Theme.png("onboarding/enable_biometrics") + } + + StatusButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Yes, use biometrics") + onClicked: root.enableBiometricsRequested(true) + } + + StatusFlatButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Maybe later") + onClicked: root.enableBiometricsRequested(false) + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/HelpUsImproveStatusPage.qml b/ui/app/AppLayouts/Onboarding2/pages/HelpUsImproveStatusPage.qml new file mode 100644 index 00000000000..4adb4d4a183 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/HelpUsImproveStatusPage.qml @@ -0,0 +1,163 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups.Dialog 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +import utils 1.0 + +OnboardingPage { + id: root + + title: qsTr("Help us improve Status") + + signal shareUsageDataRequested(bool enabled) + signal privacyPolicyRequested() + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(320, root.availableWidth) + spacing: root.padding + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Your usage data helps us make Status better") + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StatusImage { + Layout.preferredWidth: 300 + Layout.preferredHeight: 300 + Layout.topMargin: 36 + Layout.bottomMargin: 36 + Layout.alignment: Qt.AlignHCenter + mipmap: true + source: Theme.png("onboarding/status_totebag_artwork_1") + } + + StatusButton { + Layout.fillWidth: true + text: qsTr("Share usage data") + onClicked: root.shareUsageDataRequested(true) + } + StatusButton { + Layout.fillWidth: true + text: qsTr("Not now") + normalColor: "transparent" + borderWidth: 1 + borderColor: Theme.palette.baseColor2 + onClicked: root.shareUsageDataRequested(false) + } + } + } + + StatusButton { + width: 32 + height: 32 + icon.width: 20 + icon.height: 20 + icon.color: Theme.palette.directColor1 + normalColor: Theme.palette.baseColor2 + padding: 0 + anchors.right: parent.right + anchors.top: parent.top + icon.name: "info" + onClicked: helpUsImproveDetails.createObject(root).open() + } + + Component { + id: helpUsImproveDetails + StatusDialog { + title: qsTr("Help us improve Status") + width: 480 + standardButtons: Dialog.Ok + padding: 20 + destroyOnClose: true + contentItem: ColumnLayout { + spacing: 20 + StatusBaseText { + Layout.fillWidth: true + text: qsTr("We’ll collect anonymous analytics and diagnostics from your app to enhance Status’s quality and performance.") + wrapMode: Text.WordWrap + } + OnboardingFrame { + Layout.fillWidth: true + dropShadow: false + contentItem: ColumnLayout { + spacing: 12 + BulletPoint { + text: qsTr("Gather basic usage data, like clicks and page views") + check: true + } + BulletPoint { + text: qsTr("Gather core diagnostics, like bandwidth usage") + check: true + } + BulletPoint { + text: qsTr("Never collect your profile information or wallet address") + } + BulletPoint { + text: qsTr("Never collect information you input or send") + } + BulletPoint { + text: qsTr("Never sell your usage analytics data") + } + } + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("For more details and other cases where we handle your data, refer to our %1.") + .arg(Utils.getStyledLink(qsTr("Privacy Policy"), "#privacy", hoveredLink, Theme.palette.primaryColor1, Theme.palette.primaryColor1, false)) + color: Theme.palette.baseColor1 + font.pixelSize: Theme.additionalTextSize + wrapMode: Text.WordWrap + textFormat: Text.RichText + onLinkActivated: { + if (link == "#privacy") { + close() + root.privacyPolicyRequested() + } + } + HoverHandler { + // Qt CSS doesn't support custom cursor shape + cursorShape: !!parent.hoveredLink ? Qt.PointingHandCursor : undefined + } + } + } + } + } + + component BulletPoint: RowLayout { + property string text + property bool check + + spacing: 6 + StatusIcon { + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + icon: parent.check ? "check-circle" : "close-circle" + color: parent.check ? Theme.palette.successColor1 : Theme.palette.dangerColor1 + } + StatusBaseText { + Layout.fillWidth: true + text: parent.text + font.pixelSize: Theme.additionalTextSize + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardBasePage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardBasePage.qml new file mode 100644 index 00000000000..866f1367ba3 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardBasePage.qml @@ -0,0 +1,76 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils + +import utils 1.0 + +OnboardingPage { + id: root + + property string subtitle + property alias image: image + property alias infoText: infoText + property alias buttons: buttonsWrapper.children + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(400, root.availableWidth) + spacing: 20 + + StatusImage { + id: image + Layout.preferredWidth: 280 + Layout.preferredHeight: 280 + Layout.alignment: Qt.AlignHCenter + mipmap: true + } + + StatusBaseText { + Layout.fillWidth: true + text: root.title + font.pixelSize: 22 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + StatusBaseText { + Layout.fillWidth: true + text: root.subtitle + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + visible: !!text + } + StatusBaseText { + Layout.fillWidth: true + id: infoText + textFormat: Text.RichText + font.pixelSize: Theme.tertiaryTextFontSize + wrapMode: Text.WordWrap + color: Theme.palette.baseColor1 + horizontalAlignment: Text.AlignHCenter + visible: !!text + onLinkActivated: openLinkWithConfirmation(link, SQUtils.StringUtils.extractDomainFromLink(link)) + + HoverHandler { + // Qt CSS doesn't support custom cursor shape + cursorShape: !!parent.hoveredLink ? Qt.PointingHandCursor : undefined + } + } + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 4 + id: buttonsWrapper + spacing: 12 + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardCreatePinPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardCreatePinPage.qml new file mode 100644 index 00000000000..5f8de475e14 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardCreatePinPage.qml @@ -0,0 +1,126 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQml 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +import utils 1.0 + +KeycardBasePage { + id: root + + signal keycardPinCreated(string pin) + + image.source: Theme.png("onboarding/keycard/reading") + + QtObject { + id: d + property string pin + property string pin2 + + function reset() { + pin = "" + pin2 = "" + root.state = "creating" + } + + function setPins() { + if (pinInput.valid) { + if (root.state === "creating") + d.pin = pinInput.pinInput + else if (root.state === "repeating" || root.state === "mismatch") + d.pin2 = pinInput.pinInput + } + } + } + + buttons: [ + StatusPinInput { + id: pinInput + anchors.horizontalCenter: parent.horizontalCenter + validator: StatusIntValidator { bottom: 0; top: 999999 } + Component.onCompleted: { + statesInitialization() + forceFocus() + } + onPinInputChanged: { + Qt.callLater(d.setPins) + } + }, + StatusBaseText { + id: errorText + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("PINs don’t match") + font.pixelSize: Theme.tertiaryTextFontSize + color: Theme.palette.dangerColor1 + visible: false + } + ] + + state: "creating" + + states: [ + State { + name: "creating" + PropertyChanges { + target: root + title: qsTr("Create new Keycard PIN") + } + }, + State { + name: "mismatch" + extend: "repeating" + when: !!d.pin && !!d.pin2 && d.pin !== d.pin2 + PropertyChanges { + target: errorText + visible: true + } + PropertyChanges { + target: root + image.source: Theme.png("onboarding/keycard/error") + } + }, + State { + name: "success" + extend: "repeating" + when: !!d.pin && !!d.pin2 && d.pin === d.pin2 + PropertyChanges { + target: root + title: qsTr("Keycard PIN set") + } + PropertyChanges { + target: pinInput + enabled: false + } + PropertyChanges { + target: root + image.source: Theme.png("onboarding/keycard/success") + } + StateChangeScript { + script: { + pinInput.setPin("123456") // set a fake PIN, doesn't matter at this point + root.keycardPinCreated(d.pin) + } + } + }, + State { + name: "repeating" + when: d.pin !== "" + PropertyChanges { + target: root + title: qsTr("Repeat Keycard PIN") + } + StateChangeScript { + script: { + pinInput.statesInitialization() + } + } + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml b/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml new file mode 100644 index 00000000000..3d5245c832c --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/KeycardIntroPage.qml @@ -0,0 +1,238 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQml 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.controls 1.0 + +import utils 1.0 + +KeycardBasePage { + id: root + + required property string keycardState // Constants.startupState.keycardXXX + property bool displayPromoBanner + + signal reloadKeycardRequested() + signal keycardFactoryResetRequested() + signal loginWithKeycardRequested() + + signal emptyKeycardDetected() + + OnboardingFrame { + id: promoBanner + visible: false + dropShadow: false + cornerRadius: 12 + width: 600 + leftPadding: 0 + rightPadding: 20 + topPadding: Theme.halfPadding + bottomPadding: 0 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.bigPadding + + contentItem: RowLayout { + spacing: 0 + StatusImage { + Layout.preferredWidth: 154 + Layout.preferredHeight: 82 + source: Theme.png("onboarding/status_keycard_multiple") + } + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: -promoBanner.topPadding + spacing: 2 + StatusBaseText { + Layout.fillWidth: true + text: qsTr("New to Keycard?") + font.pixelSize: Theme.additionalTextSize + font.weight: Font.DemiBold + } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Store and trade your crypto with a simple, secure and slim hardware wallet.") + wrapMode: Text.Wrap + font.pixelSize: Theme.additionalTextSize + color: Theme.palette.baseColor1 + } + } + StatusButton { + Layout.leftMargin: 20 + Layout.topMargin: -promoBanner.topPadding + size: StatusBaseButton.Size.Small + text: qsTr("keycard.tech") + icon.name: "external-link" + icon.width: 24 + icon.height: 24 + onClicked: openLink("https://keycard.tech/") + } + } + } + + buttons: [ + MaybeOutlineButton { + id: btnLogin + width: 270 + anchors.horizontalCenter: parent.horizontalCenter + visible: false + text: qsTr("Log in with this Keycard") + onClicked: root.loginWithKeycardRequested() + }, + MaybeOutlineButton { + id: btnFactoryReset + width: 270 + anchors.horizontalCenter: parent.horizontalCenter + visible: false + text: qsTr("Factory reset Keycard") + onClicked: root.keycardFactoryResetRequested() + }, + MaybeOutlineButton { + id: btnReload + width: 270 + anchors.horizontalCenter: parent.horizontalCenter + visible: false + text: qsTr("I’ve inserted a Keycard") + onClicked: root.reloadKeycardRequested() + } + ] + + // inside a Column (or another Positioner), make all but the first button outline + component MaybeOutlineButton: StatusButton { + id: maybeOutlineButton + 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 { + name: "plugin" + when: root.keycardState === Constants.startupState.keycardPluginReader || + root.keycardState === "" + PropertyChanges { + target: root + title: qsTr("Plug in your Keycard reader") + image.source: Theme.png("onboarding/keycard/empty") + } + PropertyChanges { + target: promoBanner + visible: root.displayPromoBanner + } + }, + State { + name: "insert" + when: root.keycardState === Constants.startupState.keycardInsertKeycard + 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)) + image.source: Theme.png("onboarding/keycard/insert") + } + }, + State { + name: "reading" + when: root.keycardState === Constants.startupState.keycardReadingKeycard || + root.keycardState === Constants.startupState.keycardInsertedKeycard + PropertyChanges { + target: root + title: qsTr("Reading Keycard...") + image.source: Theme.png("onboarding/keycard/reading") + } + }, + // error states + State { + name: "error" + PropertyChanges { + target: root + image.source: Theme.png("onboarding/keycard/error") + } + PropertyChanges { + target: btnFactoryReset + visible: true + } + PropertyChanges { + target: btnReload + visible: true + } + }, + State { + name: "notKeycard" + extend: "error" + when: root.keycardState === Constants.startupState.keycardWrongKeycard || + root.keycardState === Constants.startupState.keycardNotKeycard + PropertyChanges { + target: root + title: qsTr("Oops this isn’t a Keycard") + subtitle: qsTr("Remove card and insert a Keycard") + image.source: Theme.png("onboarding/keycard/invalid") + } + PropertyChanges { + target: btnFactoryReset + visible: false + } + }, + State { + name: "occupied" + extend: "error" + when: root.keycardState === Constants.startupState.keycardMaxPairingSlotsReached + PropertyChanges { + target: root + title: qsTr("All pairing slots occupied") + subtitle: qsTr("Factory reset this Keycard or insert a different one") + } + }, + State { + name: "locked" + extend: "error" + when: root.keycardState === Constants.startupState.keycardLocked + PropertyChanges { + target: root + title: qsTr("Keycard locked") + subtitle: qsTr("The Keycard you have inserted is locked, you will need to factory reset it or insert a different one") + } + }, + State { + name: "notEmpty" + extend: "error" + when: root.keycardState === Constants.startupState.keycardNotEmpty + PropertyChanges { + target: root + title: qsTr("Keycard is not empty") + subtitle: qsTr("You can’t use it to store new keys right now") + } + PropertyChanges { + target: btnLogin + visible: true + } + }, + // success/exit state + State { + name: "emptyDetected" + when: root.keycardState === Constants.startupState.keycardEmpty + StateChangeScript { + script: root.emptyKeycardDetected() + } + } + ] +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml b/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml new file mode 100644 index 00000000000..5a6a81f04f3 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml @@ -0,0 +1,18 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core.Theme 0.1 + +Page { + signal openLink(string link) + signal openLinkWithConfirmation(string link, string domain) + + implicitWidth: 1200 + implicitHeight: 700 + + padding: 12 + + background: Rectangle { + color: Theme.palette.background + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/SeedphrasePage.qml b/ui/app/AppLayouts/Onboarding2/pages/SeedphrasePage.qml new file mode 100644 index 00000000000..13b7923b5a0 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/SeedphrasePage.qml @@ -0,0 +1,59 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +import shared.panels 1.0 + +OnboardingPage { + id: root + + property string subtitle + + property var isSeedPhraseValid: (mnemonic) => { console.error("isSeedPhraseValid IMPLEMENT ME"); return false } + + signal seedphraseValidated() + + contentItem: Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.min(580, 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: root.subtitle + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + EnterSeedPhrase { + id: seedPanel + Layout.fillWidth: true + isSeedPhraseValid: root.isSeedPhraseValid + onSubmitSeedPhrase: root.seedphraseValidated() + } + + StatusButton { + Layout.alignment: Qt.AlignHCenter + enabled: seedPanel.seedPhraseIsValid + text: qsTr("Continue") + onClicked: root.seedphraseValidated() + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml b/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml new file mode 100644 index 00000000000..740394e393a --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml @@ -0,0 +1,157 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtGraphicalEffects 1.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.components 1.0 + +import utils 1.0 + +OnboardingPage { + id: root + + title: qsTr("Welcome to Status") + + signal createProfileRequested() + signal loginRequested() + + signal privacyPolicyRequested() + signal termsOfUseRequested() + + QtObject { + id: d + readonly property ListModel newsModel: ListModel { + ListElement { + primary: qsTr("Own, buy and swap your crypto") + secondary: qsTr("Use the leading multi-chain self-custodial wallet") + image: "onboarding/status_key" + } + ListElement { + primary: qsTr("Chat privately with friends") + secondary: qsTr("With full metadata privacy and e2e encryption") + image: "onboarding/status_chat" + } + ListElement { + primary: qsTr("Discover web3") + secondary: qsTr("Explore and interact with the decentralised web") + image: "onboarding/status_totebag_artwork_1" + } + ListElement { + primary: qsTr("Store your assets on Keycard") + secondary: qsTr("Be safe with secure cold wallet") + image: "onboarding/status_keycard" + } + } + } + + contentItem: RowLayout { + spacing: root.padding + + // left part (welcome + buttons) + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: -headerText.height + + ColumnLayout { + width: Math.min(400, parent.width) + spacing: 18 + anchors.centerIn: parent + + StatusImage { + Layout.preferredWidth: 90 + Layout.preferredHeight: 90 + Layout.alignment: Qt.AlignHCenter + source: Theme.png("status-logo-icon") + mipmap: true + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 4 + radius: 12 + samples: 25 + spread: 0.2 + color: Theme.palette.dropShadow + } + } + + StatusBaseText { + id: headerText + Layout.fillWidth: true + text: root.title + font.pixelSize: 40 + font.bold: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + + StatusBaseText { + Layout.fillWidth: true + text: qsTr("The open-source, decentralised wallet and messenger") + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 48 - root.padding + width: Math.min(320, parent.width) + spacing: 12 + + StatusButton { + Layout.fillWidth: true + text: qsTr("Create profile") + onClicked: root.createProfileRequested() + } + StatusButton { + Layout.fillWidth: true + text: qsTr("Log in") + onClicked: root.loginRequested() + normalColor: "transparent" + borderWidth: 1 + borderColor: Theme.palette.baseColor2 + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: Theme.halfPadding + text: qsTr("By proceeding you accept Status
%1 and %2") + .arg(Utils.getStyledLink(qsTr("Terms of Use"), "#terms", hoveredLink, Theme.palette.primaryColor1, Theme.palette.primaryColor1, false)) + .arg(Utils.getStyledLink(qsTr("Privacy Policy"), "#privacy", hoveredLink, Theme.palette.primaryColor1, Theme.palette.primaryColor1, false)) + textFormat: Text.RichText + font.pixelSize: Theme.tertiaryTextFontSize + lineHeightMode: Text.FixedHeight + lineHeight: 16 + wrapMode: Text.WordWrap + color: Theme.palette.baseColor1 + horizontalAlignment: Text.AlignHCenter + onLinkActivated: { + if (link == "#terms") + root.termsOfUseRequested() + else if (link == "#privacy") + root.privacyPolicyRequested() + } + + HoverHandler { + // Qt CSS doesn't support custom cursor shape + cursorShape: !!parent.hoveredLink ? Qt.PointingHandCursor : undefined + } + } + } + } + + + // right part (news carousel) + NewsCarousel { + Layout.fillHeight: true + Layout.fillWidth: true + newsModel: d.newsModel + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/qmldir b/ui/app/AppLayouts/Onboarding2/pages/qmldir new file mode 100644 index 00000000000..40fc5fb1805 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/qmldir @@ -0,0 +1,8 @@ +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 diff --git a/ui/app/AppLayouts/Onboarding2/qmldir b/ui/app/AppLayouts/Onboarding2/qmldir new file mode 100644 index 00000000000..ac7c41394ab --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/qmldir @@ -0,0 +1 @@ +OnboardingLayout 1.0 OnboardingLayout.qml diff --git a/ui/app/mainui/Popups.qml b/ui/app/mainui/Popups.qml index 79a7cff2bb1..0002501e757 100644 --- a/ui/app/mainui/Popups.qml +++ b/ui/app/mainui/Popups.qml @@ -103,6 +103,7 @@ QtObject { Global.openSwapModalRequested.connect(openSwapModal) Global.openBuyCryptoModalRequested.connect(openBuyCryptoModal) Global.privacyPolicyRequested.connect(() => openPopup(privacyPolicyPopupComponent)) + Global.termsOfUseRequested.connect(() => openPopup(termsOfUsePopupComponent)) } property var currentPopup @@ -1253,23 +1254,25 @@ QtObject { }, Component { id: privacyPolicyPopupComponent - StatusDialog { - width: 600 - padding: 0 + StatusSimpleTextPopup { title: qsTr("Status Software Privacy Policy") - StatusScrollView { - id: privacyDialogScrollView - anchors.fill: parent - contentWidth: availableWidth - StatusBaseText { - width: privacyDialogScrollView.availableWidth - wrapMode: Text.Wrap - textFormat: Text.MarkdownText - text: SQUtils.StringUtils.readTextFile(":/imports/assets/docs/privacy.mdwn") - onLinkActivated: Global.openLinkWithConfirmation(link, SQUtils.StringUtils.extractDomainFromLink(link)) - } + content { + textFormat: Text.MarkdownText + text: SQUtils.StringUtils.readTextFile(":/imports/assets/docs/privacy.mdwn") + onLinkActivated: Global.openLinkWithConfirmation(link, SQUtils.StringUtils.extractDomainFromLink(link)) + } + destroyOnClose: true + } + }, + Component { + id: termsOfUsePopupComponent + StatusSimpleTextPopup { + title: qsTr("Status Software Terms of Use") + content { + textFormat: Text.MarkdownText + text: SQUtils.StringUtils.readTextFile(":/imports/assets/docs/terms-of-use.mdwn") + onLinkActivated: Global.openLinkWithConfirmation(link, SQUtils.StringUtils.extractDomainFromLink(link)) } - standardButtons: Dialog.Ok destroyOnClose: true } } diff --git a/ui/imports/shared/views/PasswordView.qml b/ui/imports/shared/views/PasswordView.qml index ed5fc8b8758..ca50e64fd8f 100644 --- a/ui/imports/shared/views/PasswordView.qml +++ b/ui/imports/shared/views/PasswordView.qml @@ -79,11 +79,6 @@ ColumnLayout { QtObject { id: d - property bool containsLower: false - property bool containsUpper: false - property bool containsNumbers: false - property bool containsSymbols: false - readonly property var validatorRegexp: /^[!-~]+$/ readonly property string validatorErrMessage: qsTr("Only ASCII letters, numbers, and symbols are allowed") readonly property string passTooLongErrMessage: qsTr("Maximum %n character(s)", "", Constants.maxPasswordLength) @@ -244,7 +239,7 @@ ColumnLayout { Layout.alignment: root.contentAlignment StatusBaseText { - text: qsTr("New password") + text: qsTr("Choose password") } StatusPasswordInput { @@ -255,7 +250,7 @@ ColumnLayout { Layout.alignment: root.contentAlignment Layout.fillWidth: true - placeholderText: qsTr("Enter new password") + placeholderText: qsTr("Type password") echoMode: showPassword ? TextInput.Normal : TextInput.Password rightPadding: showHideNewIcon.width + showHideNewIcon.anchors.rightMargin + Theme.padding / 2 @@ -265,11 +260,6 @@ ColumnLayout { // Update strength indicator: strengthInditactor.strength = d.convertStrength(root.passwordStrengthScoreFunction(newPswInput.text)) - d.containsLower = d.lowerCaseValidator(text) - d.containsUpper = d.upperCaseValidator(text) - d.containsNumbers = d.numbersValidator(text) - d.containsSymbols = d.symbolsValidator(text) - if(!d.validateCharacterSet(text)) return if (text.length === confirmPswInput.text.length) { @@ -292,87 +282,11 @@ ColumnLayout { onClicked: newPswInput.showPassword = !newPswInput.showPassword } } - - StatusPasswordStrengthIndicator { - id: strengthInditactor - Layout.fillWidth: true - value: Math.min(Constants.minPasswordLength, newPswInput.text.length) - from: 0 - to: Constants.minPasswordLength - labelVeryWeak: qsTr("Very weak") - labelWeak: qsTr("Weak") - labelSoso: qsTr("So-so") - labelGood: qsTr("Good") - labelGreat: qsTr("Great") - } - } - - Rectangle { - Layout.fillWidth: true - Layout.minimumHeight: 80 - border.color: Theme.palette.baseColor2 - border.width: 1 - color: "transparent" - radius: Theme.radius - implicitHeight: strengthColumn.implicitHeight - implicitWidth: strengthColumn.implicitWidth - - ColumnLayout { - id: strengthColumn - anchors.fill: parent - anchors.margins: Theme.padding - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.padding - - StatusBaseText { - id: strengthenTxt - Layout.fillHeight: true - Layout.alignment: Qt.AlignHCenter - wrapMode: Text.WordWrap - text: root.strengthenText - font.pixelSize: 12 - color: Theme.palette.baseColor1 - clip: true - } - - RowLayout { - spacing: Theme.padding - Layout.alignment: Qt.AlignHCenter - - StatusBaseText { - id: lowerCaseTxt - text: "• " + qsTr("Lower case") - font.pixelSize: 12 - color: d.containsLower ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } - - StatusBaseText { - id: upperCaseTxt - text: "• " + qsTr("Upper case") - font.pixelSize: 12 - color: d.containsUpper ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } - - StatusBaseText { - id: numbersTxt - text: "• " + qsTr("Numbers") - font.pixelSize: 12 - color: d.containsNumbers ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } - - StatusBaseText { - id: symbolsTxt - text: "• " + qsTr("Symbols") - font.pixelSize: 12 - color: d.containsSymbols ? Theme.palette.successColor1 : Theme.palette.baseColor1 - } - } - } } ColumnLayout { StatusBaseText { - text: qsTr("Confirm new password") + text: qsTr("Repeat password") } StatusPasswordInput { @@ -384,7 +298,7 @@ ColumnLayout { z: root.zFront Layout.fillWidth: true Layout.alignment: root.contentAlignment - placeholderText: qsTr("Enter new password") + placeholderText: qsTr("Type password") echoMode: showPassword ? TextInput.Normal : TextInput.Password rightPadding: showHideConfirmIcon.width + showHideConfirmIcon.anchors.rightMargin + Theme.padding / 2 @@ -427,11 +341,53 @@ ColumnLayout { } } + StatusPasswordStrengthIndicator { + id: strengthInditactor + Layout.fillWidth: true + value: Math.min(Constants.minPasswordLength, newPswInput.text.length) + from: 0 + to: Constants.minPasswordLength + } + + RowLayout { + Layout.fillWidth: true + spacing: Theme.padding + Layout.alignment: Qt.AlignHCenter + + PassIncludesIndicator { + caption: qsTr("Lower case") + checked: d.lowerCaseValidator(newPswInput.text) + } + + PassIncludesIndicator { + caption: qsTr("Upper case") + checked: d.upperCaseValidator(newPswInput.text) + } + + PassIncludesIndicator { + caption: qsTr("Numbers") + checked: d.numbersValidator(newPswInput.text) + } + + PassIncludesIndicator { + caption: qsTr("Symbols") + checked: d.symbolsValidator(newPswInput.text) + } + } + StatusBaseText { id: errorTxt Layout.alignment: root.contentAlignment - Layout.fillHeight: true - font.pixelSize: 12 + font.pixelSize: Theme.tertiaryTextFontSize color: Theme.palette.dangerColor1 } + + component PassIncludesIndicator: StatusBaseText { + property bool checked + property string caption + + text: "%1 %2".arg(checked ? "✓" : "+").arg(caption) + font.pixelSize: Theme.tertiaryTextFontSize + color: checked ? Theme.palette.successColor1 : Theme.palette.baseColor1 + } } diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 9f04a462b26..4b22c5ce12f 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -1346,6 +1346,7 @@ QtObject { readonly property string welcome: "welcome_view" readonly property string privacyAndSecurity: "privacy_and_security_view" readonly property string startApp: "start_app_after_upgrade" + readonly property string onboarding: "onboarding" } enum MutingVariations { diff --git a/ui/imports/utils/Global.qml b/ui/imports/utils/Global.qml index e1c8af31022..75e26d41564 100644 --- a/ui/imports/utils/Global.qml +++ b/ui/imports/utils/Global.qml @@ -91,6 +91,7 @@ QtObject { signal openTestnetPopup() signal privacyPolicyRequested() + signal termsOfUseRequested() // Swap signal openSwapModalRequested(var formDataParams) diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index 1c26cb7a198..abce08f6518 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -78,11 +78,11 @@ QtObject { `${link}` } - function getStyledLink(linkText, linkUrl, hoveredLink, textColor = Theme.palette.directColor1, linkColor = Theme.palette.primaryColor1) { + function getStyledLink(linkText, linkUrl, hoveredLink, textColor = Theme.palette.directColor1, linkColor = Theme.palette.primaryColor1, underlineLink = true) { return `` +