From bf5de4087e984bf2ff33fe6d47c0cc3523975dd7 Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Tue, 4 Feb 2025 10:08:13 -0500 Subject: [PATCH] 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 --- src/app/boot/app_controller.nim | 7 +- src/app/modules/onboarding/controller.nim | 61 ++++++++- src/app/modules/onboarding/io_interface.nim | 16 ++- src/app/modules/onboarding/module.nim | 119 ++++++++++++++---- src/app/modules/onboarding/view.nim | 24 +++- .../service/accounts/dto/accounts.nim | 7 ++ src/app_service/service/accounts/service.nim | 8 ++ .../service/keycardV2/async_tasks.nim | 17 +++ src/app_service/service/keycardV2/service.nim | 43 +++++-- storybook/pages/OnboardingLayoutPage.qml | 3 +- .../qmlTests/tests/tst_OnboardingLayout.qml | 32 ++--- ui/StatusQ/src/onboarding/enums.h | 5 +- .../AppLayouts/Onboarding2/OnboardingFlow.qml | 22 ++-- .../Onboarding2/OnboardingLayout.qml | 24 +--- .../components/LoginKeycardBox.qml | 29 +++-- .../Onboarding2/pages/LoginScreen.qml | 30 ++++- .../Onboarding2/stores/OnboardingStore.qml | 7 ++ ui/main.qml | 35 +++--- vendor/status-keycard-go | 2 +- 19 files changed, 369 insertions(+), 122 deletions(-) diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index ce7cf52140d..8cd470bb977 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -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() diff --git a/src/app/modules/onboarding/controller.nim b/src/app/modules/onboarding/controller.nim index 5f1c438fb1f..dcd43d37a78 100644 --- a/src/app/modules/onboarding/controller.nim +++ b/src/app/modules/onboarding/controller.nim @@ -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, + ) diff --git a/src/app/modules/onboarding/io_interface.nim b/src/app/modules/onboarding/io_interface.nim index db0d8fae1c3..a928ca6c239 100644 --- a/src/app/modules/onboarding/io_interface.nim +++ b/src/app/modules/onboarding/io_interface.nim @@ -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 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.} = diff --git a/src/app/modules/onboarding/module.nim b/src/app/modules/onboarding/module.nim index bed5752df06..ed3a1e936ec 100644 --- a/src/app/modules/onboarding/module.nim +++ b/src/app/modules/onboarding/module.nim @@ -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.. 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) = diff --git a/src/app_service/service/keycardV2/async_tasks.nim b/src/app_service/service/keycardV2/async_tasks.nim index 2f184224b40..d8d35eb5606 100644 --- a/src/app_service/service/keycardV2/async_tasks.nim +++ b/src/app_service/service/keycardV2/async_tasks.nim @@ -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, + }) diff --git a/src/app_service/service/keycardV2/service.nim b/src/app_service/service/keycardV2/service.nim index 4cbf35c9f1c..cc2603bc32c 100644 --- a/src/app_service/service/keycardV2/service.nim +++ b/src/app_service/service/keycardV2/service.nim @@ -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)) \ No newline at end of file diff --git a/storybook/pages/OnboardingLayoutPage.qml b/storybook/pages/OnboardingLayoutPage.qml index ab6770f45e6..7dfa550d577 100644 --- a/storybook/pages/OnboardingLayoutPage.qml +++ b/storybook/pages/OnboardingLayoutPage.qml @@ -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() diff --git a/storybook/qmlTests/tests/tst_OnboardingLayout.qml b/storybook/qmlTests/tests/tst_OnboardingLayout.qml index 8af1466eae8..9b91956fdb7 100644 --- a/storybook/qmlTests/tests/tst_OnboardingLayout.qml +++ b/storybook/qmlTests/tests/tst_OnboardingLayout.qml @@ -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) diff --git a/ui/StatusQ/src/onboarding/enums.h b/ui/StatusQ/src/onboarding/enums.h index f83d50c9a97..a5e22e14c9a 100644 --- a/ui/StatusQ/src/onboarding/enums.h +++ b/ui/StatusQ/src/onboarding/enums.h @@ -33,7 +33,7 @@ class OnboardingEnums: public QObject Login }; - enum class SecondaryFlow { + enum class OnboardingFlow { Unknown, CreateProfileWithPassword, @@ -50,6 +50,7 @@ class OnboardingEnums: public QObject }; enum class LoginMethod { + Unknown, Password, Keycard, }; @@ -82,7 +83,7 @@ class OnboardingEnums: public QObject private: Q_ENUM(PrimaryFlow) - Q_ENUM(SecondaryFlow) + Q_ENUM(OnboardingFlow) Q_ENUM(LoginMethod) Q_ENUM(KeycardState) Q_ENUM(ProgressState) diff --git a/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml b/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml index 7d393cdefcf..dca57c2ce55 100644 --- a/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml +++ b/ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml @@ -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() } } diff --git a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml index 0ff77a1ecea..3b9444334f7 100644 --- a/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml +++ b/ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml @@ -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("" + qsTr("Forgot password?") + "") - detailedError = "" - } else { - validationError = qsTr("Login failed. %1").arg("" + qsTr("Show details.") + "") - detailedError = error - } - - loginScreen.setError(validationError, detailedError) + loginScreen.setAccountLoginError(error, wrongPassword) } // biometrics diff --git a/ui/app/AppLayouts/Onboarding2/components/LoginKeycardBox.qml b/ui/app/AppLayouts/Onboarding2/components/LoginKeycardBox.qml index 44b40202f22..a538280ab37 100644 --- a/ui/app/AppLayouts/Onboarding2/components/LoginKeycardBox.qml +++ b/ui/app/AppLayouts/Onboarding2/components/LoginKeycardBox.qml @@ -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 isn’t a Keycard.
Remove card and insert a Keycard.") + text: qsTr("Oops this isn't a Keycard.
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" diff --git a/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml b/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml index b65e35a5a41..415b848dba8 100644 --- a/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml +++ b/ui/app/AppLayouts/Onboarding2/pages/LoginScreen.qml @@ -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("" + qsTr("Forgot password?") + "") + passwordBox.detailedError = "" + } else { + passwordBox.validationError = qsTr("Login failed. %1").arg("" + qsTr("Show details.") + "") + 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() diff --git a/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml b/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml index 0775b46f611..929dc761446 100644 --- a/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml +++ b/ui/app/AppLayouts/Onboarding2/stores/OnboardingStore.qml @@ -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) } diff --git a/ui/main.qml b/ui/main.qml index b6cb8c92203..0221743298d 100644 --- a/ui/main.qml +++ b/ui/main.qml @@ -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() + } + } } } diff --git a/vendor/status-keycard-go b/vendor/status-keycard-go index 84f3577ca01..75e09c9ec19 160000 --- a/vendor/status-keycard-go +++ b/vendor/status-keycard-go @@ -1 +1 @@ -Subproject commit 84f3577ca011b094578643d5f206848cbb20178e +Subproject commit 75e09c9ec1911b1422ff1a4371732500ae0ad60a