diff --git a/src/app/login/core.nim b/src/app/login/core.nim index d740a4942c..56d0acefb2 100644 --- a/src/app/login/core.nim +++ b/src/app/login/core.nim @@ -3,6 +3,7 @@ import ../../status/libstatus/types as status_types import ../../signals/types import ../../status/status import view +import ../../status/accounts as status_accounts type LoginController* = ref object of SignalSubscriber status*: Status @@ -19,11 +20,19 @@ proc delete*(self: LoginController) = delete self.view delete self.variant -proc init*(self: LoginController, nodeAccounts: seq[NodeAccount]) = +proc init*(self: LoginController) = + let nodeAccounts = self.status.accounts.openAccounts() self.status.accounts.nodeAccounts = nodeAccounts for nodeAccount in nodeAccounts: self.view.addAccountToList(nodeAccount) +proc reset*(self: LoginController) = + self.view.removeAccounts() + +proc handleNodeStopped(self: LoginController, data: Signal) = + self.status.events.emit("nodeStopped", Args()) + self.view.onLoggedOut() + proc handleNodeLogin(self: LoginController, data: Signal) = let response = NodeSignal(data) if self.view.currentAccount.account != nil: @@ -31,8 +40,13 @@ proc handleNodeLogin(self: LoginController, data: Signal) = if ?.response.event.error == "": self.status.events.emit("login", AccountArgs(account: self.view.currentAccount.account.toAccount)) +proc handleNodeReady(self: LoginController, data: Signal) = + self.status.events.emit("nodeReady", Args()) + method onSignal(self: LoginController, data: Signal) = - if data.signalType == SignalType.NodeLogin: - self.handleNodeLogin(data) + case data.signalType: + of SignalType.NodeLogin: handleNodeLogin(self, data) + of SignalType.NodeStopped: handleNodeStopped(self, data) + of SignalType.NodeReady: handleNodeReady(self, data) else: discard diff --git a/src/app/login/view.nim b/src/app/login/view.nim index e1fca179ac..f397cce96c 100644 --- a/src/app/login/view.nim +++ b/src/app/login/view.nim @@ -43,6 +43,11 @@ QtObject: self.accounts.add(account) self.endInsertRows() + proc removeAccounts*(self: LoginView) = + self.beginRemoveRows(newQModelIndex(), self.accounts.len, self.accounts.len) + self.accounts = @[] + self.endRemoveRows() + method rowCount(self: LoginView, index: QModelIndex = nil): int = return self.accounts.len @@ -86,3 +91,5 @@ QtObject: proc setLastLoginResponse*(self: LoginView, loginResponse: StatusGoError) = self.loginResponseChanged(loginResponse.error) + + proc onLoggedOut*(self: LoginView) {.signal.} \ No newline at end of file diff --git a/src/app/onboarding/core.nim b/src/app/onboarding/core.nim index 7a8a9538b4..0e24b13d47 100644 --- a/src/app/onboarding/core.nim +++ b/src/app/onboarding/core.nim @@ -29,6 +29,9 @@ proc init*(self: OnboardingController) = for account in accounts: self.view.addAccountToList(account) +proc reset*(self: OnboardingController) = + self.view.removeAccounts() + proc handleNodeLogin(self: OnboardingController, data: Signal) = let response = NodeSignal(data) if self.view.currentAccount.account != nil: @@ -37,7 +40,7 @@ proc handleNodeLogin(self: OnboardingController, data: Signal) = self.status.events.emit("login", AccountArgs(account: self.view.currentAccount.account.toAccount)) method onSignal(self: OnboardingController, data: Signal) = - if data.signalType == SignalType.NodeLogin: - self.handleNodeLogin(data) + case data.signalType: + of SignalType.NodeLogin: handleNodeLogin(self, data) else: discard diff --git a/src/app/onboarding/view.nim b/src/app/onboarding/view.nim index 6492da0269..40a063dd43 100644 --- a/src/app/onboarding/view.nim +++ b/src/app/onboarding/view.nim @@ -40,6 +40,11 @@ QtObject: self.beginInsertRows(newQModelIndex(), self.accounts.len, self.accounts.len) self.accounts.add(account) self.endInsertRows() + + proc removeAccounts*(self: OnboardingView) = + self.beginResetModel() + self.accounts = @[] + self.endResetModel() method rowCount(self: OnboardingView, index: QModelIndex = nil): int = return self.accounts.len diff --git a/src/app/profile/core.nim b/src/app/profile/core.nim index 580854a97d..699960440d 100644 --- a/src/app/profile/core.nim +++ b/src/app/profile/core.nim @@ -15,7 +15,7 @@ type ProfileController* = object proc newController*(status: Status): ProfileController = result = ProfileController() result.status = status - result.view = newProfileView() + result.view = newProfileView(status) result.variant = newQVariant(result.view) proc delete*(self: ProfileController) = diff --git a/src/app/profile/view.nim b/src/app/profile/view.nim index e3a9be2096..39c0462dd7 100644 --- a/src/app/profile/view.nim +++ b/src/app/profile/view.nim @@ -3,12 +3,15 @@ import views/mailservers_list import views/contact_list import views/profile_info import ../../status/profile +import ../../status/accounts as status_accounts +import ../../status/status QtObject: type ProfileView* = ref object of QObject profile*: ProfileInfoView mailserversList*: MailServersList contactList*: ContactList + status*: Status proc setup(self: ProfileView) = self.QObject.setup @@ -16,12 +19,13 @@ QtObject: proc delete*(self: ProfileView) = self.QObject.delete - proc newProfileView*(): ProfileView = + proc newProfileView*(status: Status): ProfileView = new(result, delete) result = ProfileView() result.profile = newProfileInfoView() result.mailserversList = newMailServersList() result.contactList = newContactList() + result.status = status result.setup proc addMailServerToList*(self: ProfileView, mailserver: MailServer) = @@ -50,3 +54,6 @@ QtObject: QtProperty[QVariant] profile: read = getProfile + + proc logout*(self: ProfileView) {.slot.} = + self.status.profile.logout() diff --git a/src/nim_status_client.nim b/src/nim_status_client.nim index ba77d8dc79..488f00bdba 100644 --- a/src/nim_status_client.nim +++ b/src/nim_status_client.nim @@ -21,7 +21,7 @@ logScope: proc mainProc() = let status = statuslib.newStatusInstance() - let nodeAccounts = status.initNodeAccounts() + status.initNode() let app = newQApplication() let engine = newQQmlApplicationEngine() @@ -43,13 +43,12 @@ proc mainProc() = engine.setRootContextProperty("chatsModel", chat.variant) var node = node.newController(status) - node.init() engine.setRootContextProperty("nodeModel", node.variant) var profile = profile.newController(status) engine.setRootContextProperty("profileModel", profile.variant) - status.events.once("login") do(a: Args): + status.events.on("login") do(a: Args): var args = AccountArgs(a) status.startMessenger() chat.init() @@ -59,16 +58,37 @@ proc mainProc() = var login = login.newController(status) var onboarding = onboarding.newController(status) - # TODO: replace this with routing - let showLogin = nodeAccounts.len > 0 - engine.setRootContextProperty("showLogin", newQVariant(showLogin)) - - login.init(nodeAccounts) engine.setRootContextProperty("loginModel", login.variant) - onboarding.init() engine.setRootContextProperty("onboardingModel", onboarding.variant) + # Initialize only controllers whose init functions + # do not need a running node + proc initControllers() = + node.init() + login.init() + onboarding.init() + + initControllers() + + # Handle node.stopped signal when user has logged out + status.events.on("nodeStopped") do(a: Args): + # TODO: remove this once accounts are not tracked in the AccountsModel + status.reset() + + # 1. Reset controller data + login.reset() + onboarding.reset() + # TODO: implement all controller resets + # chat.reset() + # node.reset() + # wallet.reset() + # profile.reset() + + # 2. Re-init controllers that don't require a running node + initControllers() + + signalController.init() signalController.addSubscriber(SignalType.Wallet, wallet) signalController.addSubscriber(SignalType.Wallet, node) @@ -76,6 +96,9 @@ proc mainProc() = signalController.addSubscriber(SignalType.DiscoverySummary, chat) signalController.addSubscriber(SignalType.NodeLogin, login) signalController.addSubscriber(SignalType.NodeLogin, onboarding) + signalController.addSubscriber(SignalType.NodeStopped, login) + signalController.addSubscriber(SignalType.NodeStarted, login) + signalController.addSubscriber(SignalType.NodeReady, login) engine.setRootContextProperty("signals", signalController.variant) diff --git a/src/status/accounts.nim b/src/status/accounts.nim index 11bca8bbac..63652b433a 100644 --- a/src/status/accounts.nim +++ b/src/status/accounts.nim @@ -1,14 +1,17 @@ import libstatus/accounts as status_accounts import libstatus/types import options +import eventemitter type AccountModel* = ref object generatedAddresses*: seq[GeneratedAccount] nodeAccounts*: seq[NodeAccount] + events: EventEmitter -proc newAccountModel*(): AccountModel = +proc newAccountModel*(events: EventEmitter): AccountModel = result = AccountModel() + result.events = events proc generateAddresses*(self: AccountModel): seq[GeneratedAccount] = var accounts = status_accounts.generateAddresses() @@ -16,7 +19,10 @@ proc generateAddresses*(self: AccountModel): seq[GeneratedAccount] = account.name = status_accounts.generateAlias(account.derived.whisper.publicKey) account.photoPath = status_accounts.generateIdenticon(account.derived.whisper.publicKey) self.generatedAddresses.add(account) - self.generatedAddresses + result = self.generatedAddresses + +proc openAccounts*(self: AccountModel): seq[NodeAccount] = + result = status_accounts.openAccounts() proc login*(self: AccountModel, selectedAccountIndex: int, password: string): NodeAccount = let currentNodeAccount = self.nodeAccounts[selectedAccountIndex] @@ -35,3 +41,7 @@ proc importMnemonic*(self: AccountModel, mnemonic: string): GeneratedAccount = importedAccount.name = status_accounts.generateAlias(importedAccount.derived.whisper.publicKey) importedAccount.photoPath = status_accounts.generateIdenticon(importedAccount.derived.whisper.publicKey) result = importedAccount + +proc reset*(self: AccountModel) = + self.nodeAccounts = @[] + self.generatedAddresses = @[] diff --git a/src/status/libstatus/accounts.nim b/src/status/libstatus/accounts.nim index f1be3034b6..29184cf180 100644 --- a/src/status/libstatus/accounts.nim +++ b/src/status/libstatus/accounts.nim @@ -36,17 +36,15 @@ proc ensureDir(dirname: string) = # removeDir(dirname) createDir(dirname) -proc initNodeAccounts*(): seq[NodeAccount] = - const datadir = "./data/" - const keystoredir = "./data/keystore/" - const nobackupdir = "./noBackup/" +proc initNode*() = + ensureDir(DATADIR) + ensureDir(KEYSTOREDIR) + ensureDir(NOBACKUPDIR) - ensureDir(datadir) - ensureDir(keystoredir) - ensureDir(nobackupdir) + discard $libstatus.initKeystore(KEYSTOREDIR) - discard $libstatus.initKeystore(keystoredir); - let strNodeAccounts = $libstatus.openAccounts(datadir); +proc openAccounts*(): seq[NodeAccount] = + let strNodeAccounts = $libstatus.openAccounts(DATADIR) result = Json.decode(strNodeAccounts, seq[NodeAccount]) proc saveAccountAndLogin*( @@ -215,6 +213,8 @@ proc deriveAccounts*(accountId: string): MultiAccounts = "accountID": accountId, "paths": [PATH_WALLET_ROOT, PATH_EIP_1581, PATH_WHISPER, PATH_DEFAULT_WALLET] } - # libstatus.multiAccountImportMnemonic never results in an error given ANY input let deriveResult = $libstatus.multiAccountDeriveAddresses($deriveJson) result = Json.decode(deriveResult, MultiAccounts) + +proc logout*(): StatusGoError = + result = Json.decode($libstatus.logout(), StatusGoError) diff --git a/src/status/libstatus/accounts/constants.nim b/src/status/libstatus/accounts/constants.nim index e704ec195b..a0db92e80d 100644 --- a/src/status/libstatus/accounts/constants.nim +++ b/src/status/libstatus/accounts/constants.nim @@ -174,4 +174,8 @@ let NODE_CONFIG* = %* { "WalletConfig": { "Enabled": true } -} \ No newline at end of file +} + +const DATADIR* = "./data/" +const KEYSTOREDIR* = "./data/keystore/" +const NOBACKUPDIR* = "./noBackup/" \ No newline at end of file diff --git a/src/status/libstatus/libstatus.nim b/src/status/libstatus/libstatus.nim index 895240992f..6a9895fcec 100644 --- a/src/status/libstatus/libstatus.nim +++ b/src/status/libstatus/libstatus.nim @@ -33,3 +33,5 @@ proc generateAlias*(p0: GoString): cstring {.importc: "GenerateAlias".} proc identicon*(p0: GoString): cstring {.importc: "Identicon".} proc login*(acctData: cstring, password: cstring): cstring {.importc: "Login".} + +proc logout*(): cstring {.importc: "Logout".} diff --git a/src/status/libstatus/types.nim b/src/status/libstatus/types.nim index 2c57e34d6f..bbf92aa960 100644 --- a/src/status/libstatus/types.nim +++ b/src/status/libstatus/types.nim @@ -9,6 +9,7 @@ type SignalType* {.pure.} = enum Wallet = "wallet" NodeReady = "node.ready" NodeStarted = "node.started" + NodeStopped = "node.stopped" NodeLogin = "node.login" EnvelopeSent = "envelope.sent" EnvelopeExpired = "envelope.expired" diff --git a/src/status/profile.nim b/src/status/profile.nim index 07dbaf4eb2..e7ec747c0e 100644 --- a/src/status/profile.nim +++ b/src/status/profile.nim @@ -2,6 +2,7 @@ import json import eventemitter import libstatus/types import libstatus/core as libstatus_core +import libstatus/accounts as status_accounts type MailServer* = ref object @@ -45,3 +46,13 @@ proc getContactByID*(id: string): Profile = let val = parseJSON($response) result = toProfileModel(val) +type + ProfileModel* = ref object + +proc newProfileModel*(): ProfileModel = + result = ProfileModel() + +proc logout*(self: ProfileModel) = + discard status_accounts.logout() + + diff --git a/src/status/status.nim b/src/status/status.nim index 8172a2c210..dbd92b14ed 100644 --- a/src/status/status.nim +++ b/src/status/status.nim @@ -9,6 +9,8 @@ import accounts as accounts import wallet as wallet import node as node import mailservers as mailservers +import profile +import ../signals/types as signal_types type Status* = ref object events*: EventEmitter @@ -17,19 +19,33 @@ type Status* = ref object accounts*: AccountModel wallet*: WalletModel node*: NodeModel + profile*: ProfileModel proc newStatusInstance*(): Status = result = Status() result.events = createEventEmitter() result.chat = chat.newChatModel(result.events) - result.accounts = accounts.newAccountModel() + result.accounts = accounts.newAccountModel(result.events) result.wallet = wallet.newWalletModel(result.events) result.wallet.initEvents() result.node = node.newNodeModel() result.mailservers = mailservers.newMailserverModel(result.events) + result.profile = profile.newProfileModel() -proc initNodeAccounts*(self: Status): seq[NodeAccount] = - libstatus_accounts.initNodeAccounts() +proc initNode*(self: Status) = + libstatus_accounts.initNode() proc startMessenger*(self: Status) = libstatus_core.startMessenger() + +proc reset*(self: Status) = + # TODO: remove this once accounts are not tracked in the AccountsModel + self.accounts.reset() + + # NOT NEEDED self.chat.reset() + # NOT NEEDED self.wallet.reset() + # NOT NEEDED self.node.reset() + # NOT NEEDED self.mailservers.reset() + # NOT NEEDED self.profile.reset() + + # TODO: add all resets here diff --git a/ui/app/AppLayouts/Profile/Sections/SignoutContainer.qml b/ui/app/AppLayouts/Profile/Sections/SignoutContainer.qml index 799c7cd9e3..afb4bc5c4f 100644 --- a/ui/app/AppLayouts/Profile/Sections/SignoutContainer.qml +++ b/ui/app/AppLayouts/Profile/Sections/SignoutContainer.qml @@ -2,6 +2,7 @@ import QtQuick 2.3 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.3 import "../../../../imports" +import "../../../../shared" Item { id: signoutContainer @@ -11,7 +12,7 @@ Item { Layout.fillWidth: true Text { - id: element10 + id: txtTitle text: qsTr("Sign out controls") anchors.left: parent.left anchors.leftMargin: 24 @@ -20,4 +21,15 @@ Item { font.weight: Font.Bold font.pixelSize: 20 } + + StyledButton { + id: btnLogout + anchors.top: txtTitle.bottom + anchors.topMargin: Theme.padding + label: qsTr("Logout") + + onClicked: { + profileModel.logout(); + } + } } \ No newline at end of file diff --git a/ui/main.qml b/ui/main.qml index 11d0fb1e5f..7a6f21fee3 100644 --- a/ui/main.qml +++ b/ui/main.qml @@ -2,6 +2,7 @@ import QtQuick 2.3 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import Qt.labs.platform 1.1 +import QtQml.StateMachine 1.14 as DSM import "./onboarding" import "./app" @@ -13,6 +14,8 @@ ApplicationWindow { visible: true font.family: "Inter" + signal navigateTo(string path) + SystemTrayIcon { visible: true icon.source: "shared/img/status-logo.png" @@ -30,18 +33,145 @@ ApplicationWindow { } } - OnboardingMain { - id: onboarding - visible: !app.visible + DSM.StateMachine { + id: stateMachine + initialState: onboardingState + running: true + + DSM.State { + id: onboardingState + initialState: loginModel.rowCount() ? stateLogin : stateIntro + + DSM.State { + id: stateIntro + onEntered: loader.sourceComponent = intro + + DSM.SignalTransition { + targetState: keysMainState + signal: applicationWindow.navigateTo + guard: path === "KeysMain" + } + } + + DSM.State { + id: keysMainState + onEntered: loader.sourceComponent = keysMain + + DSM.SignalTransition { + targetState: existingKeyState + signal: applicationWindow.navigateTo + guard: path === "ExistingKey" + } + + DSM.SignalTransition { + targetState: genKeyState + signal: applicationWindow.navigateTo + guard: path === "GenKey" + } + } + + DSM.State { + id: existingKeyState + onEntered: loader.sourceComponent = existingKey + + DSM.SignalTransition { + targetState: appState + signal: onboardingModel.loginResponseChanged + guard: !error + } + } + + DSM.State { + id: genKeyState + onEntered: loader.sourceComponent = genKey + + DSM.SignalTransition { + targetState: appState + signal: onboardingModel.loginResponseChanged + guard: !error + } + + DSM.SignalTransition { + targetState: existingKeyState + signal: applicationWindow.navigateTo + guard: path === "ExistingKey" + } + } + + DSM.State { + id: stateLogin + onEntered: loader.sourceComponent = login + + DSM.SignalTransition { + targetState: appState + signal: loginModel.loginResponseChanged + guard: !error + } + + DSM.SignalTransition { + targetState: genKeyState + signal: applicationWindow.navigateTo + guard: path === "GenKey" + } + } + + DSM.FinalState { + id: onboardingDoneState + } + } + + DSM.State { + id: appState + onEntered: loader.sourceComponent = app + + DSM.SignalTransition { + targetState: stateLogin + signal: loginModel.onLoggedOut + } + } + } + + Loader { + id: loader anchors.fill: parent } - AppMain { + Component { id: app - // TODO: Set this to a logic result determining when we need to show the onboarding screens - // Set to true to hide the onboarding screens manually - // Set to false to show the onboarding screens manually - visible: false // logic.accountResult !== "" - anchors.fill: parent + AppMain {} + } + + Component { + id: intro + Intro { + btnGetStarted.onClicked: applicationWindow.navigateTo("KeysMain") + } + } + + Component { + id: keysMain + KeysMain { + btnGenKey.onClicked: applicationWindow.navigateTo("GenKey") + btnExistingKey.onClicked: applicationWindow.navigateTo("ExistingKey") + } + } + + Component { + id: existingKey + ExistingKey {} + } + + Component { + id: genKey + GenKey { + btnExistingKey.onClicked: applicationWindow.navigateTo("ExistingKey") + } + } + + Component { + id: login + Login { + btnGenKey.onClicked: applicationWindow.navigateTo("GenKey") + } } } diff --git a/ui/onboarding/ExistingKey.qml b/ui/onboarding/ExistingKey.qml index 39be34cf14..c5b2a82b23 100644 --- a/ui/onboarding/ExistingKey.qml +++ b/ui/onboarding/ExistingKey.qml @@ -192,6 +192,19 @@ SwipeView { standardButtons: StandardButton.Ok } + MessageDialog { + id: passwordsDontMatchError + title: "Error" + text: "Passwords don't match" + icon: StandardIcon.Warning + standardButtons: StandardButton.Ok + onAccepted: { + txtConfirmPassword.clear(); + swipeView.currentIndex = 1; + txtPassword.focus = true; + } + } + Connections { target: onboardingModel ignoreUnknownSignals: true diff --git a/ui/onboarding/OnboardingMain.qml b/ui/onboarding/OnboardingMain.qml deleted file mode 100644 index 508ee1ad22..0000000000 --- a/ui/onboarding/OnboardingMain.qml +++ /dev/null @@ -1,130 +0,0 @@ -import QtQuick 2.3 -import QtQml.StateMachine 1.14 as DSM -import QtQuick.Controls 2.3 - -Page { - id: onboardingMain - property string state - anchors.fill: parent - - DSM.StateMachine { - id: stateMachine - initialState: showLogin ? stateLogin : stateIntro - running: onboardingMain.visible - - DSM.State { - id: stateIntro - onEntered: intro.visible = true - onExited: intro.visible = false - - DSM.SignalTransition { - targetState: keysMainState - signal: intro.btnGetStarted.clicked - } - } - - DSM.State { - id: keysMainState - onEntered: keysMain.visible = true - onExited: keysMain.visible = false - - DSM.SignalTransition { - targetState: existingKeyState - signal: keysMain.btnExistingKey.clicked - } - - DSM.SignalTransition { - targetState: genKeyState - signal: keysMain.btnGenKey.clicked - } - } - - DSM.State { - id: existingKeyState - onEntered: existingKey.visible = true - onExited: existingKey.visible = false - - DSM.SignalTransition { - targetState: appState - signal: onboardingModel.loginResponseChanged - guard: !error - } - } - - DSM.State { - id: genKeyState - onEntered: genKey.visible = true - onExited: genKey.visible = false - - DSM.SignalTransition { - targetState: appState - signal: onboardingModel.loginResponseChanged - guard: !error - } - - DSM.SignalTransition { - targetState: existingKeyState - signal: genKey.btnExistingKey.clicked - } - } - - DSM.State { - id: stateLogin - onEntered: login.visible = true - onExited: login.visible = false - - DSM.SignalTransition { - targetState: appState - signal: loginModel.loginResponseChanged - guard: !error - } - - DSM.SignalTransition { - targetState: genKeyState - signal: login.btnGenKey.clicked - } - } - - DSM.FinalState { - id: appState - onEntered: app.visible = true - onExited: app.visible = false - } - } - - Intro { - id: intro - anchors.fill: parent - visible: false - } - - KeysMain { - id: keysMain - anchors.fill: parent - visible: false - } - - ExistingKey { - id: existingKey - anchors.fill: parent - visible: false - } - - GenKey { - id: genKey - anchors.fill: parent - visible: false - } - - Login { - id: login - anchors.fill: parent - visible: false - } -} - -/*##^## -Designer { - D{i:0;autoSize:true;height:770;width:1232} -} -##^##*/