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 `` +