Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate new logins flows happy paths #17137

Merged
merged 4 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/app/boot/app_controller.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
61 changes: 57 additions & 4 deletions src/app/modules/onboarding/controller.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -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,
)
16 changes: 14 additions & 2 deletions src/app/modules/onboarding/io_interface.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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 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 onKeycardExportKeysSuccess*(self: AccessInterface, exportedKeys: KeycardExportedKeysDto) {.base.} =
method onAccountLoginError*(self: AccessInterface, error: string) {.base.} =
raise newException(ValueError, "No implementation available")

method exportRecoverKeys*(self: AccessInterface) {.base.} =
Expand Down
119 changes: 96 additions & 23 deletions src/app/modules/onboarding/module.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import NimQml, chronicles, json
import NimQml, chronicles, json, strutils
import logging

import io_interface
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -43,7 +49,8 @@ type
viewVariant: QVariant
controller: Controller
localPairingStatus: LocalPairingStatus
currentFlow: SecondaryFlow
loginFlow: LoginMethod
onboardingFlow: OnboardingFlow
exportedKeys: KeycardExportedKeysDto

proc newModule*[T](
Expand All @@ -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,
Expand Down Expand Up @@ -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) =
Expand Down Expand Up @@ -118,26 +141,26 @@ 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
let seedPhrase = data["seedphrase"].str

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(
Expand All @@ -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(
Expand All @@ -157,53 +180,78 @@ 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,
self.exportedKeys,
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)
Expand All @@ -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)
Expand All @@ -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()
Expand Down
Loading