diff --git a/storybook/pages/OnboardingLayoutPage.qml b/storybook/pages/OnboardingLayoutPage.qml new file mode 100644 index 00000000000..11bc2b40506 --- /dev/null +++ b/storybook/pages/OnboardingLayoutPage.qml @@ -0,0 +1,94 @@ +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 mainui 1.0 +import shared.stores 1.0 as SharedStores +import AppLayouts.stores 1.0 as AppLayoutStores + +// compat +import AppLayouts.Onboarding.stores 1.0 as OOBS + +SplitView { + id: root + orientation: Qt.Vertical + + Logs { id: logs } + + Popups { + popupParent: root + sharedRootStore: SharedStores.RootStore {} + rootStore: AppLayoutStores.RootStore {} + communityTokensStore: SharedStores.CommunityTokensStore {} + } + + OnboardingLayout { + id: onboarding + SplitView.fillWidth: true + SplitView.fillHeight: true + startupStore: OOBS.StartupStore { + function getPasswordStrengthScore(password) { + return Math.min(password.length, 4) + } + } + 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 + } + + QtObject { + id: localAppSettings + property bool metricsPopupSeen + } + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 150 + + logsView.logText: logs.logText + + ColumnLayout { + Label { + text: "Current page: %1".arg(onboarding.stack.currentItem ? onboarding.stack.currentItem.title : "") + } + Label { + text: "Current path: %1 -> %2".arg(onboarding.primaryPath).arg(onboarding.secondaryPath) + } + Label { + text: "Stack depth: %1".arg(onboarding.stack.depth) + } + } + } +} + +// 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/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/assets.qrc b/ui/StatusQ/src/assets.qrc index 8f7d43516fa..31f192b249c 100644 --- a/ui/StatusQ/src/assets.qrc +++ b/ui/StatusQ/src/assets.qrc @@ -8339,6 +8339,11 @@ 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/create_profile_seed.png + assets/png/onboarding/create_profile_keycard.png + assets/png/onboarding/enable_biometrics.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..673ecd60586 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..4756cd71092 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/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_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/app/AppLayouts/Onboarding2/OnboardingLayout.qml b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml new file mode 100644 index 00000000000..1e880c523ec --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml @@ -0,0 +1,207 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Onboarding2.pages 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 {} + required property SharedStores.MetricsStore metricsStore + + 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) + + QtObject { + id: d + // logic + property int primaryPath: OnboardingLayout.PrimaryPath.Unknown + property int secondaryPath: OnboardingLayout.SecondaryPath.Unknown + + // UI + readonly property int opacityDuration: 50 + readonly property int swipeDuration: 400 + + // state + property string password + property bool enableBiometrics + } + + enum PrimaryPath { + Unknown, + CreateProfile, + Login + } + + enum SecondaryPath { + Unknown, + CreateProfileWithPassword, + CreateProfileWithSeedphrase, + CreateProfileWithKeycard + // 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 + } + + // 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 + + // welcome page + function onCreateProfileRequested() { + console.warn("!!! CREATE PROFILE") + d.primaryPath = OnboardingLayout.PrimaryPath.CreateProfile + stack.push(helpUsImproveStatusPage) + } + function onLoginRequested() { + console.warn("!!! LOG IN") + d.primaryPath = OnboardingLayout.PrimaryPath.Login + } + + // help us improve page + function onShareUsageDataRequested() { + console.warn("!!! SHARE USAGE DATA") + metricsStore.toggleCentralizedMetrics(true) + 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 + } + function onDontShareUsageDataRequested() { + console.warn("!!! DONT SHARE USAGE DATA") + metricsStore.toggleCentralizedMetrics(false) + localAppSettings.metricsPopupSeen = true + + if (d.primaryPath === OnboardingLayout.PrimaryPath.CreateProfile) + stack.push(createProfilePage) + else if (d.primaryPath === OnboardingLayout.PrimaryPath.Login) + ; // TODO Login + } + + // create profile page + function onCreateProfileWithPasswordRequested() { + console.warn("!!! CREATE PROFILE WITH PASSWORD") + d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithPassword + stack.push(createPasswordPage) + } + function onCreateProfileWithSeedphraseRequested() { + console.warn("!!! CREATE PROFILE WITH SEEDPHRASE") + d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithSeedphrase + } + function onCreateProfileWithEmptyKeycardRequested() { + console.warn("!!! CREATE PROFILE WITH KEYCARD") + d.secondaryPath = OnboardingLayout.SecondaryPath.CreateProfileWithKeycard + } + + // create password page + function onPasswordSetRequested(password: string) { + console.warn("!!! SET PASSWORD REQUESTED") + //root.startupStore.setPassword(password) + d.password = password + + stack.push(enableBiometricsPage, {subtitle: qsTr("Use biometrics to fill in your password?")}) + } + + // enable biometrics page + function onEnableBiometricsRequested(enabled: bool) { + console.warn("!!! ENABLE BIOMETRICS:", enabled) + d.enableBiometrics = enabled + // TODO push final splash screen + } + } + + Component { + id: welcomePage + WelcomePage { + StackView.onActivated: { + d.primaryPath = OnboardingLayout.PrimaryPath.Unknown + d.secondaryPath = OnboardingLayout.SecondaryPath.Unknown + d.password = "" + d.enableBiometrics = false + } + } + } + + Component { + id: helpUsImproveStatusPage + HelpUsImproveStatusPage {} + } + + Component { + id: createProfilePage + CreateProfilePage {} + } + + Component { + id: createPasswordPage + CreatePasswordPage { + passwordStrengthScoreFunction: root.startupStore.getPasswordStrengthScore + StackView.onRemoved: { + d.secondaryPath = OnboardingLayout.SecondaryPath.Unknown + d.password = "" + } + } + } + + Component { + id: enableBiometricsPage + EnableBiometricsPage { + StackView.onRemoved: d.enableBiometrics = false + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml b/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml new file mode 100644 index 00000000000..11984612c09 --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/CreatePasswordPage.qml @@ -0,0 +1,98 @@ +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.Dialog 0.1 + +import utils 1.0 +import shared.views 1.0 + +OnboardingPage { + id: root + + property var passwordStrengthScoreFunction: (password) => { console.error("passwordStrengthScoreFunction: IMPLEMENT ME") } + + signal passwordSetRequested(string password) + + title: qsTr("Create profile password") + + QtObject { + id: d + + function submit() { + if (!passView.ready) + return + root.passwordSetRequested(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 + StatusDialog { + title: qsTr("Create profile password") + width: 480 + padding: 0 + standardButtons: Dialog.Ok + destroyOnClose: true + StatusScrollView { + id: detailsScrollView + anchors.fill: parent + contentWidth: availableWidth + StatusBaseText { + width: detailsScrollView.availableWidth + wrapMode: Text.Wrap + 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..defeff1347b --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/CreateProfilePage.qml @@ -0,0 +1,145 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups 0.1 + +import 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 + } + + Frame { + Layout.fillWidth: true + padding: Theme.bigPadding + background: Rectangle { + border.width: 1 + border.color: Theme.palette.baseColor2 + radius: 20 + color: "transparent" + } + 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() + } + } + } + + Frame { + id: buttonFrame + Layout.fillWidth: true + padding: 1 + background: Rectangle { + border.width: 1 + border.color: Theme.palette.baseColor2 + radius: 20 + color: "transparent" + } + contentItem: ColumnLayout { + spacing: 0 + ListItemButton { + title: qsTr("Create profile with 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 { + title: qsTr("Create profile with an empty Keycard") + subTitle: qsTr("Store your new profile keys on Keycard") + asset.name: Theme.png("onboarding/create_profile_keycard") + onClicked: root.createProfileWithEmptyKeycardRequested() + } + } + } + } + } + + component ListItemButton: StatusListItem { + Layout.fillWidth: true + 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/pages/EnableBiometricsPage.qml b/ui/app/AppLayouts/Onboarding2/pages/EnableBiometricsPage.qml new file mode 100644 index 00000000000..bc31e62e16d --- /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: qsTr("Use biometrics to fill in your password?") + 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..4279b33f99e --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/HelpUsImproveStatusPage.qml @@ -0,0 +1,158 @@ +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 utils 1.0 + +OnboardingPage { + id: root + + title: qsTr("Help us improve Status") + + signal shareUsageDataRequested() + signal dontShareUsageDataRequested() + + 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() + } + StatusButton { + Layout.fillWidth: true + text: qsTr("Not now") + normalColor: "transparent" + borderWidth: 1 + borderColor: Theme.palette.baseColor2 + onClicked: root.dontShareUsageDataRequested() + } + StatusFlatButton { + Layout.fillWidth: true + text: qsTr("Learn more") + font.pixelSize: Theme.additionalTextSize + 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 + } + Frame { + Layout.fillWidth: true + background: Rectangle { + border.width: 1 + border.color: Theme.palette.baseColor2 + radius: Theme.radius + color: "transparent" + } + 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() + Global.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/OnboardingPage.qml b/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml new file mode 100644 index 00000000000..fcae29c920c --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/OnboardingPage.qml @@ -0,0 +1,15 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core.Theme 0.1 + +Page { + implicitWidth: 1200 + implicitHeight: 700 + + padding: 12 + + background: Rectangle { + color: Theme.palette.background + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml b/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml new file mode 100644 index 00000000000..bcfaf58747d --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/WelcomePage.qml @@ -0,0 +1,256 @@ +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 utils 1.0 + +OnboardingPage { + id: root + + title: qsTr("Welcome to Status") + + signal createProfileRequested() + signal loginRequested() + + QtObject { + id: d + readonly property ListModel newsModel: ListModel { + ListElement { + primary: qsTr("Own your crypto") + secondary: qsTr("Use the leading multi-chain self-custodial wallet") + } + ListElement { + primary: qsTr("Store your assets on Keycard") + secondary: qsTr("Your secure card-shaped hardware Wallet") + } + ListElement { + primary: qsTr("Chat privately with friends") + secondary: qsTr("With full metadata privacy and e2e encryption") + } + ListElement { + primary: qsTr("Discover web3") + secondary: qsTr("Explore and interact with the decentralised web") + } + } + } + + 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") + Global.termsOfUseRequested() + else if (link == "#privacy") + Global.privacyPolicyRequested() + } + + HoverHandler { + // Qt CSS doesn't support custom cursor shape + cursorShape: !!parent.hoveredLink ? Qt.PointingHandCursor : undefined + } + } + } + } + + + // right part (news carousel) + // TODO factor out to a separate component? + Control { + Layout.fillWidth: true + Layout.fillHeight: true + + background: Rectangle { + color: "#0D1625" // FIXME not in Palette + radius: 20 + } + + contentItem: Item { + id: newsPage + readonly property string primaryText: d.newsModel.get(pageIndicator.currentIndex).primary + readonly property string secondaryText: d.newsModel.get(pageIndicator.currentIndex).secondary + + Rectangle { // FIXME placeholder + anchors.centerIn: parent + width: parent.width / 3 * 2 + height: width + radius: width/2 + color: Theme.palette.dangerColor1 + } + + 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: d.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 + onIsCurrentPageChanged: { + if (isCurrentPage) + indicatorWidth = pageIndicatorDelegate.availableWidth + else + indicatorWidth = 0 + } + + property real indicatorWidth: 0 + Behavior on indicatorWidth { + NumberAnimation { duration: pageTimer.interval } + } + + Timer { + id: pageTimer + running: pageIndicatorDelegate.isCurrentPage + repeat: true + interval: 2000 + onTriggered: { + pageIndicator.switchToNextOrFirstPage() + + pageIndicatorDelegate.indicatorWidth = 0 // reset width + } + } + + background: Rectangle { + color: Qt.rgba(1, 1, 1, 0.1) + radius: 4 + HoverHandler { + cursorShape: hovered ? Qt.PointingHandCursor : undefined + } + } + contentItem: Item { + Rectangle { + width: pageIndicatorDelegate.indicatorWidth + height: parent.height + color: pageIndicatorDelegate.isCurrentPage ? Theme.palette.white : "transparent" + radius: 4 + } + } + } + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Onboarding2/pages/qmldir b/ui/app/AppLayouts/Onboarding2/pages/qmldir new file mode 100644 index 00000000000..b2fe30ff64c --- /dev/null +++ b/ui/app/AppLayouts/Onboarding2/pages/qmldir @@ -0,0 +1,5 @@ +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 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 d0a2cb14bf9..65f34653bdf 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 @@ -1240,6 +1241,28 @@ QtObject { standardButtons: Dialog.Ok destroyOnClose: true } + }, + Component { + id: termsOfUsePopupComponent + StatusDialog { + width: 600 + padding: 0 + title: qsTr("Status Software Terms of Use") + StatusScrollView { + id: termsOfUseDialogScrollView + anchors.fill: parent + contentWidth: availableWidth + StatusBaseText { + width: termsOfUseDialogScrollView.availableWidth + wrapMode: Text.Wrap + 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 d5111d97b68..dbfc7838ca5 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 9609442a8c7..f212a7a14c0 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 `` +