chore(CPP): foundation for user onboarding

Contains minimal account creation and login

Considerations:

- migrated status-go wrapper and login code from the fix/cpp-structure (241eec)
- Minimal refactoring and changes at the moment. Expect further refactoring
follow up to reach the desired state.
- Fix missing keychain initialization
- Fix accounts DB initialization call done by startup -> Controller.openedAccounts -> status-go.OpenAccounts calls
- Small refactoring and todos for other steps
- fix SignalsManager
- fix async access to dereferenced status-go memory from SignalsManager
- fix SignalsManager not starting when registering
- finish dev end to end test for create account and login
- small improvements and added TODOs for future work
- add onboarding test helpers and start messaging test
- Refactoring towards Login UI integration

Closes: #5909
Closes: #6028
This commit is contained in:
Stefan 2022-07-04 23:14:13 +02:00 committed by Stefan Dunca
parent 9c45dcad8b
commit a710558c6b
233 changed files with 3987 additions and 6598 deletions

6
.gitignore vendored
View File

@ -90,9 +90,3 @@ build/
*.exe
*.out
*.app
# Cpp App
*build-src-cpp-structure*
src-cpp-structure/projects/App/translations.qrc
src-cpp-structure/projects/App/i18n/*
src-cpp-structure/projects/App/translations/*

View File

@ -31,5 +31,6 @@ add_subdirectory(libs)
add_subdirectory(app)
add_subdirectory(test)
# TODO: temporary not to duplicate resources until we switch to c++ app then it can be refactored
add_subdirectory(resources)
add_subdirectory(ui/imports/assets)
add_subdirectory(ui/fonts)

View File

@ -5,7 +5,7 @@ set(CMAKE_AUTORCC On)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick REQUIRED)
qt6_standard_project_setup()
qt6_add_executable(${PROJECT_NAME} "")
qt6_add_executable(${PROJECT_NAME})
# TODO: Fix temporarly workaround until we make qt6_target_qml_sources work
# Adds qml files as /Status/Application/qml/.../<file name>
@ -45,7 +45,6 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_PROJECT_APPLICATION_NAM
target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_BINARY_DIR=${CMAKE_BINARY_DIR})
target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_SOURCE_DIR=${CMAKE_SOURCE_DIR})
add_subdirectory(qml)
add_subdirectory(qml/Status/Application/Navigation)
add_subdirectory(src)
add_subdirectory(res)
@ -63,14 +62,15 @@ target_link_libraries(${PROJECT_NAME}
PRIVATE
Qt6::Quick
StatusQ_Application_Navigation
Status::Application::Navigation
# TODO: Use Status:: namespace
#Core
Helpers
Onboarding
Assets
StatusQ
Status::ApplicationCore
Status::Helpers
Status::Onboarding
Status::Assets
Status::StatusQ
Status::StatusGoQt
)
# QtCreator needs this
@ -81,4 +81,4 @@ install(
TARGETS
${PROJECT_NAME}
RUNTIME
)
)

View File

@ -1,15 +0,0 @@
# Note that the automatic plugin generation is only possible if the module does not do anything besides registering the types.
# If it needs to do something more advanced like registering an image provider in initializeEngine, you still need to manually write the plugin. qt6_add_qml_module has support for this with NO_GENERATE_PLUGIN_SOURCE.
target_sources(${PROJECT_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
)
add_subdirectory(Status/Application)
# Qt 6.3 fails calling qt6_target_qml_sources
#qt6_target_qml_sources(${PROJECT_NAME}
# QML_FILES
# main.qml
#)

View File

@ -1,12 +0,0 @@
target_sources(${PROJECT_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
)
# Qt 6.3 fails calling qt6_target_qml_sources
#qt6_target_qml_sources(${PROJECT_NAME}
# QML_FILES
# StatusWindow.qml
# StatusContentView.qml
# TODO
#)

View File

@ -1,5 +1,5 @@
# Controls specialized on user workflows
project(StatusQ_Application_Navigation)
project(Status_Application_Navigation)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
@ -23,3 +23,4 @@ qt6_add_qml_module(${PROJECT_NAME}
OUTPUT_DIRECTORY
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/Application/Navigation
)
add_library(Status::Application::Navigation ALIAS Status_Application_Navigation)

View File

@ -21,7 +21,7 @@ int main(int argc, char *argv[])
QGuiApplication app(argc, argv);
setApplicationInformation(app);
QTranslator translator;
const QStringList uiLanguages = QLocale::system().uiLanguages();
for (const QString &locale : uiLanguages) {

View File

@ -44,9 +44,8 @@ pipeline {
stage('CMake Build') {
steps {
sh "conan install ${env.WORKSPACE}/ --profile=${env.WORKSPACE}/vendor/conan-configs/linux.ini -s build_type=Release --build=missing -if=${env.WORKSPACE}/build/conan -of=${env.WORKSPACE}/build"
// TODO: switch CMAKE_PREFIX_PATH with "-DCMAKE_TOOLCHAIN_FILE=${env.WORKSPACE}/build/conan/conan_toolchain.cmake" after fixing error "c++: error: unrecognized command line option '-stdlib=libc++'"
sh "qt-cmake ${env.WORKSPACE} -G Ninja -B ${env.WORKSPACE}/build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=${env.WORKSPACE}/build/conan/conan_home/.conan/data/gtest/1.11.0/_/_/package/521ce6d2b56041e08ea425948717819429cfbc29/"
sh "cmake --build ${env.WORKSPACE}/build"
// TODO: This fails compiling status-go with Jenkins user but not when run with docker's user. Fix go installation to work for all users or build docker with jenkin's
sh "conan build ${env.WORKSPACE} --build-folder=${env.WORKSPACE}/build/conan"
}
}

View File

@ -2,7 +2,7 @@ FROM stateoftheartio/qt6:6.3-gcc-aqt
RUN export DEBIAN_FRONTEND=noninteractive \
&& sudo apt update -yq \
&& sudo apt install -yq libgl-dev libvulkan-dev libxcb*-dev libxkbcommon-x11-dev python3-pip gcc-10
&& sudo apt install -yq libgl-dev libvulkan-dev libxcb*-dev libxkbcommon-x11-dev python3-pip gcc-10 golang-go
RUN sudo pip install conan

View File

@ -5,7 +5,7 @@ class StatusDesktop(ConanFile):
name = "status-desktop"
settings = "os", "compiler", "build_type", "arch"
requires = "gtest/1.11.0" #"fruit/3.6.0",
requires = "gtest/1.11.0", "nlohmann_json/3.10.5" # "fruit/3.6.0",
# cmake_find_package and cmake_find_package_multi should be substituted with CMakeDeps
# as soon as Conan 2.0 is released and all conan-center packages are adapted

View File

@ -0,0 +1,54 @@
# Base library. Expect most of the module libraries to depend on it
#
cmake_minimum_required(VERSION 3.21)
project(ApplicationCore
VERSION 0.1.0
LANGUAGES CXX)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
qt6_standard_project_setup()
qt6_add_qml_module(ApplicationCore
URI Status.ApplicationCore
VERSION 1.0
)
add_library(Status::ApplicationCore ALIAS ApplicationCore)
target_link_libraries(ApplicationCore
PRIVATE
Qt6::Quick
Qt6::Qml
)
install(
TARGETS
ApplicationCore
RUNTIME
)
target_include_directories(ApplicationCore
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore
# TODO: Workaround to QML_ELEMENT Qt6
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src
)
target_sources(ApplicationCore
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore/Conversions.h
${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore/Conversions.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore/UserConfiguration.h
${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore/UserConfiguration.cpp
)
# QtCreator needs this
set(QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/qml;${QML_IMPORT_PATH} CACHE STRING "For QtCreator" FORCE)
list(REMOVE_DUPLICATES QML_IMPORT_PATH)

View File

@ -0,0 +1,15 @@
#include "Conversions.h"
namespace fs = std::filesystem;
namespace Status {
QString toString(const fs::path &path) {
return QString::fromStdString(path.string());
}
fs::path toPath(const QString &pathStr) {
return fs::path(pathStr.toStdString());
}
}

View File

@ -0,0 +1,12 @@
#pragma once
#include <filesystem>
#include <QString>
namespace Status {
QString toString(const std::filesystem::path& path);
std::filesystem::path toPath(const QString& pathStr);
}

View File

@ -0,0 +1,47 @@
#include "UserConfiguration.h"
#include "Conversions.h"
#include <filesystem>
namespace fs = std::filesystem;
namespace Status::ApplicationCore {
namespace {
/// `status-go` data location
constexpr auto dataSubfolder = "data";
}
UserConfiguration::UserConfiguration(QObject *parent)
: QObject{parent}
{
generateReleaseConfiguration();
}
const QString UserConfiguration::qmlUserDataFolder() const
{
return toString(m_userDataFolder.string());
}
const fs::path &UserConfiguration::userDataFolder() const
{
return m_userDataFolder;
}
void UserConfiguration::setUserDataFolder(const QString &newUserDataFolder)
{
auto newVal = Status::toPath(newUserDataFolder);
if (m_userDataFolder.compare(newVal) == 0)
return;
m_userDataFolder = newVal;
emit userDataFolderChanged();
}
void UserConfiguration::generateReleaseConfiguration()
{
m_userDataFolder = toPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))/dataSubfolder;
emit userDataFolderChanged();
}
}

View File

@ -0,0 +1,34 @@
#pragma once
#include <QObject>
#include <QtQmlIntegration>
#include <filesystem>
namespace Status::ApplicationCore {
namespace fs = std::filesystem;
class UserConfiguration: public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString userDataFolder READ qmlUserDataFolder WRITE setUserDataFolder NOTIFY userDataFolderChanged)
public:
explicit UserConfiguration(QObject *parent = nullptr);
const QString qmlUserDataFolder() const;
const fs::path &userDataFolder() const;
void setUserDataFolder(const QString &newUserDataFolder);
signals:
void userDataFolderChanged();
private:
void generateReleaseConfiguration();
fs::path m_userDataFolder;
};
}

View File

@ -16,7 +16,7 @@ set_source_files_properties(qml/Status/Assets/Resources.qml PROPERTIES
QT_QML_SINGLETON_TYPE TRUE
)
qt6_add_qml_module(${PROJECT_NAME}
qt6_add_qml_module(Assets
URI Status.Assets
VERSION 1.0
@ -27,8 +27,9 @@ qt6_add_qml_module(${PROJECT_NAME}
# Required to suppress "qmllint may not work" warning
OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/Assets/
)
add_library(Status::Assets ALIAS Assets)
target_link_libraries(${PROJECT_NAME}
target_link_libraries(Assets
PRIVATE
Qt6::Qml
@ -43,6 +44,6 @@ list(REMOVE_DUPLICATES QML_IMPORT_PATH)
install(
TARGETS
${PROJECT_NAME}
Assets
RUNTIME
)
)

View File

@ -1,6 +1,6 @@
add_subdirectory(Helpers)
# TODO: enable after adding content
#add_subdirectory(Core)
add_subdirectory(StatusQ)
add_subdirectory(Onboarding)
add_subdirectory(ApplicationCore)
add_subdirectory(Assets)
add_subdirectory(Helpers)
add_subdirectory(Onboarding)
add_subdirectory(StatusGoQt)
add_subdirectory(StatusQ)

View File

@ -1,40 +0,0 @@
# Base library. Expect most of the module libraries to depend on it
#
cmake_minimum_required(VERSION 3.21)
project(Core
VERSION 0.1.0
LANGUAGES CXX)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
qt6_standard_project_setup()
qt6_add_qml_module(${PROJECT_NAME}
URI Status.Core
VERSION 1.0
# TODO: temporary until we make qt_target_qml_sources work
QML_FILES
qml/Status/Core/DevTest.qml
# Required to suppress "qmllint may not work" warning
OUTPUT_DIRECTORY
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/Core
)
add_subdirectory(qml/Status/Core)
add_subdirectory(src)
target_link_libraries(${PROJECT_NAME}
PRIVATE
Qt6::Quick
Qt6::Qml
)
install(
TARGETS
${PROJECT_NAME}
RUNTIME
)

View File

@ -1,4 +0,0 @@
qt6_add_resources(${PROJECT_NAME} imageresources
PREFIX "/images"
FILES logo.png splashscreen.png
)

View File

@ -1,5 +0,0 @@
target_sources(${PROJECT_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
)

View File

@ -11,7 +11,8 @@ set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
qt6_standard_project_setup()
add_library(${PROJECT_NAME} SHARED "")
add_library(Helpers SHARED)
add_library(Status::Helpers ALIAS Helpers)
# Setup configuration type (Debug/Release)
# Inspired by https://programmingrecluse.wordpress.com/2020/02/04/detect-debug-build-with-cmake/
@ -36,21 +37,17 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/template/BuildConfiguration.h.in"
"${BUILD_GENERATED_DIRECTORY}/Helpers/BuildConfiguration.h"
@ONLY)
target_include_directories(${PROJECT_NAME}
target_include_directories(Helpers
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src
${BUILD_GENERATED_DIRECTORY}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers
${BUILD_GENERATED_DIRECTORY}/Helpers
)
target_sources(${PROJECT_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/template/BuildConfiguration.h.in
)
add_subdirectory(src)
target_link_libraries(${PROJECT_NAME}
target_link_libraries(Helpers
PRIVATE
Qt6::Quick
Qt6::Qml
@ -58,6 +55,14 @@ target_link_libraries(${PROJECT_NAME}
install(
TARGETS
${PROJECT_NAME}
Helpers
RUNTIME
)
)
target_sources(Helpers
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/helpers.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/Singleton.h
)

View File

@ -1,8 +0,0 @@
target_include_directories(${PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/Helpers
)
add_subdirectory(Helpers)

View File

@ -1,7 +0,0 @@
target_sources(${PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/helpers.h
${CMAKE_CURRENT_SOURCE_DIR}/logs.h
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/logs.cpp
)

View File

@ -0,0 +1,27 @@
#pragma once
#include <memory>
namespace Status::Helpers
{
template<typename T>
class Singleton
{
public:
virtual ~Singleton<T>() = default;
static T& getInstance()
{
static T instance;
return instance;
}
Singleton<T>(const Singleton<T>&) = delete;
Singleton<T>& operator = (const Singleton<T>&) = delete;
private:
Singleton<T>() = default;
};
}

View File

@ -1,4 +1,4 @@
#include <Helpers/BuildConfiguration.h>
#include "Helpers/BuildConfiguration.h"
namespace Status::Helpers {

View File

@ -6,7 +6,7 @@
#include <iostream>
#include <Helpers/BuildConfiguration.h>
#include "BuildConfiguration.h"
namespace Status::Helpers {

View File

@ -1,4 +1,4 @@
# Base library. Expect most of the module libraries to depend on it
# Onboarding Module build definition
#
cmake_minimum_required(VERSION 3.21)
@ -8,33 +8,61 @@ project(Onboarding
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml Concurrent REQUIRED)
qt6_standard_project_setup()
qt6_add_qml_module(${PROJECT_NAME}
qt6_add_qml_module(Onboarding
URI Status.Onboarding
VERSION 1.0
# TODO: temporary until we make qt_target_qml_sources work
QML_FILES
qml/Status/Onboarding/base/SetupNewProfilePageBase.qml
qml/Status/Onboarding/base/OnboardingPageBase.qml
qml/Status/Onboarding/ConfirmPasswordPage.qml
qml/Status/Onboarding/CreatePasswordPage.qml
qml/Status/Onboarding/LoginView.qml
qml/Status/Onboarding/OnboardingView.qml
qml/Status/Onboarding/SetupNewProfileView.qml
qml/Status/Onboarding/SetUserNameAndPicturePage.qml
qml/Status/Onboarding/TempTextInput.qml
qml/Status/Onboarding/WelcomeView.qml
# Required to suppress "qmllint may not work" warning
OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/Onboarding/
)
add_library(Status::Onboarding ALIAS Onboarding)
target_link_libraries(${PROJECT_NAME}
add_subdirectory(src)
add_subdirectory(tests)
target_link_libraries(Onboarding
PRIVATE
Qt6::Quick
Qt6::Qml
Qt6::Concurrent
Status::ApplicationCore
Status::StatusGoQt
Status::StatusGoConfig
)
# Required by the MacOS authenticator. Consider moving platform particular implementation in its own static linked library
if(APPLE)
target_link_libraries(Onboarding
PRIVATE
"-framework Security"
"-framework LocalAuthentication"
)
endif()
# QtCreator needs this
set(QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/qml;${QML_IMPORT_PATH} CACHE STRING "For QtCreator" FORCE)
list(REMOVE_DUPLICATES QML_IMPORT_PATH)
install(
TARGETS
${PROJECT_NAME}
Onboarding
RUNTIME
)
)

View File

@ -0,0 +1,85 @@
import QtQuick
import QtQuick.Controls
import Status.Onboarding
import "base"
SetupNewProfilePageBase {
id: root
TempTextInput {
id: confirmPasswordInput
text: qsTr("1234567890")
width: 416
height: 44
anchors {
horizontalCenter: alignmentItem.horizontalCenter
verticalCenter: alignmentItem.verticalCenter
verticalCenterOffset: -baselineOffset
}
font.pointSize: 23
verticalAlignment: TextInput.AlignVCenter
}
Label {
id: errorLabel
anchors {
bottom: finalizeButton.top
horizontalCenter: finalizeButton.horizontalCenter
margins: 10
}
color: "red"
text: qsTr("Something went wrong")
visible: false
}
Button {
id: finalizeButton
text: qsTr("Finalize Status Password Creation")
anchors {
horizontalCenter: alignmentItem.horizontalCenter
top: alignmentItem.bottom
topMargin: 125
}
enabled: confirmPasswordInput.text === newAccountController.password
onClicked: {
// TODO have states to drive async creation
errorLabel.visible = false
finalizeButton.enabled = false
busyIndicatorMouseArea.cursorShape = Qt.BusyCursor
newAccountController.createAccount()
}
}
Connections {
target: newAccountController
function onAccountCreatedAndLoggedIn() {
busyIndicatorMouseArea.cursorShape = undefined
root.pageDone()
}
function onAccountCreationError() {
errorLabel.visible = true;
busyIndicatorMouseArea.cursorShape = undefined
}
}
MouseArea {
id: busyIndicatorMouseArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
enabled: false
}
}

View File

@ -0,0 +1,68 @@
import QtQuick
import QtQuick.Controls
import Status.Onboarding
import "base"
SetupNewProfilePageBase {
id: root
TempTextInput {
id: passwordInput
text: newAccountController.password
Binding {
target: newAccountController
property: "password"
value: passwordInput.text
}
width: 416
height: 44
anchors {
horizontalCenter: parent.horizontalCenter
bottom: alignmentItem.top
bottomMargin: 112
}
font.pointSize: 23
verticalAlignment: TextInput.AlignVCenter
}
TempTextInput {
id: confirmPasswordInput
text: newAccountController.confirmationPassword
Binding {
target: newAccountController
property: "confirmationPassword"
value: confirmPasswordInput.text
}
width: 416
height: 44
anchors {
horizontalCenter: alignmentItem.horizontalCenter
verticalCenter: alignmentItem.verticalCenter
verticalCenterOffset: -baselineOffset
}
font.pointSize: 23
verticalAlignment: TextInput.AlignVCenter
}
Button {
text: qsTr("Create Password")
anchors.horizontalCenter: alignmentItem.horizontalCenter
anchors.top: alignmentItem.bottom
anchors.topMargin: 125
enabled: newAccountController.passwordIsValid && newAccountController.confirmationPasswordIsValid
onClicked: root.pageDone()
}
}

View File

@ -0,0 +1,27 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Status.Containers
import "base"
OnboardingPageBase {
id: root
ColumnLayout {
anchors {
centerIn: parent
verticalCenterOffset: 50
}
Label {
text: qsTr("Welcome back")
Layout.alignment: Qt.AlignHCenter
}
LayoutSpacer {
Layout.preferredHeight: 210
}
}
}

View File

@ -2,36 +2,64 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQml
import Qt.labs.platform
import Status.Containers
import Status.Controls.Navigation
import Status.Onboarding
import Status.ApplicationCore
/** \brief Drives the onboarding workflow
*
*/
Item {
id: root
signal userLoggedIn()
implicitWidth: mainLayout.implicitWidth
implicitHeight: mainLayout.implicitHeight
implicitWidth: 1232
implicitHeight: 770
ColumnLayout {
id: mainLayout
UserConfiguration {
id: userConfiguration
}
OnboardingModule {
id: onboardingModule
userDataPath: userConfiguration.userDataFolder
}
MacTrafficLights {
anchors.left: parent.left
anchors.margins: 13
anchors.top: parent.top
z: stackView.z + 1
}
StackView {
id: stackView
anchors.fill: parent
MacTrafficLights {
Layout.margins: 13
initialItem: WelcomeView {
onboardingController: onboardingModule.controller
onSetupNewAccount: stackView.push(setupNewProfileViewComponent)
onAccountLoggedIn: root.userLoggedIn()
}
}
LayoutSpacer {}
Label {
Layout.alignment: Qt.AlignHCenter
text: "TODO OnboardingWorkflow"
Component {
id: setupNewProfileViewComponent
SetupNewProfileView {
onAbortAccountCreation: stackView.pop()
onUserLoggedIn: root.userLoggedIn()
newAccountController: onboardingModule.controller.initNewAccountController()
Component.onDestruction: onboardingModule.controller.terminateNewAccountController()
}
Button {
text: "Done"
Layout.alignment: Qt.AlignHCenter
onClicked: root.userLoggedIn()
}
LayoutSpacer {}
}
}

View File

@ -0,0 +1,59 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Status.Containers
import "base"
SetupNewProfilePageBase {
id: root
ColumnLayout {
anchors {
centerIn: parent
verticalCenterOffset: 50
}
Label {
text: qsTr("Your profile")
Layout.alignment: Qt.AlignHCenter
}
LayoutSpacer {
Layout.preferredHeight: 210
}
TempTextInput {
id: nameInput
text: newAccountController.name
Binding {
target: newAccountController
property: "name"
value: nameInput.text
}
Layout.preferredWidth: 328
Layout.preferredHeight: 44
Layout.alignment: Qt.AlignHCenter
font.pointSize: 23
}
LayoutSpacer {
Layout.preferredHeight: 144
}
Button {
text: qsTr("Next")
Layout.alignment: Qt.AlignHCenter
enabled: newAccountController.nameIsValid
onClicked: root.pageDone()
}
}
}

View File

@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQml
import Qt.labs.platform
import Status.Containers
import Status.Controls.Navigation
import Status.Onboarding
/** \brief Drives the onboarding workflow
*
*/
Item {
id: root
// TODO: fix error "Unable to assign Status::Onboarding::NewAccountController to Status::Onboarding::NewAccountController" then enable typed properties
required property var/*NewAccountController*/ newAccountController
signal userLoggedIn()
signal abortAccountCreation()
QtObject {
id: d
function goToPreviousPage() {
if(swipeView.currentItem === setUserNameAndPicturePage)
root.abortAccountCreation()
else
swipeView.currentIndex--
}
function goToNextPage() {
if(swipeView.currentItem === confirmPasswordPage)
root.userLoggedIn()
else
swipeView.currentIndex++
}
}
ObjectModel {
id: pagesModel
SetUserNameAndPicturePage {
id: setUserNameAndPicturePage
newAccountController: root.newAccountController
}
CreatePasswordPage {
newAccountController: root.newAccountController
}
ConfirmPasswordPage {
id: confirmPasswordPage
newAccountController: root.newAccountController
}
}
SwipeView {
id: swipeView
anchors.fill: parent
Repeater {
id: pageRepeater
model: pagesModel
Loader {
id: pageLoader
active: SwipeView.isCurrentItem || SwipeView.isNextItem || SwipeView.isPreviousItem
source: modelData
}
}
Connections {
target: pageRepeater.itemAt(swipeView.currentIndex)
function onPageDone() { d.goToNextPage() }
function onGoBack() { d.goToPreviousPage() }
}
}
PageIndicator {
count: swipeView.count
currentIndex: swipeView.currentIndex
anchors.bottom: swipeView.bottom
anchors.horizontalCenter: swipeView.horizontalCenter
}
}

View File

@ -0,0 +1,18 @@
import QtQuick
TextInput {
width: 416
height: 44
font.pointSize: 23
verticalAlignment: TextInput.AlignVCenter
Rectangle {
anchors {
fill: parent
margins: -1
}
border.width: 1
z: parent.z - 1
}
}

View File

@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Status.Containers
import "base"
OnboardingPageBase {
id: root
required property var onboardingController // OnboardingController
signal setupNewAccount()
signal accountLoggedIn()
backAvailable: false
ColumnLayout {
anchors {
centerIn: parent
verticalCenterOffset: -117
}
spacing: 10
Label {
text: qsTr("Welcome to Status")
}
LayoutSpacer {
Layout.preferredHeight: 50
}
ColumnLayout {
visible: accountsComboBox.count > 0
ComboBox {
id: accountsComboBox
Layout.preferredWidth: 328
Layout.preferredHeight: 44
model: onboardingController.accounts
textRole: "name"
valueRole: "account"
}
TempTextInput {
id: passwordInput
Layout.preferredWidth: 328
Layout.preferredHeight: 44
}
Button {
text: qsTr("Login")
enabled: passwordInput.text.length >= 10
onClicked: {
errorLabel.visible = false
onboardingController.login(accountsComboBox.currentValue, passwordInput.text)
}
}
Label {
id: errorLabel
text: qsTr("Failed logging in")
visible: false
color: "red"
}
}
Button {
text: qsTr("I am new to Status")
onClicked: root.setupNewAccount()
}
}
Connections {
target: onboardingController
function onAccountLoggedIn() {
root.accountLoggedIn()
}
function onAccountLoginError(error) {
console.warn(`Error logging in "${error}"`)
errorLabel.visible = true
}
}
}

View File

@ -0,0 +1,40 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Status.Onboarding
import Status.Controls.Navigation
/*! Template to guide the onboarding layout
*/
Item {
id: root
property bool backAvailable: true
/// All done in the current page
signal pageDone()
signal goBack()
Button {
id: backButton
text: "<"
anchors {
left: parent.left
margins: 16
bottom: parent.bottom
}
visible: root.backAvailable
flat: true
background: Rectangle {
height: width
radius: width/2
color: "#4360DF"
opacity: parent.hovered ? parent.pressed ? 1 : 0.5 : 0.1
}
onClicked: root.goBack()
}
}

View File

@ -0,0 +1,21 @@
import QtQuick
/*! Proposal on how to templetize the alignment requirement of some views
*/
OnboardingPageBase {
// TODO: fix error "Unable to assign Status::Onboarding::NewAccountController to Status::Onboarding::NewAccountController" then enable typed properties
required property var/*NewAccountController*/ newAccountController
/// Common reference item that doesn't change between common views/pages
readonly property Item alignmentItem: alignmentBaselineItem
Item {
id: alignmentBaselineItem
width: 1
height: 1
anchors.horizontalCenter: parent.horizontalCenter
y: (root.height * 477/770) - baselineOffset
}
}

View File

@ -1,13 +1,14 @@
# Internally we use includes directly
# External clients have to explicitly use the module name
add_subdirectory(Core)
add_subdirectory(Onboarding)
target_include_directories(${PROJECT_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/Core
${CMAKE_CURRENT_SOURCE_DIR}/Onboarding
# TODO: Workaround to QML_ELEMENT Qt6
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/Core
${CMAKE_CURRENT_SOURCE_DIR}/Onboarding
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}

View File

@ -0,0 +1,55 @@
#pragma once
#include "Common/Constants.h"
#include "Common/SigningPhrases.h"
#include "Common/Json.h"
#include <QtCore>
// TODO: Move to StatusGo library
namespace Status::Onboarding
{
struct AccountDto
{
QString name;
long timestamp;
QString identicon;
QString keycardPairing;
QString keyUid;
bool isValid() const
{
return !(name.isEmpty() || keyUid.isEmpty());
}
static AccountDto toAccountDto(const QJsonObject& jsonObj)
{
auto result = AccountDto();
try
{
result.name = Json::getMandatoryProp(jsonObj, "name")->toString();
auto timestampIt = Json::getProp(jsonObj, "timestamp");
if(timestampIt != jsonObj.constEnd()) {
bool ok = false;
auto t = timestampIt->toString().toLong(&ok);
if(ok)
result.timestamp = t;
}
result.identicon = Json::getMandatoryProp(jsonObj, "identicon")->toString();
result.keycardPairing = Json::getMandatoryProp(jsonObj, "keycard-pairing")->toString();
result.keyUid = Json::getMandatoryProp(jsonObj, "key-uid")->toString();
/// TODO: investigate unhandled `photo-path` value
}
catch (std::exception e)
{
qWarning() << QObject::tr("Mapping AccountDto failed: %1").arg(e.what());
}
return result;
}
};
}

View File

@ -0,0 +1,419 @@
#include "AccountsService.h"
#include <StatusGo/Accounts/Accounts.h>
#include <StatusGo/General.h>
#include <StatusGo/Utils.h>
#include <StatusGo/Messenger/Service.h>
#include <ApplicationCore/Conversions.h>
#include <optional>
std::optional<QString>
getDataFromFile(const fs::path &path)
{
QFile jsonFile{Status::toString(path)};
if(!jsonFile.open(QIODevice::ReadOnly))
{
qDebug() << "unable to open" << path.filename().c_str() << " for reading";
return std::nullopt;
}
QString data = jsonFile.readAll();
jsonFile.close();
return data;
}
namespace Status::Onboarding
{
namespace StatusGo = Status::StatusGo;
namespace Utils = Status::StatusGo::Utils;
AccountsService::AccountsService()
: m_isFirstTimeAccountLogin(false)
{
}
bool AccountsService::init(const fs::path& statusgoDataDir)
{
m_statusgoDataDir = statusgoDataDir;
auto response = StatusGo::Accounts::generateAddresses(Constants::General::AccountDefaultPaths);
if(response.containsError())
{
qWarning() << response.error.message;
return false;
}
for(const auto &genAddressObj : response.result)
{
auto gAcc = GeneratedAccountDto::toGeneratedAccountDto(genAddressObj.toObject());
gAcc.alias = generateAlias(gAcc.derivedAccounts.whisper.publicKey);
gAcc.identicon = generateIdenticon(gAcc.derivedAccounts.whisper.publicKey);
m_generatedAccounts.push_back(std::move(gAcc));
}
return true;
}
std::vector<AccountDto> AccountsService::openAndListAccounts()
{
auto response = StatusGo::Accounts::openAccounts(m_statusgoDataDir.c_str());
if(response.containsError())
{
qWarning() << response.error.message;
return std::vector<AccountDto>();
}
const auto multiAccounts = response.result;
std::vector<AccountDto> result;
for(const auto &value : multiAccounts)
{
result.push_back(AccountDto::toAccountDto(value.toObject()));
}
return result;
}
const std::vector<GeneratedAccountDto>& AccountsService::generatedAccounts() const
{
return m_generatedAccounts;
}
bool AccountsService::setupAccountAndLogin(const QString &accountId, const QString &password, const QString &displayName)
{
QString installationId(QUuid::createUuid().toString(QUuid::WithoutBraces));
QJsonObject accountData(getAccountDataForAccountId(accountId, displayName));
if(!setKeyStoreDir(accountData.value("key-uid").toString()))
return false;
QJsonArray subAccountData(getSubaccountDataForAccountId(accountId, displayName));
QJsonObject settings(getAccountSettings(accountId, installationId, displayName));
QJsonObject nodeConfig(getDefaultNodeConfig(installationId));
QString hashedPassword(Utils::hashString(password));
// This initialize the DB if first time running. Required for storing accounts
if(StatusGo::Accounts::openAccounts(m_statusgoDataDir.c_str()).containsError())
return false;
AccountsService::storeDerivedAccounts(accountId, hashedPassword, Constants::General::AccountDefaultPaths);
m_loggedInAccount = saveAccountAndLogin(hashedPassword, accountData, subAccountData, settings, nodeConfig);
return getLoggedInAccount().isValid();
}
const AccountDto& AccountsService::getLoggedInAccount() const
{
return m_loggedInAccount;
}
const GeneratedAccountDto& AccountsService::getImportedAccount() const
{
return m_importedAccount;
}
bool AccountsService::isFirstTimeAccountLogin() const
{
return m_isFirstTimeAccountLogin;
}
bool AccountsService::setKeyStoreDir(const QString &key)
{
auto keyStoreDir = m_statusgoDataDir / m_keyStoreDirName / key.toStdString();
auto response = StatusGo::General::initKeystore(keyStoreDir.c_str());
return !response.containsError();
}
QString AccountsService::login(AccountDto account, const QString& password)
{
// This is a requirement. Make it more explicit into the status go module
if(!setKeyStoreDir(account.keyUid))
return QString("Failed to initialize keystore before logging in");
// This initialize the DB if first time running. Required before logging in
if(StatusGo::Accounts::openAccounts(m_statusgoDataDir.c_str()).containsError())
return QString("Failed to open accounts before logging in");
QString hashedPassword(Utils::hashString(password));
QString thumbnailImage;
QString largeImage;
auto response = StatusGo::Accounts::login(account.name, account.keyUid, hashedPassword, account.identicon,
thumbnailImage, largeImage);
if(response.containsError())
{
qWarning() << response.error.message;
return QString();
}
m_loggedInAccount = std::move(account);
return QString();
}
void AccountsService::clear()
{
m_generatedAccounts.clear();
m_loggedInAccount = AccountDto();
m_importedAccount = GeneratedAccountDto();
m_isFirstTimeAccountLogin = false;
}
QString AccountsService::generateAlias(const QString& publicKey)
{
auto response = StatusGo::Accounts::generateAlias(publicKey);
if(response.containsError())
{
qWarning() << response.error.message;
return QString();
}
return response.result;
}
QString AccountsService::generateIdenticon(const QString& publicKey)
{
auto response = StatusGo::Accounts::generateIdenticon(publicKey);
if(response.containsError())
{
qWarning() << response.error.message;
return QString();
}
return response.result;
}
DerivedAccounts AccountsService::storeDerivedAccounts(const QString& accountId, const QString& hashedPassword,
const QVector<QString>& paths)
{
auto response = StatusGo::Accounts::storeDerivedAccounts(accountId, hashedPassword, paths);
if(response.containsError())
{
qWarning() << response.error.message;
return DerivedAccounts();
}
return DerivedAccounts::toDerivedAccounts(response.result);
}
StoredAccountDto AccountsService::storeAccount(const QString& accountId, const QString& hashedPassword)
{
auto response = StatusGo::Accounts::storeAccount(accountId, hashedPassword);
if(response.containsError())
{
qWarning() << response.error.message;
return StoredAccountDto();
}
return toStoredAccountDto(response.result);
}
AccountDto AccountsService::saveAccountAndLogin(const QString& hashedPassword, const QJsonObject& account,
const QJsonArray& subaccounts, const QJsonObject& settings,
const QJsonObject& config)
{
if(!StatusGo::Accounts::saveAccountAndLogin(hashedPassword, account, subaccounts, settings, config)) {
qWarning() << "Failed saving acccount" << account.value("name");
return AccountDto();
}
m_isFirstTimeAccountLogin = true;
return AccountDto::toAccountDto(account);
}
QJsonObject AccountsService::prepareAccountJsonObject(const GeneratedAccountDto& account, const QString &displayName) const
{
return QJsonObject{{"name", displayName.isEmpty() ? account.alias : displayName},
{"address", account.address},
{"photo-path", account.identicon},
{"identicon", account.identicon},
{"key-uid", account.keyUid},
{"keycard-pairing", QJsonValue()}};
}
QJsonObject AccountsService::getAccountDataForAccountId(const QString &accountId, const QString &displayName) const
{
for(const GeneratedAccountDto &acc : m_generatedAccounts)
{
if(acc.id == accountId)
{
return AccountsService::prepareAccountJsonObject(acc, displayName);
}
}
if(m_importedAccount.isValid())
{
if(m_importedAccount.id == accountId)
{
return AccountsService::prepareAccountJsonObject(m_importedAccount, displayName);
}
}
qDebug() << "account not found";
return QJsonObject();
}
QJsonArray AccountsService::prepareSubaccountJsonObject(const GeneratedAccountDto& account, const QString &displayName) const
{
return {
QJsonObject{
{"public-key", account.derivedAccounts.defaultWallet.publicKey},
{"address", account.derivedAccounts.defaultWallet.address},
{"color", "#4360df"},
{"wallet", true},
{"path", Constants::General::PathDefaultWallet},
{"name", "Status account"}
},
QJsonObject{
{"public-key", account.derivedAccounts.whisper.publicKey},
{"address", account.derivedAccounts.whisper.address},
{"path", Constants::General::PathWhisper},
{"name", displayName.isEmpty() ? account.alias : displayName},
{"identicon", account.identicon},
{"chat", true}
}
};
}
QJsonArray AccountsService::getSubaccountDataForAccountId(const QString& accountId, const QString &displayName) const
{
// "All these for loops with a nested if cry for a std::find_if :)"
for(const GeneratedAccountDto &acc : m_generatedAccounts)
{
if(acc.id == accountId)
{
return prepareSubaccountJsonObject(acc, displayName);
}
}
if(m_importedAccount.isValid())
{
if(m_importedAccount.id == accountId)
{
return prepareSubaccountJsonObject(m_importedAccount, displayName);
}
}
// TODO: Is this expected? Have proper error propagation, otherwise throw
qDebug() << "account not found";
return QJsonArray();
}
QString AccountsService::generateSigningPhrase(int count) const
{
QStringList words;
for(int i = 0; i < count; i++)
{
words.append(Constants::SigningPhrases[QRandomGenerator::global()->bounded(
static_cast<int>(Constants::SigningPhrases.size()))]);
}
return words.join(" ");
}
QJsonObject AccountsService::prepareAccountSettingsJsonObject(const GeneratedAccountDto& account,
const QString& installationId,
const QString& displayName) const
{
try {
auto templateDefaultNetworksJson = getDataFromFile(":/Status/StaticConfig/default-networks.json").value();
auto infuraKey = getDataFromFile(":/Status/StaticConfig/infura_key").value();
QString defaultNetworksContent = templateDefaultNetworksJson.replace("%INFURA_KEY%", infuraKey);
QJsonArray defaultNetworksJson = QJsonDocument::fromJson(defaultNetworksContent.toUtf8()).array();
return QJsonObject{
{"key-uid", account.keyUid},
{"mnemonic", account.mnemonic},
{"public-key", account.derivedAccounts.whisper.publicKey},
{"name", account.alias},
{"display-name", displayName},
{"address", account.address},
{"eip1581-address", account.derivedAccounts.eip1581.address},
{"dapps-address", account.derivedAccounts.defaultWallet.address},
{"wallet-root-address", account.derivedAccounts.walletRoot.address},
{"preview-privacy", true},
{"signing-phrase", generateSigningPhrase(3)},
{"log-level", "INFO"},
{"latest-derived-path", 0},
{"networks/networks", defaultNetworksJson},
{"currency", "usd"},
{"identicon", account.identicon},
{"waku-enabled", true},
{"wallet/visible-tokens", {
{Constants::General::DefaultNetworkName, QJsonArray{"SNT"}}
}
},
{"appearance", 0},
{"networks/current-network", Constants::General::DefaultNetworkName},
{"installation-id", installationId}
};
} catch (std::bad_optional_access) {
return QJsonObject();
}
}
QJsonObject AccountsService::getAccountSettings(const QString& accountId, const QString& installationId, const QString &displayName) const
{
for(const GeneratedAccountDto &acc : m_generatedAccounts)
if(acc.id == accountId)
{
return AccountsService::prepareAccountSettingsJsonObject(acc, installationId, displayName);
}
if(m_importedAccount.isValid())
{
if(m_importedAccount.id == accountId)
{
return AccountsService::prepareAccountSettingsJsonObject(m_importedAccount, installationId, displayName);
}
}
// TODO: Is this expected? Have proper error propagation, otherwise throw
qDebug() << "account not found";
return QJsonObject();
}
QJsonArray getNodes(const QJsonObject& fleet, const QString& nodeType)
{
auto nodes = fleet[nodeType].toObject();
QJsonArray result;
for(auto it = nodes.begin(); it != nodes.end(); ++it)
result << *it;
return result;
}
QJsonObject AccountsService::getDefaultNodeConfig(const QString& installationId) const
{
try {
auto templateNodeConfigJsonStr = getDataFromFile(":/Status/StaticConfig/node-config.json").value();
auto fleetJson = getDataFromFile(":/Status/StaticConfig/fleets.json").value();
auto infuraKey = getDataFromFile(":/Status/StaticConfig/infura_key").value();
auto nodeConfigJsonStr = templateNodeConfigJsonStr.replace("%INSTALLATIONID%", installationId)
.replace("%INFURA_KEY%", infuraKey);
QJsonObject nodeConfigJson = QJsonDocument::fromJson(nodeConfigJsonStr.toUtf8()).object();
QJsonObject clusterConfig = nodeConfigJson["ClusterConfig"].toObject();
QJsonObject fleetsJson = QJsonDocument::fromJson(fleetJson.toUtf8()).object()["fleets"].toObject();
auto fleet = fleetsJson[Constants::Fleet::Prod].toObject();
clusterConfig["Fleet"] = Constants::Fleet::Prod;
clusterConfig["BootNodes"] = getNodes(fleet, Constants::FleetNodes::Bootnodes);
clusterConfig["TrustedMailServers"] = getNodes(fleet, Constants::FleetNodes::Mailservers);
clusterConfig["StaticNodes"] = getNodes(fleet, Constants::FleetNodes::Whisper);
clusterConfig["RendezvousNodes"] = getNodes(fleet, Constants::FleetNodes::Rendezvous);
clusterConfig["RelayNodes"] = getNodes(fleet, Constants::FleetNodes::Waku);
clusterConfig["StoreNodes"] = getNodes(fleet, Constants::FleetNodes::Waku);
clusterConfig["FilterNodes"] = getNodes(fleet, Constants::FleetNodes::Waku);
clusterConfig["LightpushNodes"] = getNodes(fleet, Constants::FleetNodes::Waku);
nodeConfigJson["ClusterConfig"] = clusterConfig;
return nodeConfigJson;
} catch (std::bad_optional_access) {
return QJsonObject();
}
}
}

View File

@ -0,0 +1,95 @@
#pragma once
#include "AccountsServiceInterface.h"
namespace Status::Onboarding
{
/*!
* \brief The Service class
*
* \todo Refactor static dependencies
* :/resources/default-networks.json
* :/resources/node-config.json
* :/resources/fleets.json
* :/resources/infura_key
* \todo AccountsService
* \todo Consider removing unneded states (first time account login, user)
*/
class AccountsService : public AccountsServiceInterface
{
public:
AccountsService();
/// \see ServiceInterface
bool init(const fs::path& statusgoDataDir) override;
/// \see ServiceInterface
[[nodiscard]] std::vector<AccountDto> openAndListAccounts() override;
/// \see ServiceInterface
[[nodiscard]] const std::vector<GeneratedAccountDto>& generatedAccounts() const override;
/// \see ServiceInterface
bool setupAccountAndLogin(const QString& accountId, const QString& password, const QString& displayName) override;
/// \see ServiceInterface
[[nodiscard]] const AccountDto& getLoggedInAccount() const override;
[[nodiscard]] const GeneratedAccountDto& getImportedAccount() const override;
/// \see ServiceInterface
[[nodiscard]] bool isFirstTimeAccountLogin() const override;
/// \see ServiceInterface
bool setKeyStoreDir(const QString &key) override;
QString login(AccountDto account, const QString& password) override;
void clear() override;
QString generateAlias(const QString& publicKey) override;
QString generateIdenticon(const QString& publicKey) override;
private:
QJsonObject prepareAccountJsonObject(const GeneratedAccountDto& account, const QString& displayName) const;
DerivedAccounts storeDerivedAccounts(const QString& accountId, const QString& hashedPassword,
const QVector<QString>& paths);
StoredAccountDto storeAccount(const QString& accountId, const QString& hashedPassword);
AccountDto saveAccountAndLogin(const QString& hashedPassword, const QJsonObject& account,
const QJsonArray& subaccounts, const QJsonObject& settings,
const QJsonObject& config);
QJsonObject getAccountDataForAccountId(const QString& accountId, const QString& displayName) const;
QJsonArray prepareSubaccountJsonObject(const GeneratedAccountDto& account, const QString& displayName) const;
QJsonArray getSubaccountDataForAccountId(const QString& accountId, const QString& displayName) const;
QString generateSigningPhrase(int count) const;
QJsonObject prepareAccountSettingsJsonObject(const GeneratedAccountDto& account,
const QString& installationId,
const QString& displayName) const;
QJsonObject getAccountSettings(const QString& accountId, const QString& installationId, const QString& displayName) const;
QJsonObject getDefaultNodeConfig(const QString& installationId) const;
private:
std::vector<GeneratedAccountDto> m_generatedAccounts;
fs::path m_statusgoDataDir;
bool m_isFirstTimeAccountLogin;
// TODO: don't see the need for this state here
AccountDto m_loggedInAccount;
GeneratedAccountDto m_importedAccount;
// Here for now. Extract them if used by other services
static constexpr auto m_keyStoreDirName = "keystore";
};
}

View File

@ -0,0 +1,51 @@
#pragma once
#include "AccountDto.h"
#include "GeneratedAccountDto.h"
#include <filesystem>
namespace fs = std::filesystem;
namespace Status::Onboarding
{
class AccountsServiceInterface
{
public:
virtual ~AccountsServiceInterface() = default;
/// Generates and cache addresses accessible by \c generatedAccounts
virtual bool init(const fs::path& statusgoDataDir) = 0;
/// opens database and returns accounts list.
[[nodiscard]] virtual std::vector<AccountDto> openAndListAccounts() = 0;
/// Retrieve cached accounts generated in \c init
[[nodiscard]] virtual const std::vector<GeneratedAccountDto>& generatedAccounts() const = 0;
/// Configure an generated account. \a accountID must be sourced from \c generatedAccounts
virtual bool setupAccountAndLogin(const QString& accountID, const QString& password, const QString& displayName) = 0;
/// Account that is currently logged-in
[[nodiscard]] virtual const AccountDto& getLoggedInAccount() const = 0;
[[nodiscard]] virtual const GeneratedAccountDto& getImportedAccount() const = 0;
/// Check if the login was never done in the current \c data directory
[[nodiscard]] virtual bool isFirstTimeAccountLogin() const = 0;
/// Set and initializes the keystore directory. \see StatusGo::General::initKeystore
virtual bool setKeyStoreDir(const QString &key) = 0;
virtual QString login(AccountDto account, const QString& password) = 0;
virtual void clear() = 0;
virtual QString generateAlias(const QString& publicKey) = 0;
virtual QString generateIdenticon(const QString& publicKey) = 0;
};
}

View File

@ -0,0 +1,141 @@
#pragma once
#include "Common/Constants.h"
#include "Common/SigningPhrases.h"
#include "Common/Json.h"
#include <QtCore>
namespace Status::Onboarding
{
struct DerivedAccountDetails
{
QString publicKey;
QString address;
QString derivationPath;
static DerivedAccountDetails toDerivedAccountDetails(const QJsonObject& jsonObj, const QString& derivationPath)
{
// Mapping this DTO is not strightforward since only keys are used for id. We
// handle it a bit different.
auto result = DerivedAccountDetails();
try
{
result.derivationPath = derivationPath;
result.publicKey = Json::getMandatoryProp(jsonObj, "publicKey")->toString();
result.address = Json::getMandatoryProp(jsonObj, "address")->toString();
}
catch (std::exception e)
{
qWarning() << QString("Mapping DerivedAccountDetails failed: %1").arg(e.what());
}
return result;
}
};
struct DerivedAccounts
{
DerivedAccountDetails whisper;
DerivedAccountDetails walletRoot;
DerivedAccountDetails defaultWallet;
DerivedAccountDetails eip1581;
static DerivedAccounts toDerivedAccounts(const QJsonObject& jsonObj)
{
auto result = DerivedAccounts();
for(const auto &derivationPath : jsonObj.keys())
{
auto derivedObj = jsonObj.value(derivationPath).toObject();
if(derivationPath == Constants::General::PathWhisper)
{
result.whisper = DerivedAccountDetails::toDerivedAccountDetails(derivedObj, derivationPath);
}
else if(derivationPath == Constants::General::PathWalletRoot)
{
result.walletRoot = DerivedAccountDetails::toDerivedAccountDetails(derivedObj, derivationPath);
}
else if(derivationPath == Constants::General::PathDefaultWallet)
{
result.defaultWallet = DerivedAccountDetails::toDerivedAccountDetails(derivedObj, derivationPath);
}
else if(derivationPath == Constants::General::PathEIP1581)
{
result.eip1581 = DerivedAccountDetails::toDerivedAccountDetails(derivedObj, derivationPath);
}
}
return result;
}
};
struct StoredAccountDto
{
QString publicKey;
QString address;
};
static StoredAccountDto toStoredAccountDto(const QJsonObject& jsonObj)
{
auto result = StoredAccountDto();
try {
result.address = Json::getMandatoryProp(jsonObj, "address")->toString();
result.publicKey = Json::getMandatoryProp(jsonObj, "publicKey")->toString();
} catch (std::exception e) {
qWarning() << QString("Mapping StoredAccountDto failed: %1").arg(e.what());
}
return result;
}
struct GeneratedAccountDto
{
QString id;
QString publicKey;
QString address;
QString keyUid;
QString mnemonic;
DerivedAccounts derivedAccounts;
// The following two are set additionally.
QString alias;
QString identicon;
bool isValid() const
{
return !(id.isEmpty() || publicKey.isEmpty() || address.isEmpty() || keyUid.isEmpty());
}
static GeneratedAccountDto toGeneratedAccountDto(const QJsonObject& jsonObj)
{
auto result = GeneratedAccountDto();
try
{
result.id = Json::getMandatoryProp(jsonObj, "id")->toString();
result.address = Json::getMandatoryProp(jsonObj, "address")->toString();
result.keyUid = Json::getMandatoryProp(jsonObj, "keyUid")->toString();
result.mnemonic = Json::getMandatoryProp(jsonObj, "mnemonic")->toString();
result.publicKey = Json::getMandatoryProp(jsonObj, "publicKey")->toString();
auto derivedObj = Json::getProp(jsonObj, "derived")->toObject();
if(!derivedObj.isEmpty())
{
result.derivedAccounts = DerivedAccounts::toDerivedAccounts(derivedObj);
}
}
catch (std::exception e)
{
qWarning() << QString("Mapping GeneratedAccountDto failed: %1").arg(e.what());
}
return result;
}
};
}

View File

@ -0,0 +1,26 @@
target_sources(${PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/NewAccountController.h
${CMAKE_CURRENT_SOURCE_DIR}/OnboardingController.h
${CMAKE_CURRENT_SOURCE_DIR}/OnboardingModule.h
${CMAKE_CURRENT_SOURCE_DIR}/UserAccount.h
${CMAKE_CURRENT_SOURCE_DIR}/UserAccountsModel.h
${CMAKE_CURRENT_SOURCE_DIR}/Accounts/AccountDto.h
${CMAKE_CURRENT_SOURCE_DIR}/Accounts/GeneratedAccountDto.h
${CMAKE_CURRENT_SOURCE_DIR}/Accounts/AccountsService.h
${CMAKE_CURRENT_SOURCE_DIR}/Accounts/AccountsServiceInterface.h
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/NewAccountController.cpp
${CMAKE_CURRENT_SOURCE_DIR}/OnboardingController.cpp
${CMAKE_CURRENT_SOURCE_DIR}/OnboardingModule.cpp
${CMAKE_CURRENT_SOURCE_DIR}/UserAccount.cpp
${CMAKE_CURRENT_SOURCE_DIR}/UserAccountsModel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Accounts/AccountsService.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Common/Constants.h
${CMAKE_CURRENT_SOURCE_DIR}/Common/Json.h
${CMAKE_CURRENT_SOURCE_DIR}/Common/SigningPhrases.h
)

View File

@ -0,0 +1,48 @@
#pragma once
#include <QtCore>
#include <QStringLiteral>
namespace Status::Constants
{
namespace Fleet
{
inline const auto Prod = u"eth.prod"_qs;
inline const auto Staging = u"eth.staging"_qs;
inline const auto Test = u"eth.test"_qs;
inline const auto WakuV2Prod = u"wakuv2.prod"_qs;
inline const auto WakuV2Test = u"wakuv2.test"_qs;
inline const auto GoWakuTest = u"go-waku.test"_qs;
}
namespace FleetNodes
{
inline const auto Bootnodes = u"boot"_qs;
inline const auto Mailservers = u"mail"_qs;
inline const auto Rendezvous = u"rendezvous"_qs;
inline const auto Whisper = u"whisper"_qs;
inline const auto Waku = u"waku"_qs;
inline const auto LibP2P = u"libp2p"_qs;
inline const auto Websocket = u"websocket"_qs;
}
namespace General
{
inline const auto DefaultNetworkName = u"mainnet_rpc"_qs;
//const DEFAULT_NETWORKS_IDS* = @["mainnet_rpc", "testnet_rpc", "rinkeby_rpc", "goerli_rpc", "xdai_rpc", "poa_rpc" ]
inline const auto ZeroAddress = u"0x0000000000000000000000000000000000000000"_qs;
inline const auto PathWalletRoot = u"m/44'/60'/0'/0"_qs;
// EIP1581 Root Key, the extended key from which any whisper key/encryption key can be derived
inline const auto PathEIP1581 = u"m/43'/60'/1581'"_qs;
// BIP44-0 Wallet key, the default wallet key
inline const auto PathDefaultWallet = PathWalletRoot + u"/0"_qs;
// EIP1581 Chat Key 0, the default whisper key
inline const auto PathWhisper = PathEIP1581 + u"/0'/0"_qs;
inline const QVector<QString> AccountDefaultPaths {PathWalletRoot, PathEIP1581, PathWhisper, PathDefaultWallet};
}
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <QtCore/QJsonObject>
namespace Status
{
class Json
{
public:
/// TODO: refactor to get std::optional<QJsonObject> or rename it to get*It
static QJsonObject::const_iterator getProp(const QJsonObject& object, const QString& field)
{
const auto it = object.constFind(field);
return it;
}
/// TODO: refactor to get QJsonObject
static QJsonObject::const_iterator getMandatoryProp(const QJsonObject& object, const QString& field)
{
const auto it = getProp(object, field);
if (it == object.constEnd())
{
throw std::logic_error(QString("No field `%1`").arg(field).toStdString());
}
return it;
}
};
}

View File

@ -0,0 +1,56 @@
#pragma once
#include <QtCore>
namespace Status::Constants
{
constexpr std::array SigningPhrases {
"area", "army", "atom", "aunt", "babe", "baby", "back", "bail", "bait", "bake", "ball", "band", "bank", "barn",
"base", "bass", "bath", "bead", "beak", "beam", "bean", "bear", "beat", "beef", "beer", "beet", "bell", "belt",
"bend", "bike", "bill", "bird", "bite", "blow", "blue", "boar", "boat", "body", "bolt", "bomb", "bone", "book",
"boot", "bore", "boss", "bowl", "brow", "bulb", "bull", "burn", "bush", "bust", "cafe", "cake", "calf", "call",
"calm", "camp", "cane", "cape", "card", "care", "carp", "cart", "case", "cash", "cast", "cave", "cell", "cent",
"chap", "chef", "chin", "chip", "chop", "chub", "chug", "city", "clam", "clef", "clip", "club", "clue", "coal",
"coat", "code", "coil", "coin", "coke", "cold", "colt", "comb", "cone", "cook", "cope", "copy", "cord", "cork",
"corn", "cost", "crab", "craw", "crew", "crib", "crop", "crow", "curl", "cyst", "dame", "dare", "dark", "dart",
"dash", "data", "date", "dead", "deal", "dear", "debt", "deck", "deep", "deer", "desk", "dhow", "diet", "dill",
"dime", "dirt", "dish", "disk", "dock", "doll", "door", "dory", "drag", "draw", "drop", "drug", "drum", "duck",
"dump", "dust", "duty", "ease", "east", "eave", "eddy", "edge", "envy", "epee", "exam", "exit", "face", "fact",
"fail", "fall", "fame", "fang", "farm", "fawn", "fear", "feed", "feel", "feet", "file", "fill", "film", "find",
"fine", "fire", "fish", "flag", "flat", "flax", "flow", "foam", "fold", "font", "food", "foot", "fork", "form",
"fort", "fowl", "frog", "fuel", "full", "gain", "gale", "galn", "game", "garb", "gate", "gear", "gene", "gift",
"girl", "give", "glad", "glen", "glue", "glut", "goal", "goat", "gold", "golf", "gong", "good", "gown", "grab",
"gram", "gray", "grey", "grip", "grit", "gyro", "hail", "hair", "half", "hall", "hand", "hang", "harm", "harp",
"hate", "hawk", "head", "heat", "heel", "hell", "helo", "help", "hemp", "herb", "hide", "high", "hill", "hire",
"hive", "hold", "hole", "home", "hood", "hoof", "hook", "hope", "hops", "horn", "hose", "host", "hour", "hunt",
"hurt", "icon", "idea", "inch", "iris", "iron", "item", "jail", "jeep", "jeff", "joey", "join", "joke", "judo",
"jump", "junk", "jury", "jute", "kale", "keep", "kick", "kill", "kilt", "kind", "king", "kiss", "kite", "knee",
"knot", "lace", "lack", "lady", "lake", "lamb", "lamp", "land", "lark", "lava", "lawn", "lead", "leaf", "leek",
"lier", "life", "lift", "lily", "limo", "line", "link", "lion", "lisa", "list", "load", "loaf", "loan", "lock",
"loft", "long", "look", "loss", "lout", "love", "luck", "lung", "lute", "lynx", "lyre", "maid", "mail", "main",
"make", "male", "mall", "manx", "many", "mare", "mark", "mask", "mass", "mate", "math", "meal", "meat", "meet",
"menu", "mess", "mice", "midi", "mile", "milk", "mime", "mind", "mine", "mini", "mint", "miss", "mist", "moat",
"mode", "mole", "mood", "moon", "most", "moth", "move", "mule", "mutt", "nail", "name", "neat", "neck", "need",
"neon", "nest", "news", "node", "nose", "note", "oboe", "okra", "open", "oval", "oven", "oxen", "pace", "pack",
"page", "pail", "pain", "pair", "palm", "pard", "park", "part", "pass", "past", "path", "peak", "pear", "peen",
"peer", "pelt", "perp", "pest", "pick", "pier", "pike", "pile", "pimp", "pine", "ping", "pink", "pint", "pipe",
"piss", "pith", "plan", "play", "plot", "plow", "poem", "poet", "pole", "polo", "pond", "pony", "poof", "pool",
"port", "post", "prow", "pull", "puma", "pump", "pupa", "push", "quit", "race", "rack", "raft", "rage", "rail",
"rain", "rake", "rank", "rate", "read", "rear", "reef", "rent", "rest", "rice", "rich", "ride", "ring", "rise",
"risk", "road", "robe", "rock", "role", "roll", "roof", "room", "root", "rope", "rose", "ruin", "rule", "rush",
"ruth", "sack", "safe", "sage", "sail", "sale", "salt", "sand", "sari", "sash", "save", "scow", "seal", "seat",
"seed", "self", "sell", "shed", "shin", "ship", "shoe", "shop", "shot", "show", "sick", "side", "sign", "silk",
"sill", "silo", "sing", "sink", "site", "size", "skin", "sled", "slip", "smog", "snob", "snow", "soap", "sock",
"soda", "sofa", "soft", "soil", "song", "soot", "sort", "soup", "spot", "spur", "stag", "star", "stay", "stem",
"step", "stew", "stop", "stud", "suck", "suit", "swan", "swim", "tail", "tale", "talk", "tank", "tard", "task",
"taxi", "team", "tear", "teen", "tell", "temp", "tent", "term", "test", "text", "thaw", "tile", "till", "time",
"tire", "toad", "toga", "togs", "tone", "tool", "toot", "tote", "tour", "town", "tram", "tray", "tree", "trim",
"trip", "tuba", "tube", "tuna", "tune", "turn", "tutu", "twig", "type", "unit", "user", "vane", "vase", "vast",
"veal", "veil", "vein", "vest", "vibe", "view", "vise", "wait", "wake", "walk", "wall", "wash", "wasp", "wave",
"wear", "weed", "week", "well", "west", "whip", "wife", "will", "wind", "wine", "wing", "wire", "wish", "wolf",
"wood", "wool", "word", "work", "worm", "wrap", "wren", "yard", "yarn", "yawl", "year", "yoga", "yoke", "yurt",
"zinc", "zone"
};
}

View File

@ -0,0 +1,138 @@
#include "NewAccountController.h"
#include "Accounts/AccountsService.h"
#include <StatusGo/SignalsManager.h>
#include <QtConcurrent>
namespace Status::Onboarding
{
namespace StatusGo = Status::StatusGo;
NewAccountController::NewAccountController(std::shared_ptr<AccountsServiceInterface> accountsService, QObject *parent)
// TODO: remove dev dev setup after the final implementation
: m_name("TestAccount")
, m_nameIsValid(true)
, m_password("1234567890")
, m_passwordIsValid(true)
, m_confirmationPassword("1234567890")
, m_confirmationPasswordIsValid(true)
// END dev setup
, m_accountsService(accountsService)
{
connect(this, &NewAccountController::passwordChanged, this, &NewAccountController::checkAndUpdateDataValidity);
connect(this, &NewAccountController::confirmationPasswordChanged, this, &NewAccountController::checkAndUpdateDataValidity);
connect(this, &NewAccountController::nameChanged, this, &NewAccountController::checkAndUpdateDataValidity);
}
void NewAccountController::createAccount()
{
// TODO: fix this after moving SingalManager to StatusGo wrapper lib
QObject::connect(StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeLogin, this, &NewAccountController::onNodeLogin);
auto setupAccountFn = [this]() {
if(m_nameIsValid && m_passwordIsValid && m_confirmationPasswordIsValid) {
auto genAccounts = m_accountsService->generatedAccounts();
if(genAccounts.size() > 0) {
if(m_accountsService->setupAccountAndLogin(genAccounts[0].id, m_password, m_name))
return;
}
}
};
// TODO: refactor StatusGo wrapper to work with futures instead of SignalManager
m_createAccountFuture = QtConcurrent::run(setupAccountFn)
.then([]{ /*Nothing, we expect status-go events*/ })
.onFailed([this] {
emit accountCreationError();
})
.onCanceled([this] {
emit accountCreationError();
});
}
const QString &NewAccountController::password() const
{
return m_password;
}
void NewAccountController::setPassword(const QString &newPassword)
{
if (m_password == newPassword)
return;
m_password = newPassword;
emit passwordChanged();
}
const QString &NewAccountController::confirmationPassword() const
{
return m_confirmationPassword;
}
void NewAccountController::setConfirmationPassword(const QString &newConfirmationPassword)
{
if (m_confirmationPassword == newConfirmationPassword)
return;
m_confirmationPassword = newConfirmationPassword;
emit confirmationPasswordChanged();
}
const QString &NewAccountController::name() const
{
return m_name;
}
void NewAccountController::setName(const QString &newName)
{
if (m_name == newName)
return;
m_name = newName;
emit nameChanged();
}
bool NewAccountController::passwordIsValid() const
{
return m_passwordIsValid;
}
bool NewAccountController::confirmationPasswordIsValid() const
{
return m_confirmationPasswordIsValid;
}
bool NewAccountController::nameIsValid() const
{
return m_nameIsValid;
}
void NewAccountController::onNodeLogin(const QString& error)
{
if(error.isEmpty())
emit accountCreatedAndLoggedIn();
else
emit accountCreationError();
}
void NewAccountController::checkAndUpdateDataValidity()
{
auto passwordValid = m_password.length() >= 6;
if(passwordValid != m_passwordIsValid) {
m_passwordIsValid = passwordValid;
emit passwordIsValidChanged();
}
auto confirmationPasswordValid = m_password == m_confirmationPassword;
if(confirmationPasswordValid != m_confirmationPasswordIsValid) {
m_confirmationPasswordIsValid = confirmationPasswordValid;
emit confirmationPasswordIsValidChanged();
}
auto nameValid = m_name.length() >= 10;
if(nameValid != m_nameIsValid) {
m_nameIsValid = nameValid;
emit nameIsValidChanged();
}
}
} // namespace Status::Onboarding

View File

@ -0,0 +1,84 @@
#pragma once
#include "UserAccountsModel.h"
#include "Accounts/AccountsServiceInterface.h"
#include "Accounts/AccountDto.h"
#include <QtQmlIntegration>
#include <QFuture>
#include <memory>
namespace Status::Onboarding
{
class ServiceInterface;
/*! \brief presentation layer for creation of a new account workflow
*
* \todo shared functionality should be moved to Common library (e.g. Name/Picture Validation)
*/
class NewAccountController: public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("Created and owned externally")
Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
Q_PROPERTY(QString confirmationPassword READ confirmationPassword WRITE setConfirmationPassword NOTIFY confirmationPasswordChanged)
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
Q_PROPERTY(bool passwordIsValid READ passwordIsValid NOTIFY passwordIsValidChanged)
Q_PROPERTY(bool confirmationPasswordIsValid READ confirmationPasswordIsValid NOTIFY confirmationPasswordIsValidChanged)
Q_PROPERTY(bool nameIsValid READ nameIsValid NOTIFY nameIsValidChanged)
public:
explicit NewAccountController(std::shared_ptr<AccountsServiceInterface> accountsService, QObject* parent = nullptr);
Q_INVOKABLE void createAccount();
const QString &password() const;
void setPassword(const QString &newPassword);
const QString &confirmationPassword() const;
void setConfirmationPassword(const QString &newConfirmationPassword);
const QString &name() const;
void setName(const QString &newName);
bool passwordIsValid() const;
bool confirmationPasswordIsValid() const;
bool nameIsValid() const;
signals:
void passwordChanged();
void confirmationPasswordChanged();
void nameChanged();
void nameIsValidChanged();
void passwordIsValidChanged();
void confirmationPasswordIsValidChanged();
void accountCreatedAndLoggedIn();
void accountCreationError();
private slots:
void onNodeLogin(const QString& error);
private:
void checkAndUpdateDataValidity();
QString m_password;
QString m_confirmationPassword;
QString m_name;
bool m_passwordIsValid = false;
bool m_confirmationPasswordIsValid;
bool m_nameIsValid = false;
std::shared_ptr<AccountsServiceInterface> m_accountsService;
QFuture<void> m_createAccountFuture;
};
} // namespace Status::Onboarding

View File

@ -0,0 +1,77 @@
#include "OnboardingController.h"
#include "NewAccountController.h"
#include "UserAccount.h"
#include <StatusGo/SignalsManager.h>
namespace Status::Onboarding {
namespace StatusGo = Status::StatusGo;
OnboardingController::OnboardingController(std::shared_ptr<AccountsServiceInterface> accountsService)
: QObject(nullptr)
, m_accountsService(std::move(accountsService))
{
{ // Init accounts
std::vector<std::shared_ptr<UserAccount>> accounts;
for(auto &account : getOpenedAccounts()) {
accounts.push_back(std::make_shared<UserAccount>(std::make_unique<AccountDto>(std::move(account))));
}
m_accounts = std::make_shared<UserAccountsModel>(std::move(accounts));
}
connect(StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeLogin, this, &OnboardingController::onLogin);
}
OnboardingController::~OnboardingController()
{
// Here to move instatiation of unique_ptrs into this compilation unit
}
void OnboardingController::onLogin(const QString& error)
{
if(error.isEmpty())
emit accountLoggedIn();
else
emit accountLoginError(error);
}
std::vector<AccountDto> OnboardingController::getOpenedAccounts() const
{
return m_accountsService->openAndListAccounts();
}
void OnboardingController::login(QObject* user, const QString& password)
{
auto account = qobject_cast<UserAccount*>(user);
assert(account != nullptr);
auto error = m_accountsService->login(account->accountData(), password);
if(!error.isEmpty())
emit accountLoginError(error);
}
UserAccountsModel *OnboardingController::accounts() const
{
return m_accounts.get();
}
NewAccountController *OnboardingController::initNewAccountController()
{
m_newAccountController = std::make_unique<NewAccountController>(m_accountsService);
emit newAccountControllerChanged();
return m_newAccountController.get();
}
void OnboardingController::terminateNewAccountController()
{
m_newAccountController.release()->deleteLater();
emit newAccountControllerChanged();
}
NewAccountController *OnboardingController::newAccountController() const
{
return m_newAccountController.get();
}
}

View File

@ -0,0 +1,75 @@
#pragma once
#include "UserAccountsModel.h"
#include "Accounts/AccountsServiceInterface.h"
#include "Accounts/AccountDto.h"
#include <QQmlEngine>
#include <QtQmlIntegration>
#include <memory>
namespace Status::Onboarding
{
class UserAccount;
class NewAccountController;
/*!
* \todo refactor and remove the requirement to build only shared_ptr instances or use a factory
* \todo refactor unnedded multiple inheritance
* \todo don't use DTOs in controllers, use QObjects directly
* \todo make dependency on SignalManager explicit. Now it is hidden.
*/
class OnboardingController final : public QObject
, public std::enable_shared_from_this<OnboardingController>
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("Created by Module, for now")
Q_PROPERTY(UserAccountsModel* accounts READ accounts CONSTANT)
Q_PROPERTY(NewAccountController* newAccountController READ newAccountController NOTIFY newAccountControllerChanged)
public:
explicit OnboardingController(std::shared_ptr<AccountsServiceInterface> accountsService);
~OnboardingController();
/// Retrieve available accounts
std::vector<AccountDto> getOpenedAccounts() const;
/// Login user account
/// TODO: \a user should be of type \c UserAccount but this doesn't work with Qt6 CMake API. Investigate and fix later on
Q_INVOKABLE void login(QObject* user, const QString& password);
UserAccountsModel *accounts() const;
Q_INVOKABLE NewAccountController *initNewAccountController();
Q_INVOKABLE void terminateNewAccountController();
NewAccountController *newAccountController() const;
signals:
void accountLoggedIn();
void accountLoginError(const QString& error);
void obtainingPasswordError(const QString& errorDescription);
void obtainingPasswordSuccess(const QString& password);
void newAccountControllerChanged();
private slots:
void onLogin(const QString& error);
private:
const UserAccount* getSelectedAccount() const;
private:
std::shared_ptr<AccountsServiceInterface> m_accountsService;
std::shared_ptr<UserAccountsModel> m_accounts;
std::unique_ptr<NewAccountController> m_newAccountController;
};
}

View File

@ -0,0 +1,65 @@
#include "OnboardingModule.h"
#include "Accounts/AccountsService.h"
#include <ApplicationCore/UserConfiguration.h>
namespace AppCore = Status::ApplicationCore;
namespace fs = std::filesystem;
namespace Status::Onboarding {
OnboardingModule::OnboardingModule(const fs::path& userDataPath, QObject *parent)
: OnboardingModule{parent}
{
m_userDataPath = userDataPath;
initWithUserDataPath(m_userDataPath);
}
OnboardingModule::OnboardingModule(QObject *parent)
: QObject{parent}
, m_accountsService(std::make_shared<AccountsService>())
{
}
OnboardingController* OnboardingModule::controller() const
{
return m_controller.get();
}
void OnboardingModule::componentComplete()
{
try {
initWithUserDataPath(m_userDataPath);
} catch(const std::exception &e) {
qCritical() << "OnboardingModule: failed to initialize";
}
}
void OnboardingModule::initWithUserDataPath(const fs::path &path)
{
auto result = m_accountsService->init(path);
if(!result)
throw std::runtime_error(std::string("Failed to initialize OnboadingService") + path.string());
m_controller = std::make_shared<OnboardingController>(
m_accountsService);
emit controllerChanged();
}
const QString OnboardingModule::userDataPath() const
{
return QString::fromStdString(m_userDataPath.string());
}
void OnboardingModule::setUserDataPath(const QString &newUserDataPath)
{
auto newVal = newUserDataPath.toStdString();
if (m_userDataPath.compare(newVal) == 0)
return;
m_userDataPath = newVal;
emit userDataPathChanged();
}
}

View File

@ -0,0 +1,62 @@
#pragma once
#include "OnboardingController.h"
#include <QObject>
#include <QtQmlIntegration>
#include <filesystem>
namespace fs = std::filesystem;
namespace Status::Onboarding {
class AccountsService;
/*!
* \brief Provide bootstrap of controllers and corresponding services
*
* \warning status-go is a stateful library and having multiple insteances of the same module is undefined behaviour
* \todo current state is temporary until refactor StatusGo wrapper to match status-go requirements
* \warning current state all module spawned/controlled objects have C++ ownership this generate the risk of dangling
* invalid QML objects after module is destroyed. Consider moving all the ownership into QML after refactoring
*/
class OnboardingModule : public QObject, public QQmlParserStatus
{
Q_OBJECT
QML_ELEMENT
Q_INTERFACES(QQmlParserStatus)
Q_PROPERTY(OnboardingController* controller READ controller NOTIFY controllerChanged)
Q_PROPERTY(QString userDataPath READ userDataPath WRITE setUserDataPath NOTIFY userDataPathChanged REQUIRED)
public:
explicit OnboardingModule(const fs::path& userDataPath, QObject *parent = nullptr);
explicit OnboardingModule(QObject *parent = nullptr);
OnboardingController* controller() const;
const QString userDataPath() const;
void setUserDataPath(const QString &newUserDataPath);
/// QML inteface
void classBegin() override {};
void componentComplete() override;
signals:
void controllerChanged();
void userDataPathChanged();
private:
/// Throws exceptions
void initWithUserDataPath(const fs::path &path);
// TODO: plain object after refactoring shared_ptr requirement for now
std::shared_ptr<AccountsService> m_accountsService;
std::shared_ptr<OnboardingController> m_controller;
fs::path m_userDataPath;
};
}

View File

@ -0,0 +1,9 @@
# Onboarding refactoring
TODO
- [ ] Consider moving path requirements, into `StatusGoQt` or unify them as module requirement through abstraction
- [ ] Refactor to use typed IDs across Account and Login services instead of plain strings.
- A quick workaround would be to add a generic NamedType and convert strings at status-go APIs boundaries
- [ ] Bring uniformity to namespace: `Status::<domain>`. Don't go too deep, not deeper than two domain-related namespaces
- [ ] Consider RAII for controllers, remove `init`

View File

@ -0,0 +1,37 @@
#include "UserAccount.h"
#include "Accounts/AccountDto.h"
namespace Status::Onboarding
{
UserAccount::UserAccount(std::unique_ptr<AccountDto> data)
: QObject()
, m_data(std::move(data))
{
}
const QString &UserAccount::name() const
{
return m_data->name;
}
const AccountDto &UserAccount::accountData() const
{
return *m_data;
}
void UserAccount::updateAccountData(const AccountDto& newData)
{
std::vector<std::function<void()>> notifyUpdates;
*m_data = newData;
if(newData.name != m_data->name)
notifyUpdates.push_back([this]() { emit nameChanged(); });
}
}

View File

@ -0,0 +1,38 @@
#pragma once
#include <QtQmlIntegration>
namespace Status::Onboarding
{
class AccountDto;
/*!
* \brief Represents a user account in Onboarding Presentation Layer
*
* @see OnboardingController
* @see UserAccountsModel
*/
class UserAccount: public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("Created by Controller")
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
public:
explicit UserAccount(std::unique_ptr<AccountDto> data);
const QString &name() const;
const AccountDto& accountData() const;
void updateAccountData(const AccountDto& newData);
signals:
void nameChanged();
private:
std::unique_ptr<AccountDto> m_data;
};
}

View File

@ -0,0 +1,44 @@
#include "UserAccountsModel.h"
#include <QObject>
namespace Status::Onboarding {
UserAccountsModel::UserAccountsModel(const std::vector<std::shared_ptr<UserAccount>> accounts, QObject* parent)
: QAbstractListModel(parent)
, m_accounts(std::move(accounts))
{
}
UserAccountsModel::~UserAccountsModel()
{
}
QHash<int, QByteArray> UserAccountsModel::roleNames() const
{
static QHash<int, QByteArray> roles{
{Name, "name"},
{Account, "account"}
};
return roles;
}
int UserAccountsModel::rowCount(const QModelIndex& parent) const
{
Q_UNUSED(parent)
return m_accounts.size();
}
QVariant UserAccountsModel::data(const QModelIndex& index, int role) const
{
if(!QAbstractItemModel::checkIndex(index))
return QVariant();
switch(static_cast<ModelRole>(role)) {
case Name: return QVariant::fromValue(m_accounts[index.row()].get()->name());
case Account: return QVariant::fromValue<QObject*>(m_accounts[index.row()].get());
}
}
}

View File

@ -0,0 +1,36 @@
#pragma once
#include "UserAccount.h"
#include <QAbstractListModel>
#include <QQmlEngine>
namespace Status::Onboarding {
/*!
* \brief Available UserAccount elements
*/
class UserAccountsModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("Created by OnboardingController")
enum ModelRole {
Name = Qt::UserRole + 1,
Account
};
public:
explicit UserAccountsModel(const std::vector<std::shared_ptr<UserAccount>> accounts, QObject* parent = nullptr);
~UserAccountsModel();
QHash<int, QByteArray> roleNames() const override;
virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override;
virtual QVariant data(const QModelIndex& index, int role) const override;
private:
const std::vector<std::shared_ptr<UserAccount>> m_accounts;
};
}

View File

@ -0,0 +1,54 @@
# Unit and interface tests
cmake_minimum_required(VERSION 3.21)
project(TestOnboarding VERSION 0.1.0 LANGUAGES CXX)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Core REQUIRED)
qt6_standard_project_setup()
find_package(GTest REQUIRED)
enable_testing()
add_executable(${PROJECT_NAME}
test_AccountService.cpp
test_OnboardingController.cpp
test_OnboardingModule.cpp
ServiceMock.h
)
target_include_directories(${PROJECT_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
add_subdirectory(OnboardingTestHelpers)
add_subdirectory(qml_tests)
target_link_libraries(${PROJECT_NAME}
PRIVATE
Qt6::Core
GTest::gtest
GTest::gmock
GTest::gtest_main
Status::TestHelpers
Status::ApplicationCore
Status::OnboardingTestHelpers
Status::Onboarding
# TODO tmp
Status::StatusGoQt
)
include(GoogleTest)
gtest_add_tests(
TARGET ${PROJECT_NAME}
WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
)

View File

@ -0,0 +1,50 @@
# Base library. Expect most of the module libraries to depend on it
#
cmake_minimum_required(VERSION 3.21)
project(OnboardingTestHelpers
VERSION 0.1.0
LANGUAGES CXX)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(GTest REQUIRED)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
qt6_standard_project_setup()
qt6_add_qml_module(${PROJECT_NAME}
URI Status.${PROJECT_NAME}
VERSION 1.0
)
add_library(Status::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_include_directories(${PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_sources(${PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/ScopedTestAccount.h
${CMAKE_CURRENT_SOURCE_DIR}/Constants.h
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/ScopedTestAccount.cpp
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
)
target_link_libraries(${PROJECT_NAME}
PUBLIC
Qt6::Quick
Qt6::Qml
PRIVATE
Status::TestHelpers
Status::ApplicationCore
Status::Onboarding
Status::StatusGoQt
GTest::gtest
)

View File

@ -0,0 +1,15 @@
#pragma once
#include <string>
namespace Status::Testing::Constants {
inline constexpr auto userDataDirName = "StatusTest";
inline constexpr auto statusGoDataDirName = "data";
inline constexpr auto tmpDataDirName = "tmp";
inline constexpr auto logsDataDirName = "logs";
inline constexpr auto qtDataDirName = "qt";
inline constexpr auto keystoreDataDirName = "keystore";
inline constexpr auto globalSettingsFileName = "global";
}

View File

@ -0,0 +1,119 @@
#include "ScopedTestAccount.h"
#include <Constants.h>
#include <IOTestHelpers.h>
#include <Onboarding/Accounts/AccountsService.h>
#include <Onboarding/OnboardingController.h>
#include <StatusGo/Accounts/Accounts.h>
#include <QCoreApplication>
#include <gtest/gtest.h>
namespace Testing = Status::Testing;
namespace Onboarding = Status::Onboarding;
namespace fs = std::filesystem;
namespace Status::Testing {
ScopedTestAccount::ScopedTestAccount(const std::string &tempTestSubfolderName, const QString &accountName, const QString &accountPassword, bool ignorePreviousState)
: m_fusedTestFolder{std::make_unique<AutoCleanTempTestDir>(tempTestSubfolderName)}
, m_accountName(accountName)
, m_accountPassword(accountPassword)
{
int argc = 1;
std::string appName{"test"};
char* args[] = {appName.data()};
m_app = std::make_unique<QCoreApplication>(argc, reinterpret_cast<char**>(args));
m_testFolderPath = m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName;
fs::create_directory(m_testFolderPath);
// Setup accounts
auto accountsService = std::make_shared<Onboarding::AccountsService>();
auto result = accountsService->init(m_testFolderPath);
if(!result)
throw std::runtime_error("ScopedTestAccount - Failed to create temporary test account");
// TODO refactor and merge account creation events with login into Onboarding controller
//
// Create Login early to register and not miss onLoggedIn event signal from setupAccountAndLogin
//
// Beware, smartpointer is a requirement
m_onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
if(m_onboarding->getOpenedAccounts().size() != 0 && !ignorePreviousState)
throw std::runtime_error("ScopedTestAccount - already have opened account");
int accountLoggedInCount = 0;
QObject::connect(m_onboarding.get(), &Onboarding::OnboardingController::accountLoggedIn, [&accountLoggedInCount]() {
accountLoggedInCount++;
});
bool accountLoggedInError = false;
QObject::connect(m_onboarding.get(), &Onboarding::OnboardingController::accountLoginError, [&accountLoggedInError]() {
accountLoggedInError = true;
});
// Create Accounts
auto genAccounts = accountsService->generatedAccounts();
if(genAccounts.size() == 0)
throw std::runtime_error("ScopedTestAccount - missing generated accounts");
if(accountsService->isFirstTimeAccountLogin())
throw std::runtime_error("ScopedTestAccount - Service::isFirstTimeAccountLogin returned true");
if(!accountsService->setupAccountAndLogin(genAccounts[0].id, m_accountPassword, m_accountName))
throw std::runtime_error("ScopedTestAccount - Service::setupAccountAndLogin failed");
if(!accountsService->isFirstTimeAccountLogin())
throw std::runtime_error("ScopedTestAccount - Service::isFirstTimeAccountLogin returned false");
if(!accountsService->getLoggedInAccount().isValid())
throw std::runtime_error("ScopedTestAccount - newly created account is not valid");
if(accountsService->getLoggedInAccount().name != accountName)
throw std::runtime_error("ScopedTestAccount - newly created account has a wrong name");
processMessages(2000, [accountLoggedInCount]() {
return accountLoggedInCount == 0;
});
if(accountLoggedInCount != 1)
throw std::runtime_error("ScopedTestAccount - missing confirmation of account creation");
if(accountLoggedInError)
throw std::runtime_error("ScopedTestAccount - account loggedin error");
}
ScopedTestAccount::~ScopedTestAccount()
{
}
void ScopedTestAccount::processMessages(size_t maxWaitTimeMillis, std::function<bool()> shouldWaitUntilTimeout) {
using namespace std::chrono_literals;
std::chrono::milliseconds maxWaitTime{maxWaitTimeMillis};
auto iterationSleepTime = 2ms;
auto remainingIterations = maxWaitTime/iterationSleepTime;
while (remainingIterations-- > 0 && shouldWaitUntilTimeout()) {
std::this_thread::sleep_for(iterationSleepTime);
QCoreApplication::sendPostedEvents();
}
}
void ScopedTestAccount::logOut()
{
if(Status::StatusGo::Accounts::logout().containsError())
throw std::runtime_error("ScopedTestAccount - failed logging out");
}
Onboarding::OnboardingController *ScopedTestAccount::onboardingController() const
{
return m_onboarding.get();
}
const std::filesystem::path &ScopedTestAccount::fusedTestFolder() const
{
return m_testFolderPath;
}
}

View File

@ -0,0 +1,52 @@
#pragma once
#include <string>
#include <filesystem>
#include <QString>
class QCoreApplication;
namespace Status::Onboarding {
class OnboardingController;
}
namespace Status::Testing {
class AutoCleanTempTestDir;
class ScopedTestAccount final {
public:
/*!
* \brief Create and logs in a new test account
* \param tempTestSubfolderName subfolder name of the temporary test folder where to initalize user data \see AutoCleanTempTestDir
* \todo make it more flexible by splitting into create account, login and wait for events
*/
explicit ScopedTestAccount(const std::string &tempTestSubfolderName,
const QString &accountName = defaultAccountName,
const QString &accountPassword = defaultAccountPassword,
bool ignorePreviousState = false /*workaround to status-go persisting state*/);
~ScopedTestAccount();
void processMessages(size_t millis, std::function<bool()> shouldWaitUntilTimeout);
void logOut();
Status::Onboarding::OnboardingController* onboardingController() const;
const std::filesystem::path& fusedTestFolder() const;;
private:
std::unique_ptr<AutoCleanTempTestDir> m_fusedTestFolder;
std::unique_ptr<QCoreApplication> m_app;
std::filesystem::path m_testFolderPath;
std::shared_ptr<Status::Onboarding::OnboardingController> m_onboarding;
std::function<bool()> m_checkIfShouldContinue;
QString m_accountName;
QString m_accountPassword;
static constexpr auto defaultAccountName = "test_name";
static constexpr auto defaultAccountPassword = "test_pwd*";
};
}

View File

@ -0,0 +1,37 @@
#pragma once
#include "Onboarding/Accounts/AccountsServiceInterface.h"
#include <gmock/gmock.h>
namespace Onboarding = Status::Onboarding;
namespace Status::Testing
{
/*!
* \brief The AccountsServiceMock test class
*
* \todo Consider if this is really neaded for testing controllers
* \todo Move it to mocks subfolder
*/
class AccountsServiceMock final : public Onboarding::AccountsServiceInterface
{
public:
virtual ~AccountsServiceMock() override {};
MOCK_METHOD(bool, init, (const fs::path&), (override));
MOCK_METHOD(std::vector<Onboarding::AccountDto>, openAndListAccounts, (), (override));
MOCK_METHOD(const std::vector<Onboarding::GeneratedAccountDto>&, generatedAccounts, (), (const, override));
MOCK_METHOD(bool, setupAccountAndLogin, (const QString&, const QString&, const QString&), (override));
MOCK_METHOD(const Onboarding::AccountDto&, getLoggedInAccount, (), (const, override));
MOCK_METHOD(const Onboarding::GeneratedAccountDto&, getImportedAccount, (), (const, override));
MOCK_METHOD(bool, isFirstTimeAccountLogin, (), (const, override));
MOCK_METHOD(bool, setKeyStoreDir, (const QString&), (override));
MOCK_METHOD(QString, login, (Onboarding::AccountDto, const QString&), (override));
MOCK_METHOD(void, clear, (), (override));
MOCK_METHOD(QString, generateAlias, (const QString&), (override));
MOCK_METHOD(QString, generateIdenticon, (const QString&), (override));
};
}

View File

@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.5)
project(TestOnboardingQml LANGUAGES CXX)
enable_testing(true)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml QuickTest REQUIRED)
add_executable(TestOnboardingQml
"main.cpp"
)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# no need to copy around qml test files for shadow builds - just set the respective define
add_compile_definitions(QUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
add_test(NAME TestOnboardingQml WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${CMAKE_CURRENT_BINARY_DIR}/TestOnboardingQml -input "${CMAKE_CURRENT_SOURCE_DIR}")
add_custom_target("Run_TestOnboardingQml" COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_CURRENT_BINARY_DIR}")
add_dependencies("Run_TestOnboardingQml" TestOnboardingQml)
target_link_libraries(TestOnboardingQml PRIVATE
Qt6::QuickTest
Qt6::Qml
Qt6::Quick
Status::TestHelpers
Status::Onboarding
)

View File

@ -0,0 +1,3 @@
#include <QtQuickTest>
QUICK_TEST_MAIN(TestOnboardingQml)

View File

@ -0,0 +1,86 @@
import QtQuick
import QtQml
import QtTest
import Status.Onboarding
import Status.TestHelpers
/**!
* \todo use mocked values
*/
Item {
id: root
width: 400
height: 300
Component {
id: onboardingDepsComponent
Item {
OnboardingModule {
id: module
userDataPath: "/tmp/StatusTests/demo"
}
// TODO: fix error "unable to assign Status::Onboarding::OnboardingController to Status::Onboarding::OnboardingController" then enable typed properties
readonly property var /*OnboardingController*/ controller: module.controller
readonly property var /*UserAccountsModel*/ accounts: controller.accounts
}
}
Loader {
id: testLoader
anchors.fill: parent
active: false
}
TestCase {
id: qmlWarningsTest
name: "TestQmlWarnings"
when: windowShown
//
// Test guards
function init() {
qtOuput.restartCapturing()
}
function cleanup() {
testLoader.active = false
}
//
// Tests
function test_moduleInitialization() {
testLoader.sourceComponent = onboardingDepsComponent
testLoader.active = true
verify(waitForRendering(testLoader.item))
testLoader.active = false
verify(qtOuput.qtOuput().length === 0, `No output expected. Found:\n"${qtOuput.qtOuput()}"\n`)
}
}
// TestCase {
// id: qmlBenchmarks
// name: "QmlBenchmarks"
// function benchmark_loadAndUnloadModule() {
// skip("Enable benchmarking after integrating it with reporting in CI")
// testLoader.sourceComponent = onboardingDepsComponent
// testLoader.active = true
// testLoader.active = false
// }
// }
MonitorQtOutput {
id: qtOuput
}
}

View File

@ -0,0 +1,60 @@
#include "ServiceMock.h"
#include <IOTestHelpers.h>
#include <Constants.h>
#include <Onboarding/Accounts/AccountsService.h>
#include <gtest/gtest.h>
namespace Testing = Status::Testing;
namespace Onboarding = Status::Onboarding;
namespace fs = std::filesystem;
namespace Status::Testing {
class AccountsServicesTest : public ::testing::Test
{
protected:
std::unique_ptr<Onboarding::AccountsService> m_accountsService;
std::unique_ptr<Testing::AutoCleanTempTestDir> m_fusedTestFolder;
void SetUp() override {
m_fusedTestFolder = std::make_unique<Testing::AutoCleanTempTestDir>("AccountsServicesTest");
m_accountsService = std::make_unique<Onboarding::AccountsService>();
m_accountsService->init(m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName);
}
void TearDown() override {
m_fusedTestFolder.reset();
m_accountsService.reset();
}
};
TEST_F(AccountsServicesTest, GeneratedAccounts)
{
auto genAccounts = m_accountsService->generatedAccounts();
ASSERT_EQ(5, genAccounts.size());
for(const auto& acc : genAccounts)
{
ASSERT_STRNE(qUtf8Printable(acc.id), "");
ASSERT_STRNE(qUtf8Printable(acc.publicKey), "");
ASSERT_STRNE(qUtf8Printable(acc.address), "");
ASSERT_STRNE(qUtf8Printable(acc.keyUid), "");
}
}
TEST_F(AccountsServicesTest, DISABLED_GenerateAlias) // temporary disabled till we see what's happening on the status-go side since it doesn't return aliases for any pk
{
QString testPubKey = "0x04487f44bac3e90825bfa9720148308cb64835bebb7e888f519cebc127223187067629f8b70d0661a35d4af6516b225286";
auto alias = m_accountsService->generateAlias(testPubKey);
ASSERT_NE(alias, QString(""));
}
} // namespace

View File

@ -0,0 +1,55 @@
#include "ServiceMock.h"
#include <Constants.h>
#include <IOTestHelpers.h>
#include <Onboarding/Accounts/AccountsService.h>
#include <Onboarding/OnboardingController.h>
#include <gtest/gtest.h>
#include <memory>
namespace Onboarding = Status::Onboarding;
namespace fs = std::filesystem;
namespace Status::Testing {
class LoginTest : public ::testing::Test
{
protected:
static std::shared_ptr<AccountsServiceMock> m_accountsServiceMock;
std::unique_ptr<Onboarding::AccountsService> m_accountsService;
std::unique_ptr<Testing::AutoCleanTempTestDir> m_fusedTestFolder;
static void SetUpTestSuite() {
m_accountsServiceMock = std::make_shared<AccountsServiceMock>();
}
static void TearDownTestSuite() {
m_accountsServiceMock.reset();
}
void SetUp() override {
m_fusedTestFolder = std::make_unique<Testing::AutoCleanTempTestDir>("LoginTest");
m_accountsService = std::make_unique<Onboarding::AccountsService>();
m_accountsService->init(m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName);
}
void TearDown() override {
m_fusedTestFolder.release();
m_accountsService.release();
}
};
std::shared_ptr<AccountsServiceMock> LoginTest::m_accountsServiceMock;
TEST_F(LoginTest, DISABLED_TestLoginController)
{
// Controller hides as a regular class but at runtime it must be a shared pointer; TODO: refactor
auto controller = std::make_shared<Onboarding::OnboardingController>(m_accountsServiceMock);
}
} // namespace

View File

@ -0,0 +1,170 @@
#include <IOTestHelpers.h>
#include "ServiceMock.h"
#include <Constants.h>
#include <Onboarding/Accounts/AccountsService.h>
#include <Onboarding/OnboardingController.h>
#include <StatusGo/SignalsManager.h>
#include <StatusGo/Accounts/Accounts.h>
#include <ScopedTestAccount.h>
#include <QCoreApplication>
#include <gtest/gtest.h>
namespace Testing = Status::Testing;
namespace Onboarding = Status::Onboarding;
namespace fs = std::filesystem;
namespace Status::Testing {
static std::unique_ptr<Onboarding::AccountsService> m_accountsServiceMock;
TEST(OnboardingModule, TestInitService)
{
Testing::AutoCleanTempTestDir fusedTestFolder{test_info_->name()};
auto testFolderPath = fusedTestFolder.tempFolder() / Constants::statusGoDataDirName;
fs::create_directory(testFolderPath);
auto accountsService = std::make_unique<Onboarding::AccountsService>();
ASSERT_TRUE(accountsService->init(testFolderPath));
}
/// This integration end to end test is here for documentation purpose and until all the functionality is covered by unit-tests
/// \warning the test depends on IO and it is not deterministic, fast, focused or reliable and uses production classes. It is here for documenting only and dev process
/// \todo refactor into unit-tests with mocked interfaces
TEST(OnboardingModule, TestCreateAndLoginAccountEndToEnd)
{
int argc = 1;
std::string appName{"test"};
char* args[] = {appName.data()};
QCoreApplication dummyApp{argc, reinterpret_cast<char**>(args)};
Testing::AutoCleanTempTestDir fusedTestFolder{test_info_->name()};
auto testFolderPath = fusedTestFolder.tempFolder() / "Status Desktop";
fs::create_directory(testFolderPath);
// Setup accounts
auto accountsService = std::make_shared<Onboarding::AccountsService>();
auto result = accountsService->init(testFolderPath);
ASSERT_TRUE(result);
// TODO refactor and merge account creation events with login into Onboarding controller
//
// Create Login early to register and not miss onLoggedIn event signal from setupAccountAndLogin
//
// Beware, smartpointer is a requirement
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
EXPECT_EQ(onboarding->getOpenedAccounts().size(), 0);
int accountLoggedInCount = 0;
QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoggedIn, [&accountLoggedInCount]() {
accountLoggedInCount++;
});
bool accountLoggedInError = false;
QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError, [&accountLoggedInError]() {
accountLoggedInError = true;
});
// Create Accounts
auto genAccounts = accountsService->generatedAccounts();
ASSERT_GT(genAccounts.size(), 0);
ASSERT_FALSE(accountsService->isFirstTimeAccountLogin());
constexpr auto accountName = "test_name";
constexpr auto accountPassword = "test_pwd*";
ASSERT_TRUE(accountsService->setupAccountAndLogin(genAccounts[0].id, accountPassword, accountName));
ASSERT_TRUE(accountsService->isFirstTimeAccountLogin());
ASSERT_TRUE(accountsService->getLoggedInAccount().isValid());
ASSERT_TRUE(accountsService->getLoggedInAccount().name == accountName);
ASSERT_FALSE(accountsService->getImportedAccount().isValid());
using namespace std::chrono_literals;
auto maxWaitTime = 2000ms;
auto iterationSleepTime = 2ms;
auto remainingIterations = maxWaitTime/iterationSleepTime;
while (remainingIterations-- > 0 && accountLoggedInCount == 0) {
std::this_thread::sleep_for(iterationSleepTime);
QCoreApplication::sendPostedEvents();
}
EXPECT_EQ(accountLoggedInCount, 1);
EXPECT_FALSE(accountLoggedInError);
EXPECT_FALSE(Status::StatusGo::Accounts::logout().containsError());
}
/// This integration end to end test is here for documentation purpose and until all the functionality is covered by unit-tests
/// \warning the test depends on IO and it is not deterministic, fast, focused or reliable. It is here for validation only
/// \todo find a way to test the integration within a test environment. Also how about reusing an existing account
/// \todo due to keeping status-go keeping the state thsi works only run separately
TEST(OnboardingModule, TestLoginEndToEnd)
{
// Create test account and login
//
bool createAndLogin = false;
QObject::connect(StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeLogin, [&createAndLogin](const QString& error) {
if(error.isEmpty()) {
if(createAndLogin) {
createAndLogin = false;
} else
createAndLogin = true;
}
});
constexpr auto accountName = "TestLoginAccountName";
constexpr auto accountPassword = "1234567890";
ScopedTestAccount testAccount(test_info_->name(), accountName, accountPassword, true);
testAccount.processMessages(1000, [createAndLogin]() {
return !createAndLogin;
});
ASSERT_TRUE(createAndLogin);
testAccount.logOut();
// Test account log in
//
// Setup accounts
auto accountsService = std::make_shared<Onboarding::AccountsService>();
auto result = accountsService->init(testAccount.fusedTestFolder());
ASSERT_TRUE(result);
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
// We don't have a way yet to simulate status-go process exit
//EXPECT_EQ(onboarding->getOpenedAccounts().count(), 0);
auto accounts = accountsService->openAndListAccounts();
//ASSERT_EQ(accounts.size(), 1);
ASSERT_GT(accounts.size(), 0);
int accountLoggedInCount = 0;
QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoggedIn, [&accountLoggedInCount]() {
accountLoggedInCount++;
});
bool accountLoggedInError = false;
QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError, [&accountLoggedInError]() {
accountLoggedInError = true;
});
//auto errorString = accountsService->login(accounts[0], accountPassword);
// Workaround until we reset the status-go state
auto ourAccountRes = std::find_if(accounts.begin(), accounts.end(), [accountName](const auto &a) { return a.name == accountName; });
auto errorString = accountsService->login(*ourAccountRes, accountPassword);
ASSERT_EQ(errorString.length(), 0);
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
return accountLoggedInCount == 0 && !accountLoggedInError;
});
ASSERT_EQ(accountLoggedInCount, 1);
ASSERT_EQ(accountLoggedInError, 0);
}
} // namespace

View File

@ -8,10 +8,10 @@ project(StatusGoQt
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Core REQUIRED)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Core Concurrent REQUIRED)
qt6_standard_project_setup()
add_library(${PROJECT_NAME} SHARED "")
add_library(${PROJECT_NAME} SHARED)
# Use by linker only
set_property(GLOBAL PROPERTY DEBUG_CONFIGURATIONS Debug)
@ -19,19 +19,30 @@ set_property(GLOBAL PROPERTY DEBUG_CONFIGURATIONS Debug)
add_subdirectory(src)
target_link_libraries(${PROJECT_NAME}
PRIVATE
PUBLIC
Qt6::Core
Qt6::Concurrent
PRIVATE
statusgo_shared
)
add_library(Status::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
# Copy status-go lib close to the executable
# Temporary workaround; TODO: see a better alternative that doesn't depend on target order (dedicated dependencies dir?)
# and current directory (on mac). Use bundle or set rpath relative to executable
get_target_property(STATUSGO_LIBRARY_PATH statusgo_shared IMPORTED_LOCATION)
configure_file(${STATUSGO_LIBRARY_PATH} ${CMAKE_CURRENT_BINARY_DIR} COPYONLY)
add_custom_command(
TARGET
${PROJECT_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:statusgo_shared>
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
COMMENT "Copying status-go lib beside project executable"
)
install(
TARGETS
${PROJECT_NAME}
RUNTIME
)
IMPORTED_RUNTIME_ARTIFACTS
statusgo_shared
)

View File

@ -1,14 +1,14 @@
#include "StatusBackend/Accounts.h"
#include "Accounts.h"
#include "StatusBackend/Utils.h"
#include "Utils.h"
#include "libstatus.h"
const int NUMBER_OF_ADDRESSES_TO_GENERATE = 5;
const int MNEMONIC_PHRASE_LENGTH = 12;
using namespace Backend;
namespace Status::StatusGo::Accounts {
RpcResponse<QJsonArray> Accounts::generateAddresses(const QVector<QString>& paths)
RpcResponse<QJsonArray> generateAddresses(const QVector<QString>& paths)
{
QJsonObject payload{
{"n", NUMBER_OF_ADDRESSES_TO_GENERATE},
@ -27,7 +27,7 @@ RpcResponse<QJsonArray> Accounts::generateAddresses(const QVector<QString>& path
throw std::domain_error(msg.toStdString());
}
return Utils::buildJsonRpcResponse(jsonResult);
return Utils::buildPrivateRPCResponse(jsonResult);
}
catch (std::exception& e)
{
@ -43,7 +43,7 @@ RpcResponse<QJsonArray> Accounts::generateAddresses(const QVector<QString>& path
}
}
RpcResponse<QString> Accounts::generateIdenticon(const QString& publicKey)
RpcResponse<QString> generateIdenticon(const QString& publicKey)
{
try
{
@ -52,7 +52,7 @@ RpcResponse<QString> Accounts::generateIdenticon(const QString& publicKey)
{
identicon = Identicon(publicKey.toUtf8().data());
}
return Utils::buildJsonRpcResponse(identicon);
return Utils::buildPrivateRPCResponse(identicon);
}
catch (...)
{
@ -62,7 +62,7 @@ RpcResponse<QString> Accounts::generateIdenticon(const QString& publicKey)
}
}
RpcResponse<QString> Accounts::generateAlias(const QString& publicKey)
RpcResponse<QString> generateAlias(const QString& publicKey)
{
try
{
@ -72,7 +72,7 @@ RpcResponse<QString> Accounts::generateAlias(const QString& publicKey)
alias = GenerateAlias(publicKey.toUtf8().data());
}
return Utils::buildJsonRpcResponse(alias);
return Utils::buildPrivateRPCResponse(alias);
}
catch (...)
{
@ -82,7 +82,7 @@ RpcResponse<QString> Accounts::generateAlias(const QString& publicKey)
}
}
RpcResponse<QJsonObject> Accounts::storeDerivedAccounts(const QString& id, const QString& hashedPassword,
RpcResponse<QJsonObject> storeDerivedAccounts(const QString& id, const QString& hashedPassword,
const QVector<QString>& paths)
{
QJsonObject payload{
@ -101,7 +101,9 @@ RpcResponse<QJsonObject> Accounts::storeDerivedAccounts(const QString& id, const
throw std::domain_error(msg.toStdString());
}
return Utils::buildJsonRpcResponse(jsonResult);
RpcResponse<QJsonObject> rpcResponse(jsonResult);
rpcResponse.error = Utils::getRPCErrorInJson(jsonResult).value_or(RpcError());
return rpcResponse;
}
catch (std::exception& e)
{
@ -117,17 +119,16 @@ RpcResponse<QJsonObject> Accounts::storeDerivedAccounts(const QString& id, const
}
}
RpcResponse<QJsonObject> Accounts::saveAccountAndLogin(const QString& hashedPassword, const QJsonObject& account,
const QJsonArray& subaccounts, const QJsonObject& settings,
const QJsonObject& nodeConfig)
RpcResponse<QJsonObject> storeAccount(const QString& id, const QString& hashedPassword)
{
QJsonObject payload{
{"accountID", id},
{"password", hashedPassword}
};
try
{
auto result = SaveAccountAndLogin(Utils::jsonToByteArray(std::move(account)).data(),
hashedPassword.toUtf8().data(),
Utils::jsonToByteArray(std::move(settings)).data(),
Utils::jsonToByteArray(std::move(nodeConfig)).data(),
Utils::jsonToByteArray(std::move(subaccounts)).data());
auto result = MultiAccountStoreAccount(Utils::jsonToByteArray(std::move(payload)).data());
QJsonObject jsonResult;
if(!Utils::checkReceivedResponse(result, jsonResult))
{
@ -135,39 +136,70 @@ RpcResponse<QJsonObject> Accounts::saveAccountAndLogin(const QString& hashedPass
throw std::domain_error(msg.toStdString());
}
return Utils::buildJsonRpcResponse(jsonResult);
RpcResponse<QJsonObject> rpcResponse(jsonResult);
rpcResponse.error = Utils::getRPCErrorInJson(jsonResult).value_or(RpcError());
return rpcResponse;
}
catch (std::exception& e)
{
auto response = RpcResponse<QJsonObject>(QJsonObject());
response.error.message = QObject::tr("an error saving account and login occurred, msg: %1").arg(e.what());
response.error.message = QObject::tr("an error storing account occurred, msg: %1").arg(e.what());
return response;
}
catch (...)
{
auto response = RpcResponse<QJsonObject>(QJsonObject());
response.error.message = QObject::tr("an error saving account and login occurred");
response.error.message = QObject::tr("an error storing account occurred");
return response;
}
}
Backend::RpcResponse<QJsonArray> Backend::Accounts::openAccounts(const QString& path)
bool saveAccountAndLogin(const QString& hashedPassword, const QJsonObject& account,
const QJsonArray& subaccounts, const QJsonObject& settings,
const QJsonObject& nodeConfig)
{
try
{
auto result = OpenAccounts(path.toUtf8().data());
QJsonArray jsonResult;
auto result = SaveAccountAndLogin(Utils::jsonToByteArray(account).data(),
hashedPassword.toUtf8().data(),
Utils::jsonToByteArray(settings).data(),
Utils::jsonToByteArray(nodeConfig).data(),
Utils::jsonToByteArray(subaccounts).data());
QJsonObject jsonResult;
if(!Utils::checkReceivedResponse(result, jsonResult))
{
auto msg = QObject::tr("parsing response failed");
throw std::domain_error(msg.toStdString());
}
return Utils::buildJsonRpcResponse(jsonResult);
return !Utils::getRPCErrorInJson(jsonResult).has_value();
} catch (std::exception& e) {
qWarning() << QString("an error saving account and login occurred, msg: %1").arg(e.what());
} catch (...) {
qWarning() << "an error saving account and login occurred";
}
return false;
}
RpcResponse<QJsonArray> openAccounts(const char* dataDirPath)
{
try
{
auto result = QString(OpenAccounts(const_cast<char*>(dataDirPath)));
if(result == "null")
return RpcResponse<QJsonArray>(QJsonArray());
QJsonArray jsonResult;
if(!Utils::checkReceivedResponse(result, jsonResult)) {
throw std::domain_error("parsing response failed");
}
return Utils::buildPrivateRPCResponse(jsonResult);
}
catch (std::exception& e)
{
auto response = RpcResponse<QJsonArray>(QJsonArray());
// TODO: don't translate exception messages. Exceptions are for developers and should never reach users
response.error.message = QObject::tr("an error opening accounts occurred, msg: %1").arg(e.what());
return response;
}
@ -179,14 +211,13 @@ Backend::RpcResponse<QJsonArray> Backend::Accounts::openAccounts(const QString&
}
}
RpcResponse<QJsonObject> Accounts::login(const QString& name, const QString& keyUid, const QString& hashedPassword,
RpcResponse<QJsonObject> login(const QString& name, const QString& keyUid, const QString& hashedPassword,
const QString& identicon, const QString& thumbnail, const QString& large)
{
QJsonObject payload{
{"name", name},
{"key-uid", keyUid},
{"identityImage", QJsonValue()},
{"identicon", identicon}
{"identityImage", QJsonValue()}
};
if(!thumbnail.isEmpty() && !large.isEmpty())
@ -196,7 +227,8 @@ RpcResponse<QJsonObject> Accounts::login(const QString& name, const QString& key
try
{
auto result = Login(Utils::jsonToByteArray(std::move(payload)).data(), hashedPassword.toUtf8().data());
auto payloadData = Utils::jsonToByteArray(std::move(payload));
auto result = Login(payloadData.data(), hashedPassword.toUtf8().data());
QJsonObject jsonResult;
if(!Utils::checkReceivedResponse(result, jsonResult))
{
@ -204,7 +236,7 @@ RpcResponse<QJsonObject> Accounts::login(const QString& name, const QString& key
throw std::domain_error(msg.toStdString());
}
return Utils::buildJsonRpcResponse(jsonResult);
return Utils::buildPrivateRPCResponse(jsonResult);
}
catch (std::exception& e)
{
@ -219,3 +251,78 @@ RpcResponse<QJsonObject> Accounts::login(const QString& name, const QString& key
return response;
}
}
RpcResponse<QJsonObject> loginWithConfig(const QString& name, const QString& keyUid, const QString& hashedPassword,
const QString& identicon, const QString& thumbnail, const QString& large,
const QJsonObject& nodeConfig)
{
QJsonObject payload{
{"name", name},
{"key-uid", keyUid},
{"identityImage", QJsonValue()},
};
if(!thumbnail.isEmpty() && !large.isEmpty())
{
payload["identityImage"] = QJsonObject{{"thumbnail", thumbnail}, {"large", large}};
}
try
{
auto payloadData = Utils::jsonToByteArray(std::move(payload));
auto nodeConfigData = Utils::jsonToByteArray(nodeConfig);
auto result = LoginWithConfig(payloadData.data(), hashedPassword.toUtf8().data(), nodeConfigData.data());
QJsonObject jsonResult;
if(!Utils::checkReceivedResponse(result, jsonResult))
{
auto msg = QObject::tr("parsing response failed");
throw std::domain_error(msg.toStdString());
}
return Utils::buildPrivateRPCResponse(jsonResult);
}
catch (std::exception& e)
{
auto response = RpcResponse<QJsonObject>(QJsonObject());
response.error.message = QObject::tr("an error logining in account occurred, msg: %1").arg(e.what());
return response;
}
catch (...)
{
auto response = RpcResponse<QJsonObject>(QJsonObject());
response.error.message = QObject::tr("an error logining in account occurred");
return response;
}
}
RpcResponse<QJsonObject> logout()
{
try
{
auto result = Logout();
QJsonObject jsonResult;
if(!Utils::checkReceivedResponse(result, jsonResult))
{
auto msg = QObject::tr("parsing response failed");
throw std::domain_error(msg.toStdString());
}
RpcResponse<QJsonObject> rpcResponse(jsonResult);
rpcResponse.error = Utils::getRPCErrorInJson(jsonResult).value_or(RpcError());
return rpcResponse;
}
catch (std::exception& e)
{
auto response = RpcResponse<QJsonObject>(QJsonObject());
response.error.message = QObject::tr("an error logging out account occurred, msg: %1").arg(e.what());
return response;
}
catch (...)
{
auto response = RpcResponse<QJsonObject>(QJsonObject());
response.error.message = QObject::tr("an error logging out account occurred");
return response;
}
}
}

View File

@ -4,7 +4,7 @@
#include <QtCore>
namespace Backend::Accounts
namespace Status::StatusGo::Accounts
{
RpcResponse<QJsonArray> generateAddresses(const QVector<QString>& paths);
@ -15,12 +15,19 @@ namespace Backend::Accounts
RpcResponse<QJsonObject> storeDerivedAccounts(const QString& accountId, const QString& hashedPassword,
const QVector<QString>& paths);
RpcResponse<QJsonObject> saveAccountAndLogin(const QString& hashedPassword, const QJsonObject& account,
RpcResponse<QJsonObject> storeAccount(const QString& id, const QString& hashedPassword);
bool saveAccountAndLogin(const QString& hashedPassword, const QJsonObject& account,
const QJsonArray& subaccounts, const QJsonObject& settings,
const QJsonObject& nodeConfig);
RpcResponse<QJsonArray> openAccounts(const QString& path);
/// opens database and returns accounts list.
RpcResponse<QJsonArray> openAccounts(const char* dataDirPath);
RpcResponse<QJsonObject> login(const QString& name, const QString& keyUid, const QString& hashedPassword,
const QString& identicon, const QString& thumbnail, const QString& large);
RpcResponse<QJsonObject> loginWithConfig(const QString& name, const QString& keyUid, const QString& hashedPassword,
const QString& identicon, const QString& thumbnail, const QString& large,
const QJsonObject& nodeConfig);
RpcResponse<QJsonObject> logout();
}

View File

@ -0,0 +1,22 @@
target_sources(${PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/General.h
${CMAKE_CURRENT_SOURCE_DIR}/Utils.h
${CMAKE_CURRENT_SOURCE_DIR}/Accounts/Accounts.h
${CMAKE_CURRENT_SOURCE_DIR}/Messenger/Service.h
${CMAKE_CURRENT_SOURCE_DIR}/SignalsManager.h
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/General.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Types.h
${CMAKE_CURRENT_SOURCE_DIR}/Utils.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Accounts/Accounts.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Messenger/Service.cpp
${CMAKE_CURRENT_SOURCE_DIR}/SignalsManager.cpp
)

View File

@ -0,0 +1,37 @@
#include "General.h"
#include "Utils.h"
#include "libstatus.h"
namespace Status::StatusGo::General
{
RpcResponse<QJsonObject> initKeystore(const char* keystoreDir)
{
try
{
auto result = InitKeystore(const_cast<char*>(keystoreDir));
QJsonObject jsonResult;
if(!Utils::checkReceivedResponse(result, jsonResult)) {
throw std::domain_error("parsing response failed");
}
return Utils::buildPrivateRPCResponse(jsonResult);
}
catch (std::exception& e)
{
// TODO: either use optional/smartpointers or exceptions instead of plain objects
auto response = RpcResponse<QJsonObject>(QJsonObject());
// TODO: don't translate exception messages. Exceptions are for developers and should never reach users
response.error.message = QObject::tr("an error opening accounts occurred, msg: %1").arg(e.what());
return response;
}
catch (...)
{
auto response = RpcResponse<QJsonObject>(QJsonObject());
response.error.message = QObject::tr("an error opening accounts occurred");
return response;
}
}
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "Types.h"
#include <QJsonObject>
namespace Status::StatusGo::General
{
RpcResponse<QJsonObject> initKeystore(const char* keystoreDir);
}

View File

@ -0,0 +1,22 @@
#include "Service.h"
#include "Utils.h"
namespace Status::StatusGo::Messenger
{
bool startMessenger()
{
QJsonObject payload{
{"jsonrpc", "2.0"},
{"method", "wakuext_startMessenger"},
{"params", QJsonArray()}
};
auto callResult = Utils::callPrivateRpc<QJsonObject>(Utils::jsonToByteArray(payload));
if(callResult.containsError())
qWarning() << "Failed starting Messenger service. Error: " << callResult.error.message;
return !callResult.containsError();
}
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "Types.h"
#include <QtCore>
namespace Status::StatusGo::Messenger
{
bool startMessenger();
}

View File

@ -0,0 +1,138 @@
#include "SignalsManager.h"
#include <QtConcurrent>
#include "libstatus.h"
using namespace std::string_literals;
namespace Status::StatusGo {
std::map<std::string, SignalType> SignalsManager::signalMap;
// TODO: make me thread safe or better refactor into broadcasting mechanism
SignalsManager* SignalsManager::instance()
{
static SignalsManager manager;
return &manager;
}
SignalsManager::SignalsManager()
: QObject(nullptr)
{
SetSignalEventCallback((void*)&SignalsManager::signalCallback);
signalMap = {
{"node.ready"s, SignalType::NodeReady},
{"node.started"s, SignalType::NodeStarted},
{"node.stopped"s, SignalType::NodeStopped},
{"node.login"s, SignalType::NodeLogin},
{"node.crashed"s, SignalType::NodeCrashed},
{"discovery.started"s, SignalType::DiscoveryStarted},
{"discovery.stopped"s, SignalType::DiscoveryStopped},
{"discovery.summary"s, SignalType::DiscoverySummary},
{"mailserver.changed"s, SignalType::MailserverChanged},
{"mailserver.available"s, SignalType::MailserverAvailable},
{"history.request.started"s, SignalType::HistoryRequestStarted},
{"history.request.batch.processed"s, SignalType::HistoryRequestBatchProcessed},
{"history.request.completed"s, SignalType::HistoryRequestCompleted}
};
}
SignalsManager::~SignalsManager()
{
}
void SignalsManager::processSignal(const QString& statusSignal)
{
try
{
QJsonParseError json_error;
const QJsonDocument signalEventDoc(QJsonDocument::fromJson(statusSignal.toUtf8(), &json_error));
if(json_error.error != QJsonParseError::NoError)
{
qWarning() << "Invalid signal received";
return;
}
decode(signalEventDoc.object());
}
catch(const std::exception& e)
{
qWarning() << "Error decoding signal, err: ", e.what();
return;
}
}
void SignalsManager::decode(const QJsonObject& signalEvent)
{
SignalType signalType(Unknown);
auto signalName = signalEvent["type"].toString().toStdString();
if(!signalMap.contains(signalName))
{
qWarning() << "Unknown signal received: " << signalName.c_str();
return;
}
signalType = signalMap[signalName];
auto signalError = signalEvent["event"]["error"].toString();
switch(signalType)
{
// TODO: create extractor functions like in nim
case NodeLogin:
emit nodeLogin(signalError);
break;
case NodeReady:
emit nodeReady(signalError);
break;
case NodeStarted:
emit nodeStarted(signalError);
break;
case NodeStopped:
emit nodeStopped(signalError);
break;
case NodeCrashed:
qWarning() << "node.crashed, error: " << signalError;
emit nodeCrashed(signalError);
break;
case DiscoveryStarted:
emit discoveryStarted(signalError);
break;
case DiscoveryStopped:
emit discoveryStopped(signalError);
break;
case DiscoverySummary:
emit discoverySummary(signalEvent["event"].toArray().count(), signalError);
break;
case MailserverChanged:
emit mailserverChanged(signalError);
break;
case MailserverAvailable:
emit mailserverAvailable(signalError);
break;
case HistoryRequestStarted:
emit historyRequestStarted(signalError);
break;
case HistoryRequestBatchProcessed:
emit historyRequestBatchProcessed(signalError);
break;
case HistoryRequestCompleted:
emit historyRequestCompleted(signalError);
break;
case Unknown: assert(false); break;
}
}
void SignalsManager::signalCallback(const char* data)
{
// TODO: overkill, use some kind of message broker
auto dataStrPtr = std::make_shared<QString>(data);
QFuture<void> future = QtConcurrent::run([dataStrPtr](){
SignalsManager::instance()->processSignal(*dataStrPtr);
});
}
}

View File

@ -0,0 +1,70 @@
#pragma once
#include <QObject>
namespace Status::StatusGo {
enum SignalType
{
Unknown,
NodeLogin,
NodeReady,
NodeStarted,
NodeStopped,
NodeCrashed,
DiscoveryStarted,
DiscoveryStopped,
DiscoverySummary,
MailserverChanged,
MailserverAvailable,
HistoryRequestStarted,
HistoryRequestBatchProcessed,
HistoryRequestCompleted
};
/*!
\todo refactor into a message broker helper to be used by specific service APIs to deliver signals
as part of the specific StatusGoAPI service
\todo address thread safety
*/
class SignalsManager final : public QObject
{
Q_OBJECT
public:
static SignalsManager* instance();
void processSignal(const QString& ev);
signals:
void nodeReady(const QString& error);
void nodeStarted(const QString& error);
void nodeStopped(const QString& error);
void nodeLogin(const QString& error);
void nodeCrashed(const QString& error);
void discoveryStarted(const QString& error);
void discoveryStopped(const QString& error);
void discoverySummary(size_t nodeCount, const QString& error);
void mailserverChanged(const QString& error);
void mailserverAvailable(const QString& error);
void historyRequestStarted(const QString& error);
void historyRequestBatchProcessed(const QString& error);
void historyRequestCompleted(const QString& error);
private:
explicit SignalsManager();
~SignalsManager();
private:
static std::map<std::string, SignalType> signalMap;
static void signalCallback(const char* data);
void decode(const QJsonObject& signalEvent);
};
}

View File

@ -0,0 +1,43 @@
#pragma once
#include <QString>
namespace Status::StatusGo
{
// Used in calls where we don't have version and id returned from `status-go`
struct RpcError
{
// TODO: enum instead for known errors?
static constexpr int NoError = -1;
int code = NoError;
QString message;
static constexpr auto UnknownVersion{""};
static constexpr int UnknownId = 0;
};
template <typename T>
struct RpcResponse
{
T result;
QString jsonRpcVersion;
int id;
RpcError error;
explicit
RpcResponse(T result, QString version = RpcError::UnknownVersion, int id = RpcError::UnknownId,
RpcError error = RpcError())
: result(std::move(result))
, jsonRpcVersion(std::move(version))
, id(id)
, error(std::move(error))
{ }
bool containsError() const {
return !error.message.isEmpty() || error.code != RpcError::NoError;
}
};
}

View File

@ -0,0 +1,48 @@
#include "Utils.h"
#include "libstatus.h"
#include <QtCore>
namespace Status::StatusGo::Utils
{
QJsonArray toJsonArray(const QVector<QString>& value)
{
QJsonArray array;
for(auto& v : value)
array << v;
return array;
}
const char* statusgoCallPrivateRPC(const char* inputJSON) {
// Evil done here! status-go API doesn't follow the proper so we adapt
return CallPrivateRPC(const_cast<char*>(inputJSON));
}
QString hashString(const QString &str)
{
return "0x" + QString::fromUtf8(QCryptographicHash::hash(str.toUtf8(),
QCryptographicHash::Keccak_256).toHex());
}
std::optional<RpcError> getRPCErrorInJson(const QJsonObject& json)
{
auto errVal = json[Param::Error];
if (errVal.isNull() || errVal.isUndefined())
return std::nullopt;
if(errVal.isString() && errVal.toString().length() == 0)
return std::nullopt;
RpcError response;
auto errObj = json[Param::Id].toObject();
if (!errObj[Param::ErrorCode].isNull() && !errObj[Param::ErrorCode].isUndefined())
response.code = errObj[Param::ErrorCode].toInt();
if (!errObj[Param::ErrorMessage].isNull() && !errObj[Param::ErrorMessage].isUndefined())
response.message = errObj[Param::ErrorMessage].toString();
else
response.message = errVal.toString();
return response;
}
}

View File

@ -0,0 +1,116 @@
#pragma once
#include "Types.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
namespace Status::StatusGo::Utils
{
namespace Param {
static constexpr auto Id{"id"};
static constexpr auto JsonRpc{"jsonrpc"};
static constexpr auto Result{"result"};
static constexpr auto Error{"error"};
static constexpr auto ErrorMessage{"message"};
static constexpr auto ErrorCode{"code"};
}
template<class T>
QByteArray jsonToByteArray(const T& json)
{
static_assert(std::is_same_v<T, QJsonObject> ||
std::is_same_v<T, QJsonArray>, "Wrong Json type. Supported: Object, Array");
return QJsonDocument(json).toJson(QJsonDocument::Compact);
}
QJsonArray toJsonArray(const QVector<QString>& value);
/// Check if json contains a standard status-go error and
std::optional<RpcError> getRPCErrorInJson(const QJsonObject& json);
template<class T>
bool checkReceivedResponse(const QString& response, T& json)
{
QJsonParseError error;
auto jsonDocument = QJsonDocument::fromJson(response.toUtf8(), &error);
if (error.error != QJsonParseError::NoError)
return false;
if constexpr (std::is_same_v<T, QJsonObject>)
{
json = jsonDocument.object();
return true;
}
else if constexpr (std::is_same_v<T, QJsonArray>)
{
json = jsonDocument.array();
return true;
}
return false;
}
// TODO: Clarify scope. The assumption done here are valid only for status-go::CallPrivateRPC API.
template<class T>
RpcResponse<T> buildPrivateRPCResponse(const T& json)
{
auto response = RpcResponse<T>(T());
if constexpr (std::is_same_v<T, QJsonObject>)
{
if (!json[Param::Id].isNull() && !json[Param::Id].isUndefined())
response.id = json[Param::Id].toInt();
if (!json[Param::JsonRpc].isNull() && !json[Param::JsonRpc].isUndefined())
response.jsonRpcVersion = json[Param::JsonRpc].toString();
response.error = getRPCErrorInJson(json).value_or(RpcError());
if (!json[Param::Result].isNull() && !json[Param::Result].isUndefined())
response.result = json[Param::Result].toObject();
}
else if constexpr (std::is_same_v<T, QJsonArray>)
{
response.result = json;
}
return response;
}
const char* statusgoCallPrivateRPC(const char* inputJSON);
template<class T>
RpcResponse<T> callPrivateRpc(const QByteArray& payload)
{
try
{
auto result = statusgoCallPrivateRPC(payload.data());
T jsonResult;
if(!Utils::checkReceivedResponse(result, jsonResult))
{
auto msg = QObject::tr("parsing response failed");
throw std::domain_error(msg.toStdString());
}
return Utils::buildPrivateRPCResponse(jsonResult);
}
catch (std::exception& e)
{
auto response = RpcResponse<T>(T());
response.error.message = QObject::tr("an error executing rpc call occurred, msg: %1").arg(e.what());
return response;
}
catch (...)
{
auto response = RpcResponse<T>(T());
response.error.message = QObject::tr("an error executing rpc call");
return response;
}
}
QString hashString(const QString &str);
}

View File

@ -20,6 +20,7 @@ qt6_add_qml_module(${PROJECT_NAME}
OUTPUT_DIRECTORY
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status
)
add_library(Status::StatusQ ALIAS StatusQ)
add_subdirectory(qml/Status/Containers)
add_subdirectory(qml/Status/Controls)
@ -45,4 +46,4 @@ install(
TARGETS
${PROJECT_NAME}
RUNTIME
)
)

View File

@ -1,4 +1,4 @@
import QtQuick 2.0
import QtQuick
/*!
Template for a NavigationBar square button

View File

@ -1,6 +1,6 @@
pragma Singleton
import QtQuick 2.0
import QtQuick
/*!
Helper functions for colors and sizes transformations

View File

@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.5)
project(TestStatusQ LANGUAGES CXX)
enable_testing()
enable_testing(true)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
@ -10,7 +10,7 @@ find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml QuickTest REQUIRED)
qt6_standard_project_setup()
qt6_add_qml_module(${PROJECT_NAME}
URI StatusQ.TestHelpers
URI Status.TestHelpers
VERSION 1.0
# TODO: temporary until we make qt_target_qml_sources work
QML_FILES
@ -29,7 +29,6 @@ add_test(NAME ${PROJECT_NAME} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMM
add_custom_target("Run_${PROJECT_NAME}" COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_CURRENT_BINARY_DIR}")
add_dependencies("Run_${PROJECT_NAME}" ${PROJECT_NAME})
# TODO: move this to a test helpers library
target_include_directories(${PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}

View File

@ -14,9 +14,10 @@ find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
qt6_standard_project_setup()
qt6_add_qml_module(${PROJECT_NAME}
URI Status.TestHelpers
URI Status.${PROJECT_NAME}
VERSION 1.0
)
add_library(Status::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_include_directories(${PROJECT_NAME}
PUBLIC
@ -43,5 +44,3 @@ target_link_libraries(${PROJECT_NAME}
PRIVATE
GTest::gtest_main
)
add_library(Status::TestHelpers ALIAS TestHelpers)

View File

@ -12,25 +12,25 @@ fs::path createTestFolder(const std::string& testName)
auto tm = *std::localtime(&t);
std::ostringstream timeOss;
timeOss << std::put_time(&tm, "%d-%m-%Y_%H-%M-%S");
auto tmpPath = fs::path(testing::TempDir())/(testName + "-" + timeOss.str());
fs::create_directories(tmpPath);
return tmpPath;
return fs::path(testing::TempDir())/"StatusTests"/(testName + "-" + timeOss.str());
}
AutoCleanTempTestDir::AutoCleanTempTestDir(const std::string &testName)
AutoCleanTempTestDir::AutoCleanTempTestDir(const std::string &testName, bool createDir)
: m_testFolder(createTestFolder(testName))
{
if(createDir)
fs::create_directories(m_testFolder);
}
AutoCleanTempTestDir::~AutoCleanTempTestDir()
{
fs::remove_all(m_testFolder);
// TODO: Consider making this concurrent safe and cleanup the root folder as well if empty
fs::remove_all(m_testFolder);
}
const std::filesystem::path& AutoCleanTempTestDir::testFolder()
const std::filesystem::path& AutoCleanTempTestDir::tempFolder()
{
return m_testFolder;
}
}

View File

@ -10,10 +10,10 @@ class AutoCleanTempTestDir {
public:
/// Creates a temporary folder to be used in tests. The folder content's will
/// be removed when out of scope
explicit AutoCleanTempTestDir(const std::string& testName);
explicit AutoCleanTempTestDir(const std::string& testName, bool createDir = true);
~AutoCleanTempTestDir();
const std::filesystem::path& testFolder();
const std::filesystem::path& tempFolder();
private:
const std::filesystem::path m_testFolder;

View File

@ -22,6 +22,8 @@ namespace Status::Testing {
class MonitorQtOutput : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
public:
MonitorQtOutput();
~MonitorQtOutput();

31
resources/CMakeLists.txt Normal file
View File

@ -0,0 +1,31 @@
# Temporary library not to duplicate resources
# TODO: refactor it when switching to C++ code
#
cmake_minimum_required(VERSION 3.21)
project(StatusGoConfig
VERSION 0.1.0
LANGUAGES CXX)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Qml REQUIRED)
qt6_standard_project_setup()
# Resource path ":/Status/StaticConfig/<file>"
qt6_add_qml_module(${PROJECT_NAME}
URI Status.StaticConfig
VERSION 1.0
RESOURCES
default-networks.json
fleets.json
infura_key
node-config.json
RESOURCE_PREFIX ""
)
add_library(Status::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_link_libraries(${PROJECT_NAME}
PRIVATE
Qt6::Qml
)

View File

@ -1,27 +0,0 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
project(Status DESCRIPTION "Status project" LANGUAGES CXX)
include("${CMAKE_SOURCE_DIR}/cmake/project-config.cmake")
include("${CMAKE_SOURCE_DIR}/cmake/conan.cmake")
find_package(Qt5 REQUIRED COMPONENTS
Core
Gui
Quick
Widgets
Qml
Quick
QuickControls2
QuickTemplates2
Multimedia
Concurrent
LinguistTools)
# The following should be moved to conan.
# But so far we're just adding libs from the vendor folder this way.
# statusgo lib is registered globaly - /vendor/status-go/CMakeLists.txt
SET(STATUS_GO_LIB statusgo_shared)
add_subdirectory(${CMAKE_SOURCE_DIR}/../vendor/status-go bin/status-go)
add_subdirectory(projects)

View File

@ -1,28 +0,0 @@
{
"configurations": [
{
"name": "Release",
"generator": "Ninja",
"configurationType": "RelWithDebInfo",
"buildRoot": "${projectDir}\\build\\${name}",
"installRoot": "${projectDir}\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "msvc_x64_x64" ],
"variables": []
},
{
"name": "Debug",
"generator": "Ninja",
"configurationType": "Debug",
"inheritEnvironments": [ "msvc_x64_x64" ],
"buildRoot": "${projectDir}\\build\\${name}",
"installRoot": "${projectDir}\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"variables": []
}
]
}

View File

@ -1,25 +0,0 @@
add_executable(${PROJECT_NAME})
target_link_libraries(
${PROJECT_NAME} PRIVATE
Qt5::Core
Qt5::Gui
Qt5::Widgets
Qt5::Quick
Qt5::Qml
Qt5::Quick
Qt5::QuickControls2
Qt5::QuickTemplates2
Qt5::Multimedia
Qt5::Concurrent
Status.Services
${STATUS_GO_LIB}
)
file(GLOB_RECURSE SOURCES
"*.h"
"*.cpp"
${STATUS_RCC}
${STATUS_RESOURCES_QRC}
${STATUS_QRC}
)

View File

@ -1,31 +0,0 @@
add_executable(${PROJECT_NAME} MACOSX_BUNDLE)
find_library(FOUNDATION_FRAMEWORK Foundation)
find_library(IO_KIT_FRAMEWORK IOKit)
target_link_libraries(
${PROJECT_NAME} PRIVATE
Qt5::Core
Qt5::Gui
Qt5::Widgets
Qt5::Quick
Qt5::Qml
Qt5::Quick
Qt5::QuickControls2
Qt5::QuickTemplates2
Qt5::Multimedia
Qt5::Concurrent
${FOUNDATION_FRAMEWORK}
${IO_KIT_FRAMEWORK}
Status.Services
${STATUS_GO_LIB}
)
file(GLOB_RECURSE SOURCES
"*.h"
"*.cpp"
"*.mm"
${STATUS_RCC}
${STATUS_RESOURCES_QRC}
${STATUS_QRC}
)

View File

@ -1,25 +0,0 @@
add_executable(${PROJECT_NAME} WIN32)
target_link_libraries(
${PROJECT_NAME} PRIVATE
Qt5::Core
Qt5::Gui
Qt5::Widgets
Qt5::Quick
Qt5::Qml
Qt5::Quick
Qt5::QuickControls2
Qt5::QuickTemplates2
Qt5::Multimedia
Qt5::Concurrent
Status.Services
${STATUS_GO_LIB}
)
file(GLOB_RECURSE SOURCES
"*.h"
"*.cpp"
${STATUS_RCC}
${STATUS_RESOURCES_QRC}
${STATUS_QRC}
)

View File

@ -1,27 +0,0 @@
set(CONAN_WORKING_DIR ${CMAKE_BINARY_DIR}/conan)
if (EXISTS ${CONAN_WORKING_DIR})
file(REMOVE_RECURSE ${CONAN_WORKING_DIR})
endif ()
file(MAKE_DIRECTORY ${CONAN_WORKING_DIR})
if (${CMAKE_BUILD_TYPE} STREQUAL Debug)
set(CONAN_PROFILE ${PROJECT_SOURCE_DIR}/conan-debug-profile)
else ()
set(CONAN_PROFILE ${PROJECT_SOURCE_DIR}/conan-release-profile)
endif ()
execute_process(
COMMAND conan install ${PROJECT_SOURCE_DIR} -pr=${CONAN_PROFILE}
WORKING_DIRECTORY ${CONAN_WORKING_DIR}
RESULT_VARIABLE CONAN_RESULT
)
if (NOT ${CONAN_RESULT} EQUAL 0)
message(FATAL_ERROR "Conan failed: ${CONAN_RESULT}.")
endif ()
include(${CONAN_WORKING_DIR}/conanbuildinfo.cmake)
conan_basic_setup(KEEP_RPATHS)

View File

@ -1,19 +0,0 @@
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
#set(CMAKE_AUTORCC ON) This is disabled because of `/ui/generate-rcc.go` script usage for generating .qrc and cmake command for .rcc
# Set this to TRUE if you want to create .ts files and translations.qrc
set(UPDATE_TRANSLATIONS FALSE)
if ($ENV{QTDIR} LESS_EQUAL "")
message(FATAL_ERROR "Please set the path to your Qt dir as `QTDIR` variable in your ENV. Example: QTDIR=/Qt/Qt5.14.2/5.14.2/clang_64")
endif()
message("Located QtDir: " $ENV{QTDIR})
set(CMAKE_PREFIX_PATH $ENV{QTDIR})
add_definitions(-DSTATUS_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
add_definitions(-DSTATUS_DEVELOPMENT=true)

View File

@ -1,10 +0,0 @@
target_link_libraries(
${PROJECT_NAME} PRIVATE
Qt5::Core
Status.Backend
)
file(GLOB_RECURSE SOURCES
"*.h"
"*.cpp"
)

Some files were not shown because too many files have changed in this diff Show More