feat: add login functionality

Add login functionality. If node accounts exist in status-go (keystores in ./data dir), then show the Login screen. Otherwise, show the Onboarding screen (generate keys screen).

Update nim-stew to latest version.

Change references to Address to the common type Account to prevent repeating of types.

Distinguish between unknown and unhandled signals.

Pass signals through to subscribers regardless if the signal type is known (in case the SignalSubscriber handles it with string comparison or other).

Update serialization as much as possible

Latest nim-stew updates allow type inheritance during de/serialization
This commit is contained in:
emizzle 2020-05-27 17:15:42 +10:00 committed by Iuri Matias
parent dd9b4c50ea
commit 0f59529c57
17 changed files with 545 additions and 153 deletions

43
src/app/login/core.nim Normal file
View File

@ -0,0 +1,43 @@
import NimQml
import ../../status/types as status_types
import ../../signals/types
import eventemitter
import view
import ../../models/accounts as AccountModel
import chronicles
import options
import std/wrapnils
type LoginController* = ref object of SignalSubscriber
view*: LoginView
variant*: QVariant
appEvents*: EventEmitter
model: AccountModel
proc newController*(appEvents: EventEmitter): LoginController =
result = LoginController()
result.appEvents = appEvents
result.model = newAccountModel()
result.view = newLoginView(result.model)
result.variant = newQVariant(result.view)
proc delete*(self: LoginController) =
delete self.view
delete self.variant
proc init*(self: LoginController, nodeAccounts: seq[NodeAccount]) =
self.model.nodeAccounts = nodeAccounts
for nodeAccount in nodeAccounts:
self.view.addAccountToList(nodeAccount)
proc handleNodeLogin(self: LoginController, data: Signal) =
var response = NodeSignal(data)
self.view.setLastLoginResponse($response.event.toJson)
if ?.response.event.error == "" and self.model.currentAccount != nil:
self.appEvents.emit("login", AccountArgs(account: self.model.currentAccount))
method onSignal(self: LoginController, data: Signal) =
if data.signalType == SignalType.NodeLogin:
self.handleNodeLogin(data)
else:
discard

86
src/app/login/view.nim Normal file
View File

@ -0,0 +1,86 @@
import NimQml
import Tables
import json
import nimcrypto
import ../../signals/types
import ../../status/types as status_types
import ../../status/accounts as status_accounts
import strformat
import json_serialization
import core
import ../../models/accounts as AccountModel
type
AccountRoles {.pure.} = enum
Username = UserRole + 1,
Identicon = UserRole + 2,
Key = UserRole + 3
QtObject:
type LoginView* = ref object of QAbstractListModel
accounts: seq[NodeAccount]
lastLoginResponse: string
model*: AccountModel
proc setup(self: LoginView) =
self.QAbstractListModel.setup
proc delete*(self: LoginView) =
self.QAbstractListModel.delete
self.accounts = @[]
proc newLoginView*(model: AccountModel): LoginView =
new(result, delete)
result.accounts = @[]
result.lastLoginResponse = ""
result.model = model
result.setup
proc addAccountToList*(self: LoginView, account: NodeAccount) =
self.beginInsertRows(newQModelIndex(), self.accounts.len, self.accounts.len)
self.accounts.add(account)
self.endInsertRows()
method rowCount(self: LoginView, index: QModelIndex = nil): int =
return self.accounts.len
method data(self: LoginView, index: QModelIndex, role: int): QVariant =
if not index.isValid:
return
if index.row < 0 or index.row >= self.accounts.len:
return
let asset = self.accounts[index.row]
let assetRole = role.AccountRoles
case assetRole:
of AccountRoles.Username: result = newQVariant(asset.name)
of AccountRoles.Identicon: result = newQVariant(asset.photoPath)
of AccountRoles.Key: result = newQVariant(asset.keyUid)
method roleNames(self: LoginView): Table[int, string] =
{ AccountRoles.Username.int:"username",
AccountRoles.Identicon.int:"identicon",
AccountRoles.Key.int:"key" }.toTable
proc login(self: LoginView, selectedAccountIndex: int, password: string): string {.slot.} =
try:
result = self.model.login(selectedAccountIndex, password).toJson
except:
let
e = getCurrentException()
msg = getCurrentExceptionMsg()
result = SignalError(error: msg).toJson
proc lastLoginResponse*(self: LoginView): string =
result = self.lastLoginResponse
proc loginResponseChanged*(self: LoginView, response: string) {.signal.}
proc setLastLoginResponse*(self: LoginView, loginResponse: string) {.slot.} =
self.lastLoginResponse = loginResponse
self.loginResponseChanged(loginResponse)
QtProperty[string] loginResponse:
read = lastLoginResponse
write = setLastLoginResponse
notify = loginResponseChanged

View File

@ -1,14 +1,18 @@
import NimQml
import ../../models/accounts
import ../../signals/types
import ../../status/types as status_types
import ../../status/accounts as status_accounts
import ../../models/accounts as AccountModel
import eventemitter
import view
import chronicles
import ../../signals/types
import std/wrapnils
type OnboardingController* = object
type OnboardingController* = ref object of SignalSubscriber
view*: OnboardingView
variant*: QVariant
model*: AccountModel
appEvents*: EventEmitter
model: AccountModel
proc newController*(appEvents: EventEmitter): OnboardingController =
result = OnboardingController()
@ -16,8 +20,6 @@ proc newController*(appEvents: EventEmitter): OnboardingController =
result.model = newAccountModel()
result.view = newOnboardingView(result.model)
result.variant = newQVariant(result.view)
result.model.events.on("accountsReady") do(a: Args):
appEvents.emit("accountsReady", a)
proc delete*(self: OnboardingController) =
delete self.view
@ -25,6 +27,17 @@ proc delete*(self: OnboardingController) =
proc init*(self: OnboardingController) =
let accounts = self.model.generateAddresses()
for account in accounts:
self.view.addAddressToList(account.toAddress())
self.view.addAccountToList(account)
proc handleNodeLogin(self: OnboardingController, data: Signal) =
var response = NodeSignal(data)
self.view.setLastLoginResponse($response.event.toJson)
if ?.response.event.error == "" and self.model.currentAccount != nil:
self.appEvents.emit("login", AccountArgs(account: self.model.currentAccount))
method onSignal(self: OnboardingController, data: Signal) =
if data.signalType == SignalType.NodeLogin:
self.handleNodeLogin(data)
else:
discard

View File

@ -2,61 +2,83 @@ import NimQml
import Tables
import json
import nimcrypto
import ../../models/accounts as Models
import ../../status/types as status_types
import ../../signals/types
import strformat
import json_serialization
import ../../models/accounts as AccountModel
type
AddressRoles {.pure.} = enum
AccountRoles {.pure.} = enum
Username = UserRole + 1,
Identicon = UserRole + 2,
Key = UserRole + 3
QtObject:
type OnboardingView* = ref object of QAbstractListModel
addresses*: seq[Address]
model: AccountModel
accounts*: seq[GeneratedAccount]
lastLoginResponse: string
model*: AccountModel
proc setup(self: OnboardingView) =
self.QAbstractListModel.setup
proc delete*(self: OnboardingView) =
self.QAbstractListModel.delete
self.addresses = @[]
self.accounts = @[]
proc newOnboardingView*(model: AccountModel): OnboardingView =
new(result, delete)
result.accounts = @[]
result.lastLoginResponse = ""
result.model = model
result.addresses = @[]
result.setup
proc addAddressToList*(self: OnboardingView, address: Address) =
self.beginInsertRows(newQModelIndex(), self.addresses.len, self.addresses.len)
self.addresses.add(address)
proc addAccountToList*(self: OnboardingView, account: GeneratedAccount) =
self.beginInsertRows(newQModelIndex(), self.accounts.len, self.accounts.len)
self.accounts.add(account)
self.endInsertRows()
method rowCount(self: OnboardingView, index: QModelIndex = nil): int =
return self.addresses.len
return self.accounts.len
method data(self: OnboardingView, index: QModelIndex, role: int): QVariant =
if not index.isValid:
return
if index.row < 0 or index.row >= self.addresses.len:
if index.row < 0 or index.row >= self.accounts.len:
return
let asset = self.addresses[index.row]
let assetRole = role.AddressRoles
let asset = self.accounts[index.row]
let assetRole = role.AccountRoles
case assetRole:
of AddressRoles.Username: result = newQVariant(asset.username)
of AddressRoles.Identicon: result = newQVariant(asset.identicon)
of AddressRoles.Key: result = newQVariant(asset.key)
of AccountRoles.Username: result = newQVariant(asset.name)
of AccountRoles.Identicon: result = newQVariant(asset.photoPath)
of AccountRoles.Key: result = newQVariant(asset.keyUid)
method roleNames(self: OnboardingView): Table[int, string] =
{ AddressRoles.Username.int:"username",
AddressRoles.Identicon.int:"identicon",
AddressRoles.Key.int:"key" }.toTable
{ AccountRoles.Username.int:"username",
AccountRoles.Identicon.int:"identicon",
AccountRoles.Key.int:"key" }.toTable
proc storeAccountAndLogin(self: OnboardingView, selectedAccountIndex: int, password: string) {.slot.} =
discard self.model.storeAccountAndLogin(selectedAccountIndex, password)
proc storeAccountAndLogin(self: OnboardingView, selectedAccountIndex: int, password: string): string {.slot.} =
try:
result = self.model.storeAccountAndLogin(selectedAccountIndex, password).toJson
except:
let
e = getCurrentException()
msg = getCurrentExceptionMsg()
result = SignalError(error: msg).toJson
# TODO: this is temporary and will be removed once accounts import and creation is working
proc generateRandomAccountAndLogin*(self: OnboardingView) {.slot.} =
self.model.generateRandomAccountAndLogin()
proc lastLoginResponse*(self: OnboardingView): string =
result = self.lastLoginResponse
proc loginResponseChanged*(self: OnboardingView, response: string) {.signal.}
proc setLastLoginResponse*(self: OnboardingView, loginResponse: string) {.slot.} =
self.lastLoginResponse = loginResponse
self.loginResponseChanged(loginResponse)
QtProperty[string] loginResponse:
read = lastLoginResponse
write = setLastLoginResponse
notify = loginResponseChanged

View File

@ -1,28 +1,16 @@
import eventemitter
import json_serialization
import ../status/accounts as status_accounts
import ../status/types
type
Address* = ref object
username*, identicon*, key*: string
proc toAddress*(account: GeneratedAccount): Address =
result = Address(username: account.name, identicon: account.photoPath, key: account.address)
import options
type
AccountModel* = ref object
generatedAddresses*: seq[GeneratedAccount]
events*: EventEmitter
nodeAccounts*: seq[NodeAccount]
currentAccount*: Account
proc newAccountModel*(): AccountModel =
result = AccountModel()
result.events = createEventEmitter()
result.generatedAddresses = @[]
proc delete*(self: AccountModel) =
# delete self.generatedAddresses
discard
result.currentAccount = nil
proc generateAddresses*(self: AccountModel): seq[GeneratedAccount] =
var accounts = status_accounts.generateAddresses()
@ -32,13 +20,12 @@ proc generateAddresses*(self: AccountModel): seq[GeneratedAccount] =
self.generatedAddresses.add(account)
self.generatedAddresses
# TODO: this is temporary and will be removed once accounts import and creation is working
proc generateRandomAccountAndLogin*(self: AccountModel) =
let generatedAccounts = status_accounts.generateAddresses()
let account = status_accounts.setupAccount(generatedAccounts[0], "qwerty")
self.events.emit("accountsReady", AccountArgs(account: account))
proc login*(self: AccountModel, selectedAccountIndex: int, password: string): NodeAccount =
let currentNodeAccount = self.nodeAccounts[selectedAccountIndex]
self.currentAccount = currentNodeAccount.toAccount
result = status_accounts.login(currentNodeAccount, password)
proc storeAccountAndLogin*(self: AccountModel, selectedAccountIndex: int, password: string): Account =
let generatedAccount: GeneratedAccount = self.generatedAddresses[selectedAccountIndex]
result = status_accounts.setupAccount(generatedAccount, password)
self.events.emit("accountsReady", AccountArgs(account: result))
self.currentAccount = generatedAccount.toAccount

View File

@ -1,4 +1,5 @@
import eventemitter
import ../status/types
type
MailServer* = ref object
@ -11,5 +12,5 @@ type
type Profile* = ref object
username*, identicon*: string
proc toProfileModel*(obj: object): Profile =
result = Profile(username: obj.name, identicon: obj.photoPath)
proc toProfileModel*(account: Account): Profile =
result = Profile(username: account.name, identicon: account.photoPath)

View File

@ -6,13 +6,12 @@ import app/node/core as node
import app/profile/core as profile
import signals/core as signals
import app/onboarding/core as onboarding
import app/login/core as login
import state
import status/accounts as status_accounts
import status/core as status_core
import status/chat as status_chat
import status/types as types
import status/libstatus
import models/accounts
import state
import status/types
import eventemitter
@ -24,7 +23,7 @@ logScope:
topics = "main"
proc mainProc() =
let nodeAccounts = Json.decode(status_accounts.initNodeAccounts(), seq[NodeAccount]) # to be used for login
let nodeAccounts = status_accounts.initNodeAccounts()
let app = newQApplication()
let engine = newQQmlApplicationEngine()
let signalController = signals.newController(app)
@ -56,23 +55,33 @@ proc mainProc() =
var profile = profile.newController(appEvents)
engine.setRootContextProperty("profileModel", profile.variant)
# var accountsModel = newAccountModel()
appEvents.on("accountsReady") do(a: Args):
appEvents.on("login") do(a: Args):
var args = AccountArgs(a)
status_core.startMessenger()
wallet.init()
profile.init(args.account) # TODO: use correct account
profile.init(args.account)
# var onboarding = onboarding.newController(accountsModel)
var login = login.newController(appEvents)
var onboarding = onboarding.newController(appEvents)
onboarding.init()
engine.setRootContextProperty("onboardingModel", onboarding.variant)
# TODO: replace this with routing
let showLogin = nodeAccounts.len > 0
engine.setRootContextProperty("showLogin", newQVariant(showLogin))
if nodeAccounts.len > 0:
login.init(nodeAccounts)
engine.setRootContextProperty("loginModel", login.variant)
else:
onboarding.init()
engine.setRootContextProperty("onboardingModel", onboarding.variant)
signalController.init()
signalController.addSubscriber(SignalType.Wallet, wallet)
signalController.addSubscriber(SignalType.Wallet, node)
signalController.addSubscriber(SignalType.Message, chat)
signalController.addSubscriber(SignalType.WhisperFilterAdded, chat)
signalController.addSubscriber(SignalType.NodeLogin, login)
signalController.addSubscriber(SignalType.NodeLogin, onboarding)
engine.setRootContextProperty("signals", signalController.variant)
@ -81,8 +90,8 @@ proc mainProc() =
chat.load(channel.name)
)
# accountsModel.appEvents.on("accountsReady") do(a: Args):
# appEvents.on("accountsReady") do(a: Args):
# accountsModel.appEvents.on("login") do(a: Args):
# appEvents.on("login") do(a: Args):
# appState.addChannel("test")
# appState.addChannel("test2")
# appState.addChannel("status")

View File

@ -6,6 +6,8 @@ import types
import messages
import chronicles
import whisperFilter
import strutils
import json_serialization
logScope:
topics = "signals"
@ -45,27 +47,35 @@ QtObject:
let jsonSignal = (self.statusSignal).parseJson
let signalString = $jsonSignal["type"].getStr
var signalType: SignalType
var signal: Signal
trace "Raw signal data", data = $jsonSignal
case signalString:
of "messages.new":
signalType = SignalType.Message
var signalType: SignalType
try:
signalType = parseEnum[SignalType](signalString)
except:
warn "Unknown signal received", type = signalString
signalType = SignalType.Unknown
return
var signal: Signal = Signal(signalType: signalType)
case signalType:
of SignalType.Message:
signal = messages.fromEvent(jsonSignal)
of "whisper.filter.added":
signalType = SignalType.WhisperFilterAdded
of SignalType.WhisperFilterAdded:
signal = whisperFilter.fromEvent(jsonSignal)
of "wallet":
signalType = SignalType.Wallet
of SignalType.Wallet:
signal = WalletSignal(content: $jsonSignal)
of SignalType.NodeLogin:
signal = Json.decode($jsonSignal, NodeSignal)
else:
warn "Unhandled signal received", type = signalString
signalType = SignalType.Unknown
return
discard
signal.signalType = signalType
if not self.signalSubscribers.hasKey(signalType):
warn "Unhandled signal received", type = signalString
self.signalSubscribers[signalType] = @[]
for subscriber in self.signalSubscribers[signalType]:

View File

@ -1,10 +1,17 @@
import chronicles
import ../status/types
import json_serialization
type SignalSubscriber* = ref object of RootObj
type Signal* = ref object of RootObj
signalType*: SignalType
signalType* {.serializedFieldName("type").}: SignalType
type SignalError* = object
error*: string
type NodeSignal* = ref object of Signal
event*: SignalError
type WalletSignal* = ref object of Signal
content*: string

View File

@ -34,7 +34,7 @@ proc ensureDir(dirname: string) =
# removeDir(dirname)
createDir(dirname)
proc initNodeAccounts*(): string =
proc initNodeAccounts*(): seq[NodeAccount] =
const datadir = "./data/"
const keystoredir = "./data/keystore/"
const nobackupdir = "./noBackup/"
@ -44,9 +44,17 @@ proc initNodeAccounts*(): string =
ensureDir(nobackupdir)
discard $libstatus.initKeystore(keystoredir);
result = $libstatus.openAccounts(datadir);
let strNodeAccounts = $libstatus.openAccounts(datadir);
result = Json.decode(strNodeAccounts, seq[NodeAccount])
proc saveAccountAndLogin*(multiAccounts: MultiAccounts, alias: string, identicon: string, accountData: string, password: string, configJSON: string, settingsJSON: string): Account =
proc saveAccountAndLogin*(
multiAccounts: MultiAccounts,
alias: string,
identicon: string,
accountData: string,
password: string,
configJSON: string,
settingsJSON: string): Account =
let hashedPassword = "0x" & $keccak_256.digest(password)
let subaccountData = %* [
{
@ -69,11 +77,14 @@ proc saveAccountAndLogin*(multiAccounts: MultiAccounts, alias: string, identicon
var savedResult = $libstatus.saveAccountAndLogin(accountData, hashedPassword, settingsJSON, configJSON, $subaccountData)
let parsedSavedResult = savedResult.parseJson
let error = parsedSavedResult["error"].getStr
if parsedSavedResult["error"].getStr == "":
if error == "":
debug "Account saved succesfully"
result = Account(name: alias, photoPath: identicon)
return
result = Account(name: alias, photoPath: identicon)
raise newException(LoginError, "Error saving account and logging in: " & error)
proc generateMultiAccounts*(account: GeneratedAccount, password: string): MultiAccounts =
let hashedPassword = "0x" & $keccak_256.digest(password)
@ -135,3 +146,16 @@ proc setupAccount*(account: GeneratedAccount, password: string): Account =
# TODO this is needed for now for the retrieving of past messages. We'll either move or remove it later
let peer = "enode://44160e22e8b42bd32a06c1532165fa9e096eebedd7fa6d6e5f8bbef0440bc4a4591fe3651be68193a7ec029021cdb496cfe1d7f9f1dc69eb99226e6f39a7a5d4@35.225.221.245:443"
discard libstatus.addPeer(peer)
proc login*(nodeAccount: NodeAccount, password: string): NodeAccount =
let hashedPassword = "0x" & $keccak_256.digest(password)
let account = nodeAccount.toAccount
let loginResult = $libstatus.login($toJson(account), hashedPassword)
let error = parseJson(loginResult)["error"].getStr
if error == "":
debug "Login requested", user=nodeAccount.name
result = nodeAccount
return
raise newException(LoginError, "Error logging in: " & error)

View File

@ -25,3 +25,5 @@ proc sendTransaction*(jsonArgs: cstring, password: cstring): cstring {.importc:
proc generateAlias*(p0: GoString): cstring {.importc: "GenerateAlias".}
proc identicon*(p0: GoString): cstring {.importc: "Identicon".}
proc login*(acctData: cstring, password: cstring): cstring {.importc: "Login".}

View File

@ -7,13 +7,16 @@ type SignalCallback* = proc(eventMessage: cstring): void {.cdecl.}
type SignalType* {.pure.} = enum
Message = "messages.new"
Wallet = "wallet"
NodeReady = "node.ready"
NodeStarted = "node.started"
NodeLogin = "node.login"
EnvelopeSent = "envelope.sent"
EnvelopeExpired = "envelope.expired"
MailserverRequestCompleted = "mailserver.request.completed"
MailserverRequestExpired = "mailserver.request.expired"
DiscoverSummary = "discover.summary"
DiscoveryStarted = "discovery.started"
DiscoveryStopped = "discovery.stopped"
DiscoverySummary = "discovery.summary"
SubscriptionsData = "subscriptions.data"
SubscriptionsError = "subscriptions.error"
WhisperFilterAdded = "whisper.filter.added"
@ -36,31 +39,38 @@ type MultiAccounts* = object
type
Account* = object of RootObj
Account* = ref object of RootObj
name*: string
keyUid* {.serializedFieldName("key-uid").}: string
photoPath* {.serializedFieldName("photo-path").}: string
type
NodeAccount* = object
NodeAccount* = ref object of Account
timestamp*: int
keycardPairing* {.serializedFieldName("keycard-pairing").}: string
# deserialisation does not handle base classes, so flatten
name*: string
keyUid* {.serializedFieldName("key-uid").}: string
photoPath* {.serializedFieldName("photo-path").}: string
type
GeneratedAccount* = object
GeneratedAccount* = ref object
publicKey*: string
address*: string
id*: string
mnemonic*: string
derived*: MultiAccounts
# deserialisation does not handle base classes, so flatten
# FIXME: should inherit from Account but multiAccountGenerateAndDeriveAddresses
# response has a camel-cased properties like "publicKey" and "keyUid", so the
# serializedFieldName pragma would need to be different
name*: string
keyUid*: string
photoPath*: string
proc toAccount*(account: GeneratedAccount): Account =
result = Account(name: account.name, photoPath: account.photoPath, keyUid: account.address)
proc toAccount*(account: NodeAccount): Account =
result = Account(name: account.name, photoPath: account.photoPath, keyUid: account.keyUid)
type AccountArgs* = ref object of Args
account*: Account
type
LoginError* = object of Exception

View File

@ -9,10 +9,8 @@ SwipeView {
anchors.fill: parent
currentIndex: 0
signal loginDone
onCurrentItemChanged: {
currentItem.txtPassword.focus = true
currentItem.txtPassword.focus = true;
}
Item {
@ -87,17 +85,16 @@ SwipeView {
anchors.fill: parent
}
Button {
text: "Select"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 20
onClicked: {
console.log("button: " + wizardStep2.selectedIndex)
swipeView.incrementCurrentIndex()
}
Button {
text: "Select"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 20
onClicked: {
swipeView.incrementCurrentIndex();
}
}
}
}
@ -135,9 +132,7 @@ SwipeView {
anchors.bottom: parent.bottom
anchors.bottomMargin: 20
onClicked: {
console.log("password: " + txtPassword.text)
swipeView.incrementCurrentIndex()
swipeView.incrementCurrentIndex();
}
}
}
@ -178,9 +173,9 @@ SwipeView {
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok
onAccepted: {
txtConfirmPassword.clear()
swipeView.currentIndex = 1
txtPassword.focus = true
txtConfirmPassword.clear();
swipeView.currentIndex = 1;
txtPassword.focus = true;
}
}
@ -188,27 +183,32 @@ SwipeView {
id: storeAccountAndLoginError
title: "Error storing account and logging in"
text: "An error occurred while storing your account and logging in: "
icon: StandardIcon.Warning
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
Connections {
target: onboardingModel
onLoginResponseChanged: {
const loginResponse = JSON.parse(response);
if(loginResponse.error){
storeAccountAndLoginError.text += loginResponse.error;
storeAccountAndLoginError.open()
}
}
}
Button {
text: "Finish"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 20
onClicked: {
console.log("confirm clicked " + txtConfirmPassword.text + " : " + txtPassword.text)
if (txtConfirmPassword.text != txtPassword.text) {
return passwordsDontMatchError.open()
return passwordsDontMatchError.open();
}
const selectedAccountIndex = wizardStep2.selectedIndex
onboardingModel.storeAccountAndLogin(selectedAccountIndex,
txtPassword.text)
swipeView.loginDone()
onboardingModel.storeAccountAndLogin(selectedAccountIndex, txtPassword.text)
}
}
}

View File

@ -29,21 +29,6 @@ RowLayout {
Item {
id: itmSlide1
StyledButton {
id: btnGenRandomAcct
width: 250
height: 50
anchors.top: parent.top
anchors.topMargin: 50
anchors.left: parent.left
anchors.leftMargin: 15
label: "Generate random account and login"
onClicked: {
onboardingModel.generateRandomAccountAndLogin()
app.visible = true
}
}
Image {
id: img1
anchors.horizontalCenter: parent.horizontalCenter

176
ui/onboarding/Login.qml Normal file
View File

@ -0,0 +1,176 @@
import QtQuick 2.3
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.11
import QtQuick.Window 2.11
import QtQuick.Dialogs 1.3
SwipeView {
id: swipeView
anchors.fill: parent
currentIndex: 0
signal loginDone(response: var)
onCurrentItemChanged: {
currentItem.txtPassword.focus = true;
}
Item {
id: wizardStep2
property int selectedIndex: 0
ColumnLayout {
id: columnLayout
width: 620
height: 427
Text {
text: "Login"
font.pointSize: 36
anchors.top: parent.top
anchors.topMargin: 20
anchors.horizontalCenter: parent.horizontalCenter
}
RowLayout {
Layout.fillHeight: true
Layout.fillWidth: true
spacing: 10
ButtonGroup {
id: accountGroup
}
Component {
id: addressViewDelegate
Item {
height: 56
anchors.right: parent.right
anchors.rightMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
Row {
RadioButton {
checked: index == 0 ? true : false
ButtonGroup.group: accountGroup
onClicked: {
wizardStep2.selectedIndex = index;
}
}
Column {
Image {
source: identicon
}
}
Column {
Text {
text: username
}
Text {
text: key
width: 160
elide: Text.ElideMiddle
}
}
}
}
}
ListView {
id: addressesView
contentWidth: 200
model: loginModel
delegate: addressViewDelegate
Layout.fillHeight: true
Layout.fillWidth: true
anchors.topMargin: 36
anchors.fill: parent
}
}
Button {
text: "Select"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 20
onClicked: {
swipeView.incrementCurrentIndex();
}
}
}
}
Item {
id: wizardStep3
property Item txtPassword: txtPassword
Text {
text: "Enter password"
font.pointSize: 36
anchors.top: parent.top
anchors.topMargin: 20
anchors.horizontalCenter: parent.horizontalCenter
}
Rectangle {
color: "#EEEEEE"
anchors.left: parent.left
anchors.right: parent.right
anchors.centerIn: parent
height: 32
width: parent.width - 40
TextInput {
id: txtPassword
anchors.fill: parent
focus: true
echoMode: TextInput.Password
selectByMouse: true
}
}
MessageDialog {
id: loginError
title: "Login failed"
text: "Login failed. Please re-enter your password and try again."
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
onAccepted: {
txtPassword.clear();
txtPassword.focus = true
}
}
Connections {
target: loginModel
onLoginResponseChanged: {
const loginResponse = JSON.parse(response);
if(loginResponse.error){
loginError.open()
}
}
}
Button {
text: "Finish"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 20
onClicked: {
const selectedAccountIndex = wizardStep2.selectedIndex
const response = loginModel.login(selectedAccountIndex, txtPassword.text)
// TODO: replace me with something graphical (ie spinner)
console.log("Logging in...")
}
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -9,7 +9,7 @@ Page {
DSM.StateMachine {
id: stateMachine
initialState: stateIntro
initialState: showLogin ? stateLogin : stateIntro
running: onboardingMain.visible
DSM.State {
@ -43,25 +43,36 @@ Page {
id: existingKeyState
onEntered: existingKey.visible = true
onExited: existingKey.visible = false
// DSM.SignalTransition {
// targetState: keysMainState
// signal: keysMain.btnExistingKey.clicked
// }
}
DSM.State {
id: genKeyState
onEntered: {
genKey.visible = true
}
onEntered: genKey.visible = true
onExited: genKey.visible = false
DSM.SignalTransition {
targetState: appState
signal: genKey.loginDone
// guard: !response.error
}
DSM.SignalTransition {
targetState: appState
signal: onboardingModel.loginResponseChanged
guard: {
const resp = JSON.parse(response);
return !resp.error
}
}
}
DSM.State {
id: stateLogin
onEntered: login.visible = true
onExited: login.visible = false
DSM.SignalTransition {
targetState: appState
signal: loginModel.loginResponseChanged
guard: {
const resp = JSON.parse(response);
return !resp.error
}
}
}
DSM.FinalState {
@ -74,7 +85,7 @@ Page {
Intro {
id: intro
anchors.fill: parent
visible: true
visible: false
}
KeysMain {
@ -94,6 +105,12 @@ Page {
anchors.fill: parent
visible: false
}
Login {
id: login
anchors.fill: parent
visible: false
}
}
/*##^##

2
vendor/nim-stew vendored

@ -1 +1 @@
Subproject commit d0f5be4971ad34d115b9749d9fb69bdd2aecf525
Subproject commit a99dafab420bcbbffee35e9bd847a9014eafaffe