feat: Integrate new logins flows happy paths (#17137)

* feat(login): integrate basic login flows happy paths

Fixes #17137

* fix: rebase issues and pr comments

* chore: switch status-keycard-go to master branch

* fix: tests

---------

Co-authored-by: Igor Sirotin <sirotin@status.im>
This commit is contained in:
Jonathan Rainville 2025-02-04 10:08:13 -05:00 committed by GitHub
parent 2d549f42be
commit bf5de4087e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 369 additions and 122 deletions

View File

@ -148,12 +148,9 @@ proc connect(self: AppController) =
elif defined(production):
setLogLevel(chronicles.LogLevel.INFO)
# TODO remove these functions once we have only the new onboarding module
proc shouldStartWithOnboardingScreen(self: AppController): bool =
return self.accountsService.openedAccounts().len == 0
# TODO remove this function once we have only the new onboarding module
proc shouldUseTheNewOnboardingModule(self: AppController): bool =
# Only the onboarding for new users is implemented in the new module for now
return singletonInstance.featureFlags().getOnboardingV2Enabled() and self.shouldStartWithOnboardingScreen()
return singletonInstance.featureFlags().getOnboardingV2Enabled()
proc newAppController*(statusFoundation: StatusFoundation): AppController =
result = AppController()

View File

@ -9,6 +9,7 @@ import app_service/service/accounts/service as accounts_service
import app_service/service/accounts/dto/image_crop_rectangle
import app_service/service/devices/service as devices_service
import app_service/service/keycardV2/service as keycard_serviceV2
import app_service/common/utils
from app_service/service/keycardV2/dto import KeycardExportedKeysDto
logScope:
@ -86,14 +87,29 @@ proc init*(self: Controller) =
self.delegate.onKeycardLoadMnemonicSuccess(args.keyUID)
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_KEYS_FAILURE) do(e: Args):
handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_RESTORE_KEYS_FAILURE) do(e: Args):
let args = KeycardErrorArg(e)
self.delegate.onKeycardExportKeysFailure(args.error)
self.delegate.onKeycardExportRestoreKeysFailure(args.error)
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_KEYS_SUCCESS) do(e: Args):
handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_RESTORE_KEYS_SUCCESS) do(e: Args):
let args = KeycardExportedKeysArg(e)
self.delegate.onKeycardExportKeysSuccess(args.exportedKeys)
self.delegate.onKeycardExportRestoreKeysSuccess(args.exportedKeys)
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_LOGIN_KEYS_FAILURE) do(e: Args):
let args = KeycardErrorArg(e)
self.delegate.onKeycardExportLoginKeysFailure(args.error)
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_KEYCARD_EXPORT_LOGIN_KEYS_SUCCESS) do(e: Args):
let args = KeycardExportedKeysArg(e)
self.delegate.onKeycardExportLoginKeysSuccess(args.exportedKeys)
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_LOGIN_ERROR) do(e: Args):
let args = LoginErrorArgs(e)
self.delegate.onAccountLoginError(args.error)
self.connectionIds.add(handlerId)
proc initialize*(self: Controller, pin: string) =
@ -174,3 +190,40 @@ proc generateMnemonic*(self: Controller, length: int): string =
proc exportRecoverKeysFromKeycard*(self: Controller) =
self.keycardServiceV2.asyncExportRecoverKeys()
proc exportLoginKeysFromKeycard*(self: Controller) =
self.keycardServiceV2.asyncExportLoginKeys()
proc getOpenedAccounts*(self: Controller): seq[AccountDto] =
return self.accountsService.openedAccounts()
proc getAccountByKeyUid*(self: Controller, keyUid: string): AccountDto =
return self.accountsService.getAccountByKeyUid(keyUid)
proc login*(
self: Controller,
account: AccountDto,
password: string,
keycard: bool = false,
publicEncryptionKey: string = "",
privateWhisperKey: string = "",
mnemonic: string = "",
keycardReplacement: bool = false,
) =
var passwordHash, chatPrivateKey = ""
if not keycard:
passwordHash = hashPassword(password)
else:
passwordHash = publicEncryptionKey
chatPrivateKey = privateWhisperKey
# if keycard and keycardReplacement:
# self.delegate.applyKeycardReplacementAfterLogin()
self.accountsService.login(
account,
passwordHash,
chatPrivateKey,
mnemonic,
)

View File

@ -45,6 +45,9 @@ method loadMnemonic*(self: AccessInterface, dataJson: string) {.base.} =
method finishOnboardingFlow*(self: AccessInterface, flowInt: int, dataJson: string): string {.base.} =
raise newException(ValueError, "No implementation available")
method loginRequested*(self: AccessInterface, keyUid: string, loginFlow: int, dataJson: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onLocalPairingStatusUpdate*(self: AccessInterface, status: LocalPairingStatus) {.base.} =
raise newException(ValueError, "No implementation available")
@ -63,10 +66,19 @@ method onKeycardLoadMnemonicFailure*(self: AccessInterface, error: string) {.bas
method onKeycardLoadMnemonicSuccess*(self: AccessInterface, keyUID: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onKeycardExportKeysFailure*(self: AccessInterface, error: string) {.base.} =
method onKeycardExportRestoreKeysFailure*(self: AccessInterface, error: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onKeycardExportKeysSuccess*(self: AccessInterface, exportedKeys: KeycardExportedKeysDto) {.base.} =
method onKeycardExportRestoreKeysSuccess*(self: AccessInterface, exportedKeys: KeycardExportedKeysDto) {.base.} =
raise newException(ValueError, "No implementation available")
method onKeycardExportLoginKeysFailure*(self: AccessInterface, error: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onKeycardExportLoginKeysSuccess*(self: AccessInterface, exportedKeys: KeycardExportedKeysDto) {.base.} =
raise newException(ValueError, "No implementation available")
method onAccountLoginError*(self: AccessInterface, error: string) {.base.} =
raise newException(ValueError, "No implementation available")
method exportRecoverKeys*(self: AccessInterface) {.base.} =

View File

@ -1,4 +1,4 @@
import NimQml, chronicles, json
import NimQml, chronicles, json, strutils
import logging
import io_interface
@ -14,12 +14,14 @@ from app_service/service/settings/dto/settings import SettingsDto
from app_service/service/accounts/dto/accounts import AccountDto
from app_service/service/keycardV2/dto import KeycardEventDto, KeycardExportedKeysDto, KeycardState
import ../startup/models/login_account_item as login_acc_item
export io_interface
logScope:
topics = "onboarding-module"
type SecondaryFlow* {.pure} = enum
type OnboardingFlow* {.pure} = enum
Unknown = 0,
CreateProfileWithPassword,
CreateProfileWithSeedphrase,
@ -28,13 +30,17 @@ type SecondaryFlow* {.pure} = enum
LoginWithSeedphrase,
LoginWithSyncing,
LoginWithKeycard,
ActualLogin, # TODO get the real name and value for this when it's implemented on the front-end
type LoginMethod* {.pure} = enum
Unknown = 0,
Password,
Keycard,
type ProgressState* {.pure.} = enum
Idle,
InProgress,
Success,
Failed
Failed,
type
Module*[T: io_interface.DelegateInterface] = ref object of io_interface.AccessInterface
@ -43,7 +49,8 @@ type
viewVariant: QVariant
controller: Controller
localPairingStatus: LocalPairingStatus
currentFlow: SecondaryFlow
loginFlow: LoginMethod
onboardingFlow: OnboardingFlow
exportedKeys: KeycardExportedKeysDto
proc newModule*[T](
@ -58,6 +65,8 @@ proc newModule*[T](
result.delegate = delegate
result.view = view.newView(result)
result.viewVariant = newQVariant(result.view)
result.onboardingFlow = OnboardingFlow.Unknown
result.loginFlow = LoginMethod.Unknown
result.controller = controller.newController(
result,
events,
@ -87,6 +96,20 @@ method onAppLoaded*[T](self: Module[T]) =
method load*[T](self: Module[T]) =
singletonInstance.engine.setRootContextProperty("onboardingModule", self.viewVariant)
self.controller.init()
let openedAccounts = self.controller.getOpenedAccounts()
if openedAccounts.len > 0:
var items: seq[login_acc_item.Item]
for i in 0..<openedAccounts.len:
let acc = openedAccounts[i]
var thumbnailImage: string
var largeImage: string
acc.extractImages(thumbnailImage, largeImage)
items.add(login_acc_item.initItem(order = i, acc.name, icon = "", thumbnailImage, largeImage, acc.keyUid, acc.colorHash,
acc.colorId, acc.keycardPairing))
self.view.setLoginAccountsModelItems(items)
self.delegate.onboardingDidLoad()
method initialize*[T](self: Module[T], pin: string) =
@ -118,7 +141,7 @@ method loadMnemonic*[T](self: Module[T], mnemonic: string) =
method finishOnboardingFlow*[T](self: Module[T], flowInt: int, dataJson: string): string =
try:
self.currentFlow = SecondaryFlow(flowInt)
self.onboardingFlow = OnboardingFlow(flowInt)
let data = parseJson(dataJson)
let password = data["password"].str
@ -126,18 +149,18 @@ method finishOnboardingFlow*[T](self: Module[T], flowInt: int, dataJson: string)
var err = ""
case self.currentFlow:
case self.onboardingFlow:
# CREATE PROFILE FLOWS
of SecondaryFlow.CreateProfileWithPassword:
of OnboardingFlow.CreateProfileWithPassword:
err = self.controller.createAccountAndLogin(password)
of SecondaryFlow.CreateProfileWithSeedphrase:
of OnboardingFlow.CreateProfileWithSeedphrase:
err = self.controller.restoreAccountAndLogin(
password,
seedPhrase,
recoverAccount = false,
keycardInstanceUID = "",
)
of SecondaryFlow.CreateProfileWithKeycardNewSeedphrase:
of OnboardingFlow.CreateProfileWithKeycardNewSeedphrase:
# New user with a seedphrase we showed them
let keycardEvent = self.view.getKeycardEvent()
err = self.controller.restoreAccountAndLogin(
@ -146,7 +169,7 @@ method finishOnboardingFlow*[T](self: Module[T], flowInt: int, dataJson: string)
recoverAccount = false,
keycardInstanceUID = keycardEvent.keycardInfo.instanceUID,
)
of SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase:
of OnboardingFlow.CreateProfileWithKeycardExistingSeedphrase:
# New user who entered their own seed phrase
let keycardEvent = self.view.getKeycardEvent()
err = self.controller.restoreAccountAndLogin(
@ -157,21 +180,21 @@ method finishOnboardingFlow*[T](self: Module[T], flowInt: int, dataJson: string)
)
# LOGIN FLOWS
of SecondaryFlow.LoginWithSeedphrase:
of OnboardingFlow.LoginWithSeedphrase:
err = self.controller.restoreAccountAndLogin(
password,
seedPhrase,
recoverAccount = true,
keycardInstanceUID = "",
)
of SecondaryFlow.LoginWithSyncing:
of OnboardingFlow.LoginWithSyncing:
# The pairing was already done directly through inputConnectionStringForBootstrapping, we can login
self.controller.loginLocalPairingAccount(
self.localPairingStatus.account,
self.localPairingStatus.password,
self.localPairingStatus.chatKey,
)
of SecondaryFlow.LoginWithKeycard:
of OnboardingFlow.LoginWithKeycard:
err = self.controller.restoreKeycardAccountAndLogin(
self.view.getKeycardEvent().keycardInfo.keyUID,
self.view.getKeycardEvent().keycardInfo.instanceUID,
@ -179,31 +202,56 @@ method finishOnboardingFlow*[T](self: Module[T], flowInt: int, dataJson: string)
recoverAccount = true
)
else:
raise newException(ValueError, "Unknown flow: " & $self.currentFlow)
raise newException(ValueError, "Unknown flow: " & $self.onboardingFlow)
return err
except Exception as e:
error "Error finishing Onboarding Flow", msg = e.msg
return e.msg
method loginRequested*[T](self: Module[T], keyUid: string, loginFlow: int, dataJson: string) =
try:
self.loginFlow = LoginMethod(loginFlow)
let data = parseJson(dataJson)
let account = self.controller.getAccountByKeyUid(keyUid)
case self.loginFlow:
of LoginMethod.Password:
self.controller.login(account, data["password"].str)
of LoginMethod.Keycard:
self.authorize(data["pin"].str)
# We will continue the flow when the card is authorized in onKeycardStateUpdated
else:
raise newException(ValueError, "Unknown flow: " & $self.onboardingFlow)
except Exception as e:
error "Error finishing Login Flow", msg = e.msg
self.view.accountLoginError(e.msg, wrongPassword = false)
proc finishAppLoading2[T](self: Module[T]) =
self.delegate.appReady()
# TODO get the flow to send the right metric
var eventType = "user-logged-in"
if self.currentFlow != SecondaryFlow.ActualLogin:
if self.loginFlow == LoginMethod.Unknown:
eventType = "onboarding-completed"
singletonInstance.globalEvents.addCentralizedMetricIfEnabled(eventType,
$(%*{"flowType": repr(self.currentFlow)}))
$(%*{"flowType": repr(self.onboardingFlow)}))
self.controller.stopKeycardService()
self.delegate.finishAppLoading()
method onAccountLoginError*[T](self: Module[T], error: string) =
# SQLITE_NOTADB: "file is not a database"
var wrongPassword = false
if error.contains("file is not a database"):
wrongPassword = true
self.view.accountLoginError(error, wrongPassword)
method onNodeLogin*[T](self: Module[T], error: string, account: AccountDto, settings: SettingsDto) =
if error.len != 0:
# TODO: Handle error
echo "ERROR from onNodeLogin: ", error
self.onAccountLoginError(error)
return
self.controller.setLoggedInAccount(account)
@ -221,6 +269,11 @@ method onLocalPairingStatusUpdate*[T](self: Module[T], status: LocalPairingStatu
method onKeycardStateUpdated*[T](self: Module[T], keycardEvent: KeycardEventDto) =
self.view.setKeycardEvent(keycardEvent)
if keycardEvent.state == KeycardState.Authorized and self.loginFlow == LoginMethod.Keycard:
# After authorizing, we export the keys
self.controller.exportLoginKeysFromKeycard()
# We will login once we have the keys in onKeycardExportLoginKeysSuccess
if keycardEvent.state == KeycardState.NotEmpty and self.view.getPinSettingState() == ProgressState.InProgress.int:
# We just finished setting the pin
self.view.setPinSettingState(ProgressState.Success.int)
@ -235,19 +288,39 @@ method onKeycardSetPinFailure*[T](self: Module[T], error: string) =
method onKeycardAuthorizeFailure*[T](self: Module[T], error: string) =
self.view.setAuthorizationState(ProgressState.Failed.int)
if self.loginFlow == LoginMethod.Keycard:
# We were trying to login and the authorization failed
var wrongPassword = false
if error.contains("wrong pin"):
wrongPassword = true
self.view.accountLoginError(error, wrongPassword)
method onKeycardLoadMnemonicFailure*[T](self: Module[T], error: string) =
self.view.setAddKeyPairState(ProgressState.Failed.int)
method onKeycardLoadMnemonicSuccess*[T](self: Module[T], keyUID: string) =
self.view.setAddKeyPairState(ProgressState.Success.int)
method onKeycardExportKeysFailure*[T](self: Module[T], error: string) =
method onKeycardExportRestoreKeysFailure*[T](self: Module[T], error: string) =
self.view.setRestoreKeysExportState(ProgressState.Failed.int)
method onKeycardExportKeysSuccess*[T](self: Module[T], exportedKeys: KeycardExportedKeysDto) =
method onKeycardExportRestoreKeysSuccess*[T](self: Module[T], exportedKeys: KeycardExportedKeysDto) =
self.exportedKeys = exportedKeys
self.view.setRestoreKeysExportState(ProgressState.Success.int)
method onKeycardExportLoginKeysFailure*[T](self: Module[T], error: string) =
self.view.accountLoginError(error, wrongPassword = false)
method onKeycardExportLoginKeysSuccess*[T](self: Module[T], exportedKeys: KeycardExportedKeysDto) =
# We got the keys, now we can login. If everything goes well, we will finish the app loading
self.controller.login(
self.controller.getAccountByKeyUid(self.view.getKeycardEvent.keycardInfo.keyUID),
password = "",
keycard = true,
publicEncryptionKey = exportedKeys.encryptionKey.publicKey,
privateWhisperKey = exportedKeys.whisperKey.privateKey,
)
method exportRecoverKeys*[T](self: Module[T]) =
self.view.setRestoreKeysExportState(ProgressState.InProgress.int)
self.controller.exportRecoverKeysFromKeycard()

View File

@ -2,6 +2,10 @@ import NimQml
import io_interface
from app_service/service/keycardV2/dto import KeycardEventDto
# TODO move these files to this module when we remove the old onboarding
import ../startup/models/login_account_model as login_acc_model
import ../startup/models/login_account_item as login_acc_item
QtObject:
type
View* = ref object of QObject
@ -12,15 +16,20 @@ QtObject:
pinSettingState: int
authorizationState: int
restoreKeysExportState: int
loginAccountsModel: login_acc_model.Model
loginAccountsModelVariant: QVariant
proc delete*(self: View) =
self.QObject.delete
self.loginAccountsModel.delete
self.loginAccountsModelVariant.delete
proc newView*(delegate: io_interface.AccessInterface): View =
new(result, delete)
result.QObject.setup
result.delegate = delegate
result.loginAccountsModel = login_acc_model.newModel()
result.loginAccountsModelVariant = newQVariant(result.loginAccountsModel)
### QtSignals ###
@ -109,6 +118,16 @@ QtObject:
proc getKeycardEvent*(self: View): KeycardEventDto =
return self.keycardEvent
proc loginAccountsModelChanged*(self: View) {.signal.}
proc getLoginAccountsModel(self: View): QVariant {.slot.} =
return self.loginAccountsModelVariant
proc setLoginAccountsModelItems*(self: View, accounts: seq[login_acc_item.Item]) =
self.loginAccountsModel.setItems(accounts)
self.loginAccountsModelChanged()
QtProperty[QVariant] loginAccountsModel:
read = getLoginAccountsModel
notify = loginAccountsModelChanged
### slots ###
proc setPin(self: View, pin: string) {.slot.} =
@ -140,3 +159,6 @@ QtObject:
proc finishOnboardingFlow(self: View, flowInt: int, dataJson: string): string {.slot.} =
self.delegate.finishOnboardingFlow(flowInt, dataJson)
proc loginRequested(self: View, keyUid: string, loginFlow: int, dataJson: string): string {.slot.} =
self.delegate.loginRequested(keyUid, loginFlow, dataJson)

View File

@ -73,3 +73,10 @@ proc toWakuBackedUpProfileDto*(jsonObj: JsonNode): WakuBackedUpProfileDto =
if(jsonObj.getProp("images", obj) and obj.kind == JArray):
for imgObj in obj:
result.images.add(toImage(imgObj))
proc extractImages*(account: AccountDto, thumbnailImage: var string, largeImage: var string) =
for img in account.images:
if(img.imgType == "thumbnail"):
thumbnailImage = img.uri
elif(img.imgType == "large"):
largeImage = img.uri

View File

@ -123,6 +123,9 @@ QtObject:
error "error: ", procName="validateMnemonic", errName = e.name, errDesription = e.msg
proc openedAccounts*(self: Service): seq[AccountDto] =
if self.accounts.len > 0:
return self.accounts
try:
let response = status_account.openedAccounts(main_constants.STATUSGODIR)
@ -136,6 +139,11 @@ QtObject:
proc openedAccountsContainsKeyUid*(self: Service, keyUid: string): bool =
return (keyUID in self.openedAccounts().mapIt(it.keyUid))
proc getAccountByKeyUid*(self: Service, keyUid: string): AccountDto =
for account in self.openedAccounts():
if account.keyUid == keyUid:
return account
# FIXME: remove this method, settings should be processed in status-go
# https://github.com/status-im/status-go/issues/5359
proc addKeycardDetails(self: Service, kcInstance: string, settingsJson: var JsonNode, accountData: var JsonNode) =

View File

@ -69,3 +69,20 @@ proc asyncExportRecoverKeysTask(argEncoded: string) {.gcsafe, nimcall.} =
arg.finish(%* {
"error": e.msg,
})
type
AsyncExportLoginKeysArg = ref object of QObjectTaskArg
rpcCounter: int
proc asyncExportLoginKeysTask(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncExportLoginKeysArg](argEncoded)
try:
let response = callRPC(arg.rpcCounter, "ExportLoginKeys")
arg.finish(%*{
"response": response,
"error": ""
})
except Exception as e:
arg.finish(%* {
"error": e.msg,
})

View File

@ -31,8 +31,10 @@ const SIGNAL_KEYCARD_SET_PIN_FAILURE* = "keycardSetPinFailure"
const SIGNAL_KEYCARD_AUTHORIZE_FAILURE* = "keycardAuthorizeFailure"
const SIGNAL_KEYCARD_LOAD_MNEMONIC_FAILURE* = "keycardLoadMnemonicFailure"
const SIGNAL_KEYCARD_LOAD_MNEMONIC_SUCCESS* = "keycardLoadMnemonicSuccess"
const SIGNAL_KEYCARD_EXPORT_KEYS_FAILURE* = "keycardExportKeysFailure"
const SIGNAL_KEYCARD_EXPORT_KEYS_SUCCESS* = "keycardExportKeysSuccess"
const SIGNAL_KEYCARD_EXPORT_RESTORE_KEYS_FAILURE* = "keycardExportRestoreKeysFailure"
const SIGNAL_KEYCARD_EXPORT_RESTORE_KEYS_SUCCESS* = "keycardExportRestoreKeysSuccess"
const SIGNAL_KEYCARD_EXPORT_LOGIN_KEYS_FAILURE* = "keycardExportLoginKeysFailure"
const SIGNAL_KEYCARD_EXPORT_LOGIN_KEYS_SUCCESS* = "keycardExportLoginKeysSuccess"
type
KeycardEventArg* = ref object of Args
@ -157,10 +159,9 @@ QtObject:
let rpcResponseObj = responseObj["response"].getStr().parseJson()
if rpcResponseObj{"error"}.kind != JNull and rpcResponseObj{"error"}.getStr != "":
let error = Json.decode(rpcResponseObj["error"].getStr, RpcError)
raise newException(RpcException, "Error authorizing: " & error.message)
raise newException(RpcException, rpcResponseObj["error"].getStr)
except Exception as e:
error "error set pin: ", msg = e.msg
error "error during authorize: ", msg = e.msg
self.events.emit(SIGNAL_KEYCARD_AUTHORIZE_FAILURE, KeycardErrorArg(error: e.msg))
proc receiveKeycardSignalV2(self: Service, signal: string) {.slot.} =
@ -231,9 +232,37 @@ QtObject:
raise newException(RpcException, "Error authorizing: " & error.message)
let keys = rpcResponseObj["result"]["keys"].toKeycardExportedKeysDto()
self.events.emit(SIGNAL_KEYCARD_EXPORT_KEYS_SUCCESS, KeycardExportedKeysArg(exportedKeys: keys))
self.events.emit(SIGNAL_KEYCARD_EXPORT_RESTORE_KEYS_SUCCESS, KeycardExportedKeysArg(exportedKeys: keys))
except Exception as e:
error "error exporting recover keys", msg = e.msg
self.events.emit(SIGNAL_KEYCARD_EXPORT_KEYS_FAILURE, KeycardErrorArg(error: e.msg))
self.events.emit(SIGNAL_KEYCARD_EXPORT_RESTORE_KEYS_FAILURE, KeycardErrorArg(error: e.msg))
proc asyncExportLoginKeys*(self: Service) =
self.rpcCounter += 1
let arg = AsyncExportLoginKeysArg(
tptr: asyncExportLoginKeysTask,
vptr: cast[uint](self.vptr),
slot: "onAsyncExportLoginKeys",
rpcCounter: self.rpcCounter,
)
self.threadpool.start(arg)
proc onAsyncExportLoginKeys*(self: Service, response: string) {.slot.} =
try:
let responseObj = response.parseJson
if responseObj{"error"}.kind != JNull and responseObj{"error"}.getStr != "":
raise newException(CatchableError, responseObj{"error"}.getStr)
let rpcResponseObj = responseObj["response"].getStr().parseJson()
if rpcResponseObj{"error"}.kind != JNull and rpcResponseObj{"error"}.getStr != "":
let error = Json.decode(rpcResponseObj["error"].getStr, RpcError)
raise newException(RpcException, "Error authorizing: " & error.message)
let keys = rpcResponseObj["result"]["keys"].toKeycardExportedKeysDto()
self.events.emit(SIGNAL_KEYCARD_EXPORT_LOGIN_KEYS_SUCCESS, KeycardExportedKeysArg(exportedKeys: keys))
except Exception as e:
error "error exporting login keys", msg = e.msg
self.events.emit(SIGNAL_KEYCARD_EXPORT_LOGIN_KEYS_FAILURE, KeycardErrorArg(error: e.msg))

View File

@ -63,6 +63,7 @@ SplitView {
property int authorizationState: Onboarding.ProgressState.Idle
property int restoreKeysExportState: Onboarding.ProgressState.Idle
property int syncState: Onboarding.ProgressState.Idle
property var loginAccountsModel: ctrlLoginScreen.checked ? loginAccountsModel : emptyModel
property int keycardRemainingPinAttempts: 2
property int keycardRemainingPukAttempts: 3
@ -142,8 +143,6 @@ SplitView {
signal obtainingPasswordError(string errorDescription, string errorType /* Constants.keychain.errorType.* */, bool wrongFingerprint)
}
loginAccountsModel: ctrlLoginScreen.checked ? loginAccountsModel : emptyModel
biometricsAvailable: ctrlBiometrics.checked
isBiometricsLogin: localAccountSettings.storeToKeychainValue === Constants.keychain.storedValue.store
onBiometricsRequested: biometricsPopup.open()

View File

@ -53,7 +53,6 @@ Item {
biometricsAvailable: mockDriver.biometricsAvailable
keycardPinInfoPageDelay: 0
loginAccountsModel: emptyModel
isBiometricsLogin: biometricsAvailable
onboardingStore: OnboardingStore {
@ -62,6 +61,8 @@ Item {
readonly property int authorizationState: mockDriver.authorizationState // enum Onboarding.ProgressState
readonly property int restoreKeysExportState: mockDriver.restoreKeysExportState // enum Onboarding.ProgressState
property int keycardRemainingPinAttempts: 5
property int keycardRemainingPukAttempts: 5
property var loginAccountsModel: emptyModel
function setPin(pin: string) {
const valid = pin === mockDriver.existingPin
@ -316,7 +317,7 @@ Item {
// FINISH
tryCompare(finishedSpy, "count", 1)
compare(finishedSpy.signalArguments[0][0], Onboarding.SecondaryFlow.CreateProfileWithPassword)
compare(finishedSpy.signalArguments[0][0], Onboarding.OnboardingFlow.CreateProfileWithPassword)
const resultData = finishedSpy.signalArguments[0][1]
verify(!!resultData)
compare(resultData.password, mockDriver.dummyNewPassword)
@ -414,7 +415,7 @@ Item {
// FINISH
tryCompare(finishedSpy, "count", 1)
compare(finishedSpy.signalArguments[0][0], Onboarding.SecondaryFlow.CreateProfileWithSeedphrase)
compare(finishedSpy.signalArguments[0][0], Onboarding.OnboardingFlow.CreateProfileWithSeedphrase)
const resultData = finishedSpy.signalArguments[0][1]
verify(!!resultData)
compare(resultData.password, mockDriver.dummyNewPassword)
@ -563,7 +564,7 @@ Item {
// FINISH
tryCompare(finishedSpy, "count", 1)
compare(finishedSpy.signalArguments[0][0], Onboarding.SecondaryFlow.CreateProfileWithKeycardNewSeedphrase)
compare(finishedSpy.signalArguments[0][0], Onboarding.OnboardingFlow.CreateProfileWithKeycardNewSeedphrase)
const resultData = finishedSpy.signalArguments[0][1]
verify(!!resultData)
compare(resultData.password, "")
@ -666,7 +667,7 @@ Item {
// FINISH
tryCompare(finishedSpy, "count", 1)
compare(finishedSpy.signalArguments[0][0], Onboarding.SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase)
compare(finishedSpy.signalArguments[0][0], Onboarding.OnboardingFlow.CreateProfileWithKeycardExistingSeedphrase)
const resultData = finishedSpy.signalArguments[0][1]
verify(!!resultData)
compare(resultData.password, "")
@ -759,7 +760,7 @@ Item {
}
tryCompare(finishedSpy, "count", 1)
compare(finishedSpy.signalArguments[0][0], Onboarding.SecondaryFlow.LoginWithSeedphrase)
compare(finishedSpy.signalArguments[0][0], Onboarding.OnboardingFlow.LoginWithSeedphrase)
const resultData = finishedSpy.signalArguments[0][1]
verify(!!resultData)
compare(resultData.password, mockDriver.dummyNewPassword)
@ -851,7 +852,7 @@ Item {
// FINISH
tryCompare(finishedSpy, "count", 1)
compare(finishedSpy.signalArguments[0][0], Onboarding.SecondaryFlow.LoginWithSyncing)
compare(finishedSpy.signalArguments[0][0], Onboarding.OnboardingFlow.LoginWithSyncing)
const resultData = finishedSpy.signalArguments[0][1]
verify(!!resultData)
compare(resultData.password, "")
@ -924,7 +925,7 @@ Item {
// FINISH
tryCompare(finishedSpy, "count", 1)
compare(finishedSpy.signalArguments[0][0], Onboarding.SecondaryFlow.LoginWithKeycard)
compare(finishedSpy.signalArguments[0][0], Onboarding.OnboardingFlow.LoginWithKeycard)
const resultData = finishedSpy.signalArguments[0][1]
verify(!!resultData)
compare(resultData.password, "")
@ -950,7 +951,7 @@ Item {
}
function test_loginScreen(data) {
verify(!!controlUnderTest)
controlUnderTest.loginAccountsModel = loginAccountsModel
controlUnderTest.onboardingStore.loginAccountsModel = loginAccountsModel
controlUnderTest.biometricsAvailable = data.biometrics // both available _and_ enabled for this profile
controlUnderTest.restartFlow()
@ -1017,6 +1018,7 @@ Item {
compare(resultData.password, data.password)
// verify validation & pass error
console.log("---- passwords:", data.password, mockDriver.dummyNewPassword)
tryCompare(passwordInput, "hasError", data.password !== mockDriver.dummyNewPassword)
} else if (!!data.pin) { // keycard profile
mockDriver.keycardState = Onboarding.KeycardState.NotEmpty // happy path; keycard ready
@ -1053,7 +1055,7 @@ Item {
} else { // manual PIN
keyClickSequence(data.pin)
if (data.pin !== mockDriver.existingPin) {
expectFail(data.tag, "Wrong PIN entered, expected to fail to login")
// Everything will still be called as with a good pin, the wrong pin return is async
}
}
@ -1076,7 +1078,7 @@ Item {
}
function test_loginScreen_launchesExternalFlow(data) {
verify(!!controlUnderTest)
controlUnderTest.loginAccountsModel = loginAccountsModel
controlUnderTest.onboardingStore.loginAccountsModel = loginAccountsModel
controlUnderTest.restartFlow()
const page = getCurrentPage(controlUnderTest.stack, LoginScreen)
@ -1100,7 +1102,7 @@ Item {
function test_loginScreenLostKeycardSeedphraseLoginFlow() {
verify(!!controlUnderTest)
controlUnderTest.loginAccountsModel = loginAccountsModel
controlUnderTest.onboardingStore.loginAccountsModel = loginAccountsModel
controlUnderTest.biometricsAvailable = false
controlUnderTest.restartFlow()
@ -1174,7 +1176,7 @@ Item {
// FINISH
tryCompare(finishedSpy, "count", 1)
compare(finishedSpy.signalArguments[0][0], Onboarding.SecondaryFlow.LoginWithLostKeycardSeedphrase)
compare(finishedSpy.signalArguments[0][0], Onboarding.OnboardingFlow.LoginWithLostKeycardSeedphrase)
const resultData = finishedSpy.signalArguments[0][1]
verify(!!resultData)
compare(resultData.password, mockDriver.dummyNewPassword)
@ -1185,7 +1187,7 @@ Item {
function test_loginScreenLostKeycardCreateReplacementFlow() {
verify(!!controlUnderTest)
controlUnderTest.loginAccountsModel = loginAccountsModel
controlUnderTest.onboardingStore.loginAccountsModel = loginAccountsModel
controlUnderTest.biometricsAvailable = false
controlUnderTest.restartFlow()
@ -1260,7 +1262,7 @@ Item {
// FINISH
tryCompare(finishedSpy, "count", 1)
compare(finishedSpy.signalArguments[0][0], Onboarding.SecondaryFlow.LoginWithRestoredKeycard)
compare(finishedSpy.signalArguments[0][0], Onboarding.OnboardingFlow.LoginWithRestoredKeycard)
const resultData = finishedSpy.signalArguments[0][1]
verify(!!resultData)
compare(resultData.enableBiometrics, false)

View File

@ -33,7 +33,7 @@ public:
Login
};
enum class SecondaryFlow {
enum class OnboardingFlow {
Unknown,
CreateProfileWithPassword,
@ -50,6 +50,7 @@ public:
};
enum class LoginMethod {
Unknown,
Password,
Keycard,
};
@ -82,7 +83,7 @@ public:
private:
Q_ENUM(PrimaryFlow)
Q_ENUM(SecondaryFlow)
Q_ENUM(OnboardingFlow)
Q_ENUM(LoginMethod)
Q_ENUM(KeycardState)
Q_ENUM(ProgressState)

View File

@ -166,7 +166,7 @@ SQUtils.QObject {
CreateProfilePage {
onCreateProfileWithPasswordRequested: createNewProfileFlow.init()
onCreateProfileWithSeedphraseRequested: {
d.flow = Onboarding.SecondaryFlow.CreateProfileWithSeedphrase
d.flow = Onboarding.OnboardingFlow.CreateProfileWithSeedphrase
useRecoveryPhraseFlow.init(UseRecoveryPhraseFlow.Type.NewProfile)
}
onCreateProfileWithEmptyKeycardRequested: keycardCreateProfileFlow.init()
@ -183,7 +183,7 @@ SQUtils.QObject {
onLoginWithKeycardRequested: loginWithKeycardFlow.init()
onLoginWithSeedphraseRequested: {
d.flow = Onboarding.SecondaryFlow.LoginWithSeedphrase
d.flow = Onboarding.OnboardingFlow.LoginWithSeedphrase
useRecoveryPhraseFlow.init(UseRecoveryPhraseFlow.Type.Login)
}
}
@ -194,12 +194,12 @@ SQUtils.QObject {
KeycardLostPage {
onCreateReplacementKeycardRequested: {
d.flow = Onboarding.SecondaryFlow.LoginWithRestoredKeycard
d.flow = Onboarding.OnboardingFlow.LoginWithRestoredKeycard
keycardCreateReplacementFlow.init()
}
onUseProfileWithoutKeycardRequested: {
d.flow = Onboarding.SecondaryFlow.LoginWithLostKeycardSeedphrase
d.flow = Onboarding.OnboardingFlow.LoginWithLostKeycardSeedphrase
useRecoveryPhraseFlow.init(UseRecoveryPhraseFlow.Type.KeycardRecovery)
}
}
@ -213,7 +213,7 @@ SQUtils.QObject {
onFinished: (password) => {
root.setPasswordRequested(password)
d.flow = Onboarding.SecondaryFlow.CreateProfileWithPassword
d.flow = Onboarding.OnboardingFlow.CreateProfileWithPassword
d.pushOrSkipBiometricsPage()
}
}
@ -263,8 +263,8 @@ SQUtils.QObject {
onFinished: (withNewSeedphrase) => {
d.flow = withNewSeedphrase
? Onboarding.SecondaryFlow.CreateProfileWithKeycardNewSeedphrase
: Onboarding.SecondaryFlow.CreateProfileWithKeycardExistingSeedphrase
? Onboarding.OnboardingFlow.CreateProfileWithKeycardNewSeedphrase
: Onboarding.OnboardingFlow.CreateProfileWithKeycardExistingSeedphrase
d.pushOrSkipBiometricsPage()
}
@ -281,12 +281,12 @@ SQUtils.QObject {
root.syncProceedWithConnectionString(connectionString)
onLoginWithSeedphraseRequested: {
d.flow = Onboarding.SecondaryFlow.LoginWithSeedphrase
d.flow = Onboarding.OnboardingFlow.LoginWithSeedphrase
useRecoveryPhraseFlow.init(UseRecoveryPhraseFlow.Type.Login)
}
onFinished: {
d.flow = Onboarding.SecondaryFlow.LoginWithSyncing
d.flow = Onboarding.OnboardingFlow.LoginWithSyncing
d.pushOrSkipBiometricsPage()
}
}
@ -314,7 +314,7 @@ SQUtils.QObject {
onUnblockWithPukRequested: unblockWithPukFlow.init()
onFinished: {
d.flow = Onboarding.SecondaryFlow.LoginWithKeycard
d.flow = Onboarding.OnboardingFlow.LoginWithKeycard
d.pushOrSkipBiometricsPage()
}
}
@ -365,7 +365,7 @@ SQUtils.QObject {
root.loginRequested(root.loginScreen.selectedProfileKeyId,
Onboarding.LoginMethod.Keycard, { pin })
} else {
d.flow = Onboarding.SecondaryFlow.LoginWithKeycard
d.flow = Onboarding.OnboardingFlow.LoginWithKeycard
d.pushOrSkipBiometricsPage()
}
}

View File

@ -17,10 +17,6 @@ Page {
required property OnboardingStore onboardingStore
// [{keyUid:string, username:string, thumbnailImage:string, colorId:int, colorHash:var, order:int, keycardCreatedAccount:bool}]
// NB: this also decides whether we show the Login screen (if not empty), or the Onboarding
required property var loginAccountsModel
property bool biometricsAvailable: Qt.platform.os === Constants.mac
property bool isBiometricsLogin // FIXME should come from the loginAccountsModel for each profile separately?
signal biometricsRequested() // emitted when the user wants to try the biometrics prompt again
@ -33,7 +29,7 @@ Page {
signal shareUsageDataRequested(bool enabled)
// flow: Onboarding.SecondaryFlow
// flow: Onboarding.OnboardingFlow
signal finished(int flow, var data)
// -> "keyUid:string": User ID to login; "method:int": password or keycard (cf Onboarding.LoginMethod.*) enum;
@ -153,7 +149,7 @@ Page {
stackView: stack
loginAccountsModel: root.loginAccountsModel
loginAccountsModel: root.onboardingStore.loginAccountsModel
keycardState: root.onboardingStore.keycardState
pinSettingState: root.onboardingStore.pinSettingState
@ -220,22 +216,10 @@ Page {
function onAccountLoginError(error: string, wrongPassword: bool) {
const loginScreen = onboardingFlow.loginScreen
if (!error || !loginScreen || loginScreen.currentProfileIsKeycard)
if (!loginScreen)
return
let validationError
let detailedError
// SQLITE_NOTADB: "file is not a database"
if (error.includes("file is not a database") || wrongPassword) {
validationError = qsTr("Password incorrect. %1").arg("<a href='#password'>" + qsTr("Forgot password?") + "</a>")
detailedError = ""
} else {
validationError = qsTr("Login failed. %1").arg("<a href='#details'>" + qsTr("Show details.") + "</a>")
detailedError = error
}
loginScreen.setError(validationError, detailedError)
loginScreen.setAccountLoginError(error, wrongPassword)
}
// biometrics

View File

@ -14,9 +14,9 @@ Control {
id: root
required property int keycardState
property var tryToSetPinFunction: (pin) => { console.error("LoginKeycardBox::tryToSetPinFunction: IMPLEMENT ME"); return false }
required property int keycardRemainingPinAttempts
required property int keycardRemainingPukAttempts
property string loginError
required property bool isBiometricsLogin
required property bool biometricsSuccessful
@ -37,6 +37,12 @@ Control {
pinInputField.forceFocus()
}
function markAsWrongPin() {
d.wrongPin = true
pinInputField.statesInitialization()
pinInputField.forceFocus()
}
function setPin(pin: string) {
pinInputField.setPin(pin)
}
@ -106,14 +112,7 @@ Control {
onPinInputChanged: {
if (pinInput.length === 6) {
if (root.tryToSetPinFunction(pinInput)) {
root.loginRequested(pinInput)
d.wrongPin = false
} else {
d.wrongPin = true
pinInputField.statesInitialization()
pinInputField.forceFocus()
}
root.loginRequested(pinInput)
}
}
onPinEditedManually: {
@ -157,7 +156,7 @@ Control {
PropertyChanges {
target: infoText
color: Theme.palette.dangerColor1
text: qsTr("Oops this isnt a Keycard.<br>Remove card and insert a Keycard.")
text: qsTr("Oops this isn't a Keycard.<br>Remove card and insert a Keycard.")
}
},
State {
@ -212,6 +211,16 @@ Control {
text: qsTr("PIN incorrect. %n attempt(s) remaining.", "", root.keycardRemainingPinAttempts)
}
},
State {
// TODO this is a deadend. We should never end up here, but I still don't know what it should look like
name: "errorDuringLogin"
when: !!root.loginError
PropertyChanges {
target: infoText
color: Theme.palette.dangerColor1
text: qsTr("Error during login: %1").arg(root.loginError)
}
},
// exit states
State {
name: "notEmpty"

View File

@ -121,6 +121,35 @@ OnboardingPage {
passwordBox.detailedError = detailedError
}
// (password) login
function setAccountLoginError(error: string, wrongPassword: bool) {
if (!error) {
return
}
if (d.currentProfileIsKeycard) {
// Login with keycard
if (wrongPassword) {
keycardBox.markAsWrongPin()
} else {
keycardBox.loginError = error
}
return
}
// Login with password
if (wrongPassword) {
passwordBox.validationError = qsTr("Password incorrect. %1").arg("<a href='#password'>" + qsTr("Forgot password?") + "</a>")
passwordBox.detailedError = ""
} else {
passwordBox.validationError = qsTr("Login failed. %1").arg("<a href='#details'>" + qsTr("Show details.") + "</a>")
passwordBox.detailedError = error
}
passwordBox.clear()
passwordBox.forceActiveFocus()
}
padding: 40
contentItem: Item {
@ -202,7 +231,6 @@ OnboardingPage {
biometricsSuccessful: d.biometricsSuccessful
biometricsFailed: d.biometricsFailed
keycardState: root.keycardState
tryToSetPinFunction: root.tryToSetPinFunction
keycardRemainingPinAttempts: root.keycardRemainingPinAttempts
keycardRemainingPukAttempts: root.keycardRemainingPukAttempts
onUnblockWithSeedphraseRequested: root.unblockWithSeedphraseRequested()

View File

@ -8,6 +8,7 @@ QtObject {
id: root
signal appLoaded
readonly property QtObject d: StatusQUtils.QObject {
id: d
readonly property var onboardingModuleInst: onboardingModule
@ -18,6 +19,8 @@ QtObject {
}
}
readonly property var loginAccountsModel: d.onboardingModuleInst.loginAccountsModel
// keycard
readonly property int keycardState: d.onboardingModuleInst.keycardState // cf. enum Onboarding.KeycardState
readonly property int pinSettingState: d.onboardingModuleInst.pinSettingState // cf. enum Onboarding.ProgressState
@ -30,6 +33,10 @@ QtObject {
return d.onboardingModuleInst.finishOnboardingFlow(flow, JSON.stringify(data))
}
function loginRequested(keyUid: string, method: int, data: Object) { // -> void
d.onboardingModuleInst.loginRequested(keyUid, method, JSON.stringify(data))
}
function setPin(pin: string) {
d.onboardingModuleInst.setPin(pin)
}

View File

@ -193,7 +193,6 @@ StatusWindow {
}
startupOnboardingLoader.item.unload()
startupOnboardingLoader.active = false
onboardingStoreLoader.active = false
Theme.changeTheme(localAppSettings.theme, systemPalette.isCurrentSystemThemeDark())
Theme.changeFontSize(localAccountSensitiveSettings.fontSize)
@ -407,15 +406,6 @@ StatusWindow {
}
}
Loader {
id: onboardingStoreLoader
active: featureFlagsStore.onboardingV2Enabled
sourceComponent: OnboardingStore {
onAppLoaded: moveToAppMain()
}
}
Loader {
id: startupOnboardingLoader
anchors.fill: parent
@ -443,32 +433,33 @@ StatusWindow {
id: onboardingV2
Onboarding2.OnboardingLayout {
id: onboardingLayout
objectName: "startupOnboardingLayout"
anchors.fill: parent
// TODO implement those two
loginAccountsModel: ListModel {}
isBiometricsLogin: false
networkChecksEnabled: true
biometricsAvailable: Qt.platform.os === Constants.mac
onboardingStore: onboardingStoreLoader.item
onboardingStore: onboardingStore
onFinished: (flow, data) => {
console.warn("!!! ONBOARDING FINISHED; flow:", flow, "; data:", JSON.stringify(data))
let error = onboardingStoreLoader.item.finishOnboardingFlow(flow, data)
const error = onboardingStore.finishOnboardingFlow(flow, data)
if (error != "") {
// We should never be here since everything should be validated already
console.error("!!! ONBOARDING FINISHED WITH ERROR:", error)
// TODO show error
return
}
console.warn("!!! Onboarding completed!")
stack.clear()
stack.push(splashScreenV2, { runningProgressAnimation: true })
}
onLoginRequested: function (keyUid, method, data) {
stack.push(splashScreenV2, { runningProgressAnimation: true })
onboardingStore.loginRequested(keyUid, method, data)
}
onShareUsageDataRequested: {
applicationWindow.metricsStore.toggleCentralizedMetrics(enabled)
if (enabled) {
@ -476,6 +467,14 @@ StatusWindow {
}
}
onCurrentPageNameChanged: Global.addCentralizedMetricIfEnabled("navigation", {viewId: currentPageName})
OnboardingStore {
id: onboardingStore
onAppLoaded: moveToAppMain()
onAccountLoginError: function (error, wrongPassword) {
onboardingLayout.stack.pop()
}
}
}
}

@ -1 +1 @@
Subproject commit 84f3577ca011b094578643d5f206848cbb20178e
Subproject commit 75e09c9ec1911b1422ff1a4371732500ae0ad60a