From afa7928baecc218409bfbb10a4be74466ca618f0 Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Wed, 31 Aug 2022 19:09:07 +0200 Subject: [PATCH] feat(@desktop/keycard): keycard settings -> migrate keypair flow - Added flow which covers `Setup a new Keycard with an existing account` from the keycard settings part (though two sub-flows there are missing, `Unlock Keycard` and `Authentication` cause we don't have them yet). - Updated factory reset flow (part of shared module) that it can read and display metadata from a keycard if they are set, with this update this flow is almost complete, we are missing `Unlock Keycard` flow for it as well. --- src/app/global/local_account_settings.nim | 2 +- .../profile_section/keycard/controller.nim | 9 +- .../profile_section/keycard/io_interface.nim | 13 + .../main/profile_section/keycard/module.nim | 40 +- .../main/profile_section/keycard/view.nim | 17 +- .../modules/main/profile_section/module.nim | 3 +- .../keycard_popup/controller.nim | 183 ++++++- .../internal/create_pin_state.nim | 23 + .../internal/enter_pin_state.nim | 59 +++ .../internal/enter_seed_phrase_state.nim | 42 ++ ..._confirmation_displayed_metadata_state.nim | 25 + .../factory_reset_confirmation_state.nim | 13 +- .../internal/factory_reset_success_state.nim | 10 + .../internal/insert_keycard_state.nim | 14 +- .../key_pair_migrate_failure_state.nim | 13 + .../key_pair_migrate_success_state.nim | 16 + .../internal/keycard_empty_metadata_state.nim | 20 + .../internal/keycard_empty_state.nim | 3 +- .../internal/keycard_inserted_state.nim | 19 + .../keycard_metadata_display_state.nim | 20 + .../internal/keycard_not_empty_state.nim | 22 + .../max_pin_retries_reached_state.nim | 20 + .../internal/migrating_key_pair_state.nim | 38 ++ .../internal/not_keycard_state.nim | 5 +- .../keycard_popup/internal/pin_set_state.nim | 21 + .../internal/pin_verified_state.nim | 20 + .../internal/plugin_reader_state.nim | 11 +- .../internal/reading_keycard_state.nim | 23 +- .../internal/recognized_keycard_state.nim | 14 +- .../internal/repeat_pin_state.nim | 34 ++ .../internal/seed_phrase_display_state.nim | 16 + .../seed_phrase_enter_words_state.nim | 31 ++ .../select_existing_key_pair_state.nim | 21 + .../keycard_popup/internal/state.nim | 35 +- .../keycard_popup/internal/state_factory.nim | 187 +++++++- .../keycard_popup/internal/state_wrapper.nim | 6 +- .../internal/wrong_pin_state.nim | 62 +++ .../internal/wrong_seed_phrase_state.nim | 40 ++ .../keycard_popup/io_interface.nim | 56 ++- .../keycard_popup/models/key_pair_item.nim | 82 ++++ .../keycard_popup/models/key_pair_model.nim | 90 ++++ .../models/key_pair_selected_item.nim | 75 +++ .../shared_modules/keycard_popup/module.nim | 214 ++++++++- .../shared_modules/keycard_popup/view.nim | 104 +++- .../service/wallet_account/dto.nim | 6 + ui/app/AppLayouts/Profile/ProfileLayout.qml | 4 +- .../Profile/stores/KeycardStore.qml | 3 + .../AppLayouts/Profile/views/KeycardView.qml | 39 +- .../shared/popups/keycard/KeycardPopup.qml | 384 +++++++++++++-- .../popups/keycard/helpers/KeyPairItem.qml | 117 +++++ .../popups/keycard/helpers/KeyPairList.qml | 77 +++ .../keycard/helpers/KeyPairUnknownItem.qml | 170 +++++++ .../popups/keycard/helpers/KeycardImage.qml | 93 ++++ .../popups/keycard/states/EnterSeedPhrase.qml | 328 +++++++++++++ .../keycard/states/EnterSeedPhraseWords.qml | 168 +++++++ .../keycard/states/KeycardConfirmation.qml | 101 +++- .../popups/keycard/states/KeycardInit.qml | 447 ++++++++++++++++-- .../popups/keycard/states/KeycardPin.qml | 292 ++++++++++++ .../popups/keycard/states/SeedPhrase.qml | 125 +++++ .../popups/keycard/states/SelectKeyPair.qml | 109 +++++ ui/imports/shared/stores/BIP39_en.qml | 12 +- ui/imports/utils/Constants.qml | 37 ++ 62 files changed, 4123 insertions(+), 160 deletions(-) create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/create_pin_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/enter_seed_phrase_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/factory_reset_confirmation_displayed_metadata_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/key_pair_migrate_failure_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/key_pair_migrate_success_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/keycard_empty_metadata_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/keycard_inserted_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/keycard_metadata_display_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/keycard_not_empty_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/max_pin_retries_reached_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/migrating_key_pair_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/pin_set_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/pin_verified_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/repeat_pin_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/seed_phrase_display_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/seed_phrase_enter_words_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/select_existing_key_pair_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/wrong_pin_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/internal/wrong_seed_phrase_state.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/models/key_pair_item.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/models/key_pair_model.nim create mode 100644 src/app/modules/shared_modules/keycard_popup/models/key_pair_selected_item.nim create mode 100644 ui/imports/shared/popups/keycard/helpers/KeyPairItem.qml create mode 100644 ui/imports/shared/popups/keycard/helpers/KeyPairList.qml create mode 100644 ui/imports/shared/popups/keycard/helpers/KeyPairUnknownItem.qml create mode 100644 ui/imports/shared/popups/keycard/helpers/KeycardImage.qml create mode 100644 ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml create mode 100644 ui/imports/shared/popups/keycard/states/EnterSeedPhraseWords.qml create mode 100644 ui/imports/shared/popups/keycard/states/KeycardPin.qml create mode 100644 ui/imports/shared/popups/keycard/states/SeedPhrase.qml create mode 100644 ui/imports/shared/popups/keycard/states/SelectKeyPair.qml diff --git a/src/app/global/local_account_settings.nim b/src/app/global/local_account_settings.nim index 5a59688c1f..38d3e141a0 100644 --- a/src/app/global/local_account_settings.nim +++ b/src/app/global/local_account_settings.nim @@ -7,7 +7,7 @@ const LS_KEY_STORE_TO_KEYCHAIN* = "storeToKeychain" const DEFAULT_STORE_TO_KEYCHAIN = "notNow" # Local Account Settings values: const LS_VALUE_STORE* = "store" -const LS_VALUE_NOTNOW* = "notNow" +const LS_VALUE_NOT_NOW* = "notNow" const LS_VALUE_NEVER* = "never" QtObject: diff --git a/src/app/modules/main/profile_section/keycard/controller.nim b/src/app/modules/main/profile_section/keycard/controller.nim index 7997120b68..9c1c4f8b1b 100644 --- a/src/app/modules/main/profile_section/keycard/controller.nim +++ b/src/app/modules/main/profile_section/keycard/controller.nim @@ -4,6 +4,8 @@ import io_interface import ../../../../core/eventemitter +import ../../../shared_modules/keycard_popup/io_interface as keycard_shared_module + logScope: topics = "profile-section-keycard-module-controller" @@ -23,4 +25,9 @@ proc delete*(self: Controller) = discard proc init*(self: Controller) = - discard \ No newline at end of file + self.events.on(SignalSharedKeycarModuleFlowTerminated) do(e: Args): + let args = SharedKeycarModuleFlowTerminatedArgs(e) + self.delegate.onSharedKeycarModuleFlowTerminated(args.lastStepInTheCurrentFlow) + + self.events.on(SignalSharedKeycarModuleDisplayPopup) do(e: Args): + self.delegate.onDisplayKeycardSharedModuleFlow() \ No newline at end of file diff --git a/src/app/modules/main/profile_section/keycard/io_interface.nim b/src/app/modules/main/profile_section/keycard/io_interface.nim index 86445bc6f8..7737b50167 100644 --- a/src/app/modules/main/profile_section/keycard/io_interface.nim +++ b/src/app/modules/main/profile_section/keycard/io_interface.nim @@ -16,6 +16,19 @@ method isLoaded*(self: AccessInterface): bool {.base.} = method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} = raise newException(ValueError, "No implementation available") +method getKeycardSharedModule*(self: AccessInterface): QVariant {.base.} = + raise newException(ValueError, "No implementation available") + +method onDisplayKeycardSharedModuleFlow*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method onSharedKeycarModuleFlowTerminated*(self: AccessInterface, lastStepInTheCurrentFlow: bool) {.base.} = + raise newException(ValueError, "No implementation available") + +method runSetupKeycardPopup*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + + # View Delegate Interface # Delegate for the view must be declared here due to use of QtObject and multi # inheritance, which is not well supported in Nim. diff --git a/src/app/modules/main/profile_section/keycard/module.nim b/src/app/modules/main/profile_section/keycard/module.nim index 23f95ce2e8..aa2fe34537 100644 --- a/src/app/modules/main/profile_section/keycard/module.nim +++ b/src/app/modules/main/profile_section/keycard/module.nim @@ -6,8 +6,12 @@ import ../io_interface as delegate_interface import ../../../../core/eventemitter import ../../../../../app_service/service/keycard/service as keycard_service +import ../../../../../app_service/service/privacy/service as privacy_service +import ../../../../../app_service/service/accounts/service as accounts_service import ../../../../../app_service/service/wallet_account/service as wallet_account_service +import ../../../shared_modules/keycard_popup/module as keycard_shared_module + export io_interface logScope: @@ -22,16 +26,23 @@ type moduleLoaded: bool events: EventEmitter keycardService: keycard_service.Service + privacyService: privacy_service.Service + accountsService: accounts_service.Service walletAccountService: wallet_account_service.Service + keycardSharedModule: keycard_shared_module.AccessInterface proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, keycardService: keycard_service.Service, + privacyService: privacy_service.Service, + accountsService: accounts_service.Service, walletAccountService: wallet_account_service.Service): Module = result = Module() result.delegate = delegate result.events = events result.keycardService = keycardService + result.privacyService = privacyService + result.accountsService = accountsService result.walletAccountService = walletAccountService result.view = view.newView(result) result.viewVariant = newQVariant(result.view) @@ -42,6 +53,8 @@ method delete*(self: Module) = self.view.delete self.viewVariant.delete self.controller.delete + if not self.keycardSharedModule.isNil: + self.keycardSharedModule.delete method load*(self: Module) = self.controller.init() @@ -55,4 +68,29 @@ method viewDidLoad*(self: Module) = self.delegate.profileModuleDidLoad() method getModuleAsVariant*(self: Module): QVariant = - return self.viewVariant \ No newline at end of file + return self.viewVariant + +method getKeycardSharedModule*(self: Module): QVariant = + return self.keycardSharedModule.getModuleAsVariant() + +proc createSharedKeycardModule(self: Module) = + self.keycardSharedModule = keycard_shared_module.newModule[Module](self, self.events, self.keycardService, + self.privacyService, self.accountsService, self.walletAccountService) + +proc isSharedKeycardModuleFlowRunning(self: Module): bool = + return not self.keycardSharedModule.isNil + +method onSharedKeycarModuleFlowTerminated*(self: Module, lastStepInTheCurrentFlow: bool) = + if self.isSharedKeycardModuleFlowRunning(): + self.view.emitDestroyKeycardSharedModuleFlow() + self.keycardSharedModule.delete + self.keycardSharedModule = nil + +method runSetupKeycardPopup*(self: Module) = + self.createSharedKeycardModule() + if self.keycardSharedModule.isNil: + return + self.keycardSharedModule.runFlow(keycard_shared_module.FlowType.SetupNewKeycard) + +method onDisplayKeycardSharedModuleFlow*(self: Module) = + self.view.emitDisplayKeycardSharedModuleFlow() \ No newline at end of file diff --git a/src/app/modules/main/profile_section/keycard/view.nim b/src/app/modules/main/profile_section/keycard/view.nim index abc3a3e685..a7c43ee3b2 100644 --- a/src/app/modules/main/profile_section/keycard/view.nim +++ b/src/app/modules/main/profile_section/keycard/view.nim @@ -17,4 +17,19 @@ QtObject: proc load*(self: View) = self.delegate.viewDidLoad() - \ No newline at end of file + + proc getKeycardSharedModule(self: View): QVariant {.slot.} = + return self.delegate.getKeycardSharedModule() + QtProperty[QVariant] keycardSharedModule: + read = getKeycardSharedModule + + proc displayKeycardSharedModuleFlow*(self: View) {.signal.} + proc emitDisplayKeycardSharedModuleFlow*(self: View) = + self.displayKeycardSharedModuleFlow() + + proc destroyKeycardSharedModuleFlow*(self: View) {.signal.} + proc emitDestroyKeycardSharedModuleFlow*(self: View) = + self.destroyKeycardSharedModuleFlow() + + proc runSetupKeycardPopup*(self: View) {.slot.} = + self.delegate.runSetupKeycardPopup() \ No newline at end of file diff --git a/src/app/modules/main/profile_section/module.nim b/src/app/modules/main/profile_section/module.nim index c131dc366e..8bb313bc96 100644 --- a/src/app/modules/main/profile_section/module.nim +++ b/src/app/modules/main/profile_section/module.nim @@ -99,7 +99,8 @@ proc newModule*(delegate: delegate_interface.AccessInterface, result, events, settingsService, ensService, walletAccountService, networkService ) result.communitiesModule = communities_module.newModule(result, communityService) - result.keycardModule = keycard_module.newModule(result, events, keycardService, walletAccountService) + result.keycardModule = keycard_module.newModule(result, events, keycardService, privacyService, accountsService, + walletAccountService) singletonInstance.engine.setRootContextProperty("profileSectionModule", result.viewVariant) diff --git a/src/app/modules/shared_modules/keycard_popup/controller.nim b/src/app/modules/shared_modules/keycard_popup/controller.nim index fb0f794496..d92b68187d 100644 --- a/src/app/modules/shared_modules/keycard_popup/controller.nim +++ b/src/app/modules/shared_modules/keycard_popup/controller.nim @@ -6,6 +6,9 @@ import ../../../global/global_singleton import ../../../core/signals/types import ../../../core/eventemitter import ../../../../app_service/service/keycard/service as keycard_service +import ../../../../app_service/service/privacy/service as privacy_service +import ../../../../app_service/service/accounts/service as accounts_service +import ../../../../app_service/service/wallet_account/service as wallet_account_service logScope: topics = "keycard-popup-controller" @@ -15,21 +18,46 @@ type delegate: io_interface.AccessInterface events: EventEmitter keycardService: keycard_service.Service + privacyService: privacy_service.Service + accountsService: accounts_service.Service + walletAccountService: wallet_account_service.Service connectionIds: seq[UUID] tmpKeycardContainsMetadata: bool + tmpPin: string + tmpPinMatch: bool + tmpPassword: string + tmpKeyUid: string + tmpSelectedKeyPairIsProfile: bool + tmpSelectedKeyPairName: string + tmpSelectedKeyPairWalletPaths: seq[string] + tmpSeedPhrase: string + tmpSeedPhraseLength: int proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter, - keycardService: keycard_service.Service): + keycardService: keycard_service.Service, + privacyService: privacy_service.Service, + accountsService: accounts_service.Service, + walletAccountService: wallet_account_service.Service): Controller = result = Controller() result.delegate = delegate result.events = events result.keycardService = keycardService + result.privacyService = privacyService + result.accountsService = accountsService + result.walletAccountService = walletAccountService result.tmpKeycardContainsMetadata = false + result.tmpPinMatch = false + result.tmpSeedPhraseLength = 0 + result.tmpSelectedKeyPairIsProfile = false + +proc disconnect*(self: Controller) = + for id in self.connectionIds: + self.events.disconnect(id) proc delete*(self: Controller) = - discard + self.disconnect() proc init*(self: Controller) = let handlerId = self.events.onWithUUID(SignalKeycardResponse) do(e: Args): @@ -37,9 +65,8 @@ proc init*(self: Controller) = self.delegate.onKeycardResponse(args.flowType, args.flowEvent) self.connectionIds.add(handlerId) -proc disconnect*(self: Controller) = - for id in self.connectionIds: - self.events.disconnect(id) +proc getKeycardData*(self: Controller): string = + return self.delegate.getKeycardData() proc setKeycardData*(self: Controller, value: string) = self.delegate.setKeycardData(value) @@ -50,6 +77,83 @@ proc containsMetadata*(self: Controller): bool = proc setContainsMetadata*(self: Controller, value: bool) = self.tmpKeycardContainsMetadata = value +proc setPin*(self: Controller, value: string) = + self.tmpPin = value + +proc getPin*(self: Controller): string = + return self.tmpPin + +proc setPinMatch*(self: Controller, value: bool) = + self.tmpPinMatch = value + +proc getPinMatch*(self: Controller): bool = + return self.tmpPinMatch + +proc setPassword*(self: Controller, value: string) = + self.tmpPassword = value + +proc getPassword*(self: Controller): string = + return self.tmpPassword + +proc setKeyUid*(self: Controller, value: string) = + self.tmpKeyUid = value + +proc setSelectedKeyPairIsProfile*(self: Controller, value: bool) = + self.tmpSelectedKeyPairIsProfile = value + +proc getSelectedKeyPairIsProfile*(self: Controller): bool = + return self.tmpSelectedKeyPairIsProfile + +proc setSelectedKeyPairName*(self: Controller, value: string) = + self.tmpSelectedKeyPairName = value + +proc getSelectedKeyPairName*(self: Controller): string = + return self.tmpSelectedKeyPairName + +proc setSelectedKeyPairWalletPaths*(self: Controller, paths: seq[string]) = + self.tmpSelectedKeyPairWalletPaths = paths + +proc getSelectedKeyPairWalletPaths*(self: Controller): seq[string] = + return self.tmpSelectedKeyPairWalletPaths + +proc setSeedPhrase*(self: Controller, value: string) = + let words = value.split(" ") + self.tmpSeedPhrase = value + self.tmpSeedPhraseLength = words.len + +proc getSeedPhrase*(self: Controller): string = + return self.tmpSeedPhrase + +proc getSeedPhraseLength*(self: Controller): int = + return self.tmpSeedPhraseLength + +proc validSeedPhrase*(self: Controller, seedPhrase: string): bool = + let err = self.accountsService.validateMnemonic(seedPhrase) + return err.len == 0 + +proc seedPhraseRefersToLoggedInUser*(self: Controller, seedPhrase: string): bool = + let acc = self.accountsService.createAccountFromMnemonic(seedPhrase) + return acc.keyUid == singletonInstance.userProfile.getAddress() + +proc verifyPassword*(self: Controller, password: string): bool = + return self.accountsService.verifyPassword(password) + +proc convertToKeycardAccount*(self: Controller, password: string): bool = + singletonInstance.localAccountSettings.setStoreToKeychainValue(LS_VALUE_NOT_NOW) + return self.accountsService.convertToKeycardAccount(self.tmpKeyUid, password) + +proc getLoggedInAccount*(self: Controller): AccountDto = + return self.accountsService.getLoggedInAccount() + +proc getCurrentKeycardServiceFlow*(self: Controller): keycard_service.KCSFlowType = + return self.keycardService.getCurrentFlow() + +proc getLastReceivedKeycardData*(self: Controller): tuple[flowType: string, flowEvent: KeycardEvent] = + return self.keycardService.getLastReceivedKeycardData() + +proc setMetadataFromKeycard*(self: Controller, cardMetadata: CardMetadata) = + self.delegate.setKeyPairStoredOnKeycard(cardMetadata) + proc cancelCurrentFlow(self: Controller) = self.keycardService.cancelCurrentFlow() # in most cases we're running another flow after canceling the current one, @@ -64,9 +168,76 @@ proc runGetMetadataFlow*(self: Controller) = self.cancelCurrentFlow() self.keycardService.startGetMetadataFlow() +proc runStoreMetadataFlow*(self: Controller, cardName: string, pin: string, walletPaths: seq[string]) = + self.cancelCurrentFlow() + self.keycardService.startStoreMetadataFlow(cardName, pin, walletPaths) + +proc runLoadAccountFlow*(self: Controller, factoryReset = false) = + self.cancelCurrentFlow() + self.keycardService.startLoadAccountFlow(factoryReset) + proc resumeCurrentFlowLater*(self: Controller) = self.keycardService.resumeCurrentFlowLater() +proc readyToDisplayPopup*(self: Controller) = + self.events.emit(SignalSharedKeycarModuleDisplayPopup, Args()) + proc terminateCurrentFlow*(self: Controller, lastStepInTheCurrentFlow: bool) = let data = SharedKeycarModuleFlowTerminatedArgs(lastStepInTheCurrentFlow: lastStepInTheCurrentFlow) - self.events.emit(SignalSharedKeycarModuleFlowTerminated, data) \ No newline at end of file + self.events.emit(SignalSharedKeycarModuleFlowTerminated, data) + +proc getWalletAccounts*(self: Controller): seq[wallet_account_service.WalletAccountDto] = + if self.walletAccountService.isNil: + debug "walletAccountService doesn't meant to be used from the context it's used, check the context shared popup module is used" + return + return self.walletAccountService.fetchAccounts() + +proc getBalanceForAddress*(self: Controller, address: string): float64 = + if self.walletAccountService.isNil: + debug "walletAccountService doesn't meant to be used from the context it's used, check the context shared popup module is used" + return + return self.walletAccountService.fetchBalanceForAddress(address) + +proc enterKeycardPin*(self: Controller, pin: string) = + self.keycardService.enterPin(pin) + +proc storePinToKeycard*(self: Controller, pin: string, puk: string) = + self.keycardService.storePin(pin, puk) + +proc storeSeedPhraseToKeycard*(self: Controller, seedPhraseLength: int, seedPhrase: string) = + self.keycardService.storeSeedPhrase(seedPhraseLength, seedPhrase) + +proc generateRandomPUK*(self: Controller): string = + return self.keycardService.generateRandomPUK() + +proc isMnemonicBackedUp*(self: Controller): bool = + if self.privacyService.isNil: + debug "privacyService doesn't meant to be used from the context it's used, check the context shared popup module is used" + return + return self.privacyService.isMnemonicBackedUp() + +proc getMnemonic*(self: Controller): string = + if self.privacyService.isNil: + debug "privacyService doesn't meant to be used from the context it's used, check the context shared popup module is used" + return + return self.privacyService.getMnemonic() + +proc removeMnemonic*(self: Controller) = + if self.privacyService.isNil: + debug "privacyService doesn't meant to be used from the context it's used, check the context shared popup module is used" + return + self.privacyService.removeMnemonic() + +proc getMnemonicWordAtIndex*(self: Controller, index: int): string = + if self.privacyService.isNil: + debug "privacyService doesn't meant to be used from the context it's used, check the context shared popup module is used" + return + return self.privacyService.getMnemonicWordAtIndex(index) + +proc loggedInUserUsesBiometricLogin*(self: Controller): bool = + if(not defined(macosx)): + return false + let value = singletonInstance.localAccountSettings.getStoreToKeychainValue() + if (value != LS_VALUE_STORE): + return false + return true \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/create_pin_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/create_pin_state.nim new file mode 100644 index 0000000000..453fa98925 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/create_pin_state.nim @@ -0,0 +1,23 @@ +type + CreatePinState* = ref object of State + +proc newCreatePinState*(flowType: FlowType, backState: State): CreatePinState = + result = CreatePinState() + result.setup(flowType, StateType.CreatePin, backState) + +proc delete*(self: CreatePinState) = + self.State.delete + +method executeBackCommand*(self: CreatePinState, controller: Controller) = + controller.setPin("") + controller.setPinMatch(false) + +method executeSecondaryCommand*(self: CreatePinState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method getNextTertiaryState*(self: CreatePinState, controller: Controller): State = + if self.flowType == FlowType.SetupNewKeycard: + if controller.getPin().len == PINLengthForStatusApp: + return createState(StateType.RepeatPin, self.flowType, self) + return nil \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/enter_pin_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/enter_pin_state.nim index a57d57872d..e190f0f579 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/enter_pin_state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/enter_pin_state.nim @@ -7,3 +7,62 @@ proc newEnterPinState*(flowType: FlowType, backState: State): EnterPinState = proc delete*(self: EnterPinState) = self.State.delete + +method getNextPrimaryState*(self: EnterPinState, controller: Controller): State = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + return createState(StateType.FactoryResetConfirmation, self.flowType, self) + return nil + +method executeSecondaryCommand*(self: EnterPinState, controller: Controller) = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method executeTertiaryCommand*(self: EnterPinState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard or + self.flowType == FlowType.FactoryReset: + if controller.getPin().len == PINLengthForStatusApp: + controller.enterKeycardPin(controller.getPin()) + +method resolveKeycardNextState*(self: EnterPinState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + let state = ensureReaderAndCardPresence(self, keycardFlowType, keycardEvent, controller) + if not state.isNil: + return state + if self.flowType == FlowType.FactoryReset: + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPIN: + controller.setKeycardData($keycardEvent.pinRetries) + if keycardEvent.pinRetries > 0: + return createState(StateType.WrongPin, self.flowType, nil) + return createState(StateType.MaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len == 0: + return createState(StateType.MaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.MaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + controller.setMetadataFromKeycard(keycardEvent.cardMetadata) + return createState(StateType.PinVerified, self.flowType, nil) + if self.flowType == FlowType.SetupNewKeycard: + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPIN: + controller.setKeycardData($keycardEvent.pinRetries) + if keycardEvent.pinRetries > 0: + return createState(StateType.WrongPin, self.flowType, nil) + return createState(StateType.MaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len == 0: + return createState(StateType.MaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.MaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + controller.setMetadataFromKeycard(keycardEvent.cardMetadata) + return createState(StateType.PinVerified, self.flowType, nil) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/enter_seed_phrase_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/enter_seed_phrase_state.nim new file mode 100644 index 0000000000..6436d6f905 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/enter_seed_phrase_state.nim @@ -0,0 +1,42 @@ +type + EnterSeedPhraseState* = ref object of State + verifiedSeedPhrase: bool + +proc newEnterSeedPhraseState*(flowType: FlowType, backState: State): EnterSeedPhraseState = + result = EnterSeedPhraseState() + result.setup(flowType, StateType.EnterSeedPhrase, backState) + result.verifiedSeedPhrase = false + +proc delete*(self: EnterSeedPhraseState) = + self.State.delete + +method executePrimaryCommand*(self: EnterSeedPhraseState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + self.verifiedSeedPhrase = controller.validSeedPhrase(controller.getSeedPhrase()) and + (not controller.getSelectedKeyPairIsProfile() or + controller.getSelectedKeyPairIsProfile() and + controller.seedPhraseRefersToLoggedInUser(controller.getSeedPhrase())) + if self.verifiedSeedPhrase: + controller.storeSeedPhraseToKeycard(controller.getSeedPhraseLength(), controller.getSeedPhrase()) + else: + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.WrongSeedPhrase, add = true)) + +method executeSecondaryCommand*(self: EnterSeedPhraseState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method getNextPrimaryState*(self: EnterSeedPhraseState, controller: Controller): State = + if self.flowType == FlowType.SetupNewKeycard: + if not self.verifiedSeedPhrase: + return createState(StateType.WrongSeedPhrase, self.flowType, nil) + +method resolveKeycardNextState*(self: EnterSeedPhraseState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + let state = ensureReaderAndCardPresence(self, keycardFlowType, keycardEvent, controller) + if not state.isNil: + return state + if self.flowType == FlowType.SetupNewKeycard: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.keyUid.len > 0: + controller.setKeyUid(keycardEvent.keyUid) + return createState(StateType.MigratingKeyPair, self.flowType, nil) diff --git a/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_confirmation_displayed_metadata_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_confirmation_displayed_metadata_state.nim new file mode 100644 index 0000000000..3d2640299f --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_confirmation_displayed_metadata_state.nim @@ -0,0 +1,25 @@ +type + FactoryResetConfirmationDisplayMetadataState* = ref object of State + +proc newFactoryResetConfirmationDisplayMetadataState*(flowType: FlowType, backState: State): FactoryResetConfirmationDisplayMetadataState = + result = FactoryResetConfirmationDisplayMetadataState() + result.setup(flowType, StateType.FactoryResetConfirmationDisplayMetadata, backState) + +proc delete*(self: FactoryResetConfirmationDisplayMetadataState) = + self.State.delete + +method executePrimaryCommand*(self: FactoryResetConfirmationDisplayMetadataState, controller: Controller) = + if self.flowType == FlowType.FactoryReset: + controller.runGetAppInfoFlow(factoryReset = true) + elif self.flowType == FlowType.SetupNewKeycard: + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.HideKeyPair, add = true)) + controller.runGetAppInfoFlow(factoryReset = true) + +method executeSecondaryCommand*(self: FactoryResetConfirmationDisplayMetadataState, controller: Controller) = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method resolveKeycardNextState*(self: FactoryResetConfirmationDisplayMetadataState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + return ensureReaderAndCardPresenceAndResolveNextState(self, keycardFlowType, keycardEvent, controller) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_confirmation_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_confirmation_state.nim index b609638396..d379707710 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_confirmation_state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_confirmation_state.nim @@ -11,10 +11,15 @@ proc delete*(self: FactoryResetConfirmationState) = method executePrimaryCommand*(self: FactoryResetConfirmationState, controller: Controller) = if self.flowType == FlowType.FactoryReset: controller.runGetAppInfoFlow(factoryReset = true) - + elif self.flowType == FlowType.SetupNewKeycard: + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.HideKeyPair, add = true)) + controller.runGetAppInfoFlow(factoryReset = true) + method executeSecondaryCommand*(self: FactoryResetConfirmationState, controller: Controller) = - if self.flowType == FlowType.FactoryReset: + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) -method getNextPrimaryState*(self: FactoryResetConfirmationState, controller: Controller): State = - return createState(StateType.PluginReader, self.flowType, nil) \ No newline at end of file +method resolveKeycardNextState*(self: FactoryResetConfirmationState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + return ensureReaderAndCardPresenceAndResolveNextState(self, keycardFlowType, keycardEvent, controller) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_success_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_success_state.nim index 67c5dff268..66ec3f52a7 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_success_state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/factory_reset_success_state.nim @@ -11,3 +11,13 @@ proc delete*(self: FactoryResetSuccessState) = method executePrimaryCommand*(self: FactoryResetSuccessState, controller: Controller) = if self.flowType == FlowType.FactoryReset: controller.terminateCurrentFlow(lastStepInTheCurrentFlow = true) + elif self.flowType == FlowType.SetupNewKeycard: + controller.runLoadAccountFlow() + +method executeSecondaryCommand*(self: FactoryResetSuccessState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = true) + +method resolveKeycardNextState*(self: FactoryResetSuccessState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + return ensureReaderAndCardPresenceAndResolveNextState(self, keycardFlowType, keycardEvent, controller) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/insert_keycard_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/insert_keycard_state.nim index 58c87cc0c1..b24425ad87 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/insert_keycard_state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/insert_keycard_state.nim @@ -9,17 +9,23 @@ proc delete*(self: InsertKeycardState) = self.State.delete method executePrimaryCommand*(self: InsertKeycardState, controller: Controller) = - if self.flowType == FlowType.FactoryReset: + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) method resolveKeycardNextState*(self: InsertKeycardState, keycardFlowType: string, keycardEvent: KeycardEvent, controller: Controller): State = + let state = ensureReaderAndCardPresenceAndResolveNextState(self, keycardFlowType, keycardEvent, controller) + if not state.isNil: + return state if keycardFlowType == ResponseTypeValueInsertCard and keycardEvent.error.len > 0 and keycardEvent.error == ErrorConnection: - controller.setKeycardData(ResponseTypeValueInsertCard) + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.WronglyInsertedCard, add = true)) return nil if keycardFlowType == ResponseTypeValueCardInserted: - controller.setKeycardData("") - return createState(StateType.ReadingKeycard, self.flowType, nil) + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.WronglyInsertedCard, add = false)) + if self.flowType == FlowType.SetupNewKeycard: + return createState(StateType.KeycardInserted, self.flowType, self.getBackState) + return createState(StateType.KeycardInserted, self.flowType, nil) return nil \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/key_pair_migrate_failure_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/key_pair_migrate_failure_state.nim new file mode 100644 index 0000000000..3fc7608b3a --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/key_pair_migrate_failure_state.nim @@ -0,0 +1,13 @@ +type + KeyPairMigrateFailureState* = ref object of State + +proc newKeyPairMigrateFailureState*(flowType: FlowType, backState: State): KeyPairMigrateFailureState = + result = KeyPairMigrateFailureState() + result.setup(flowType, StateType.KeyPairMigrateFailure, backState) + +proc delete*(self: KeyPairMigrateFailureState) = + self.State.delete + +method executePrimaryCommand*(self: KeyPairMigrateFailureState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = true) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/key_pair_migrate_success_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/key_pair_migrate_success_state.nim new file mode 100644 index 0000000000..79f3e1ef82 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/key_pair_migrate_success_state.nim @@ -0,0 +1,16 @@ +type + KeyPairMigrateSuccessState* = ref object of State + +proc newKeyPairMigrateSuccessState*(flowType: FlowType, backState: State): KeyPairMigrateSuccessState = + result = KeyPairMigrateSuccessState() + result.setup(flowType, StateType.KeyPairMigrateSuccess, backState) + +proc delete*(self: KeyPairMigrateSuccessState) = + self.State.delete + +method executePrimaryCommand*(self: KeyPairMigrateSuccessState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = true) + if controller.getSelectedKeyPairIsProfile(): + info "restart the app because of successfully migrated profile keypair" + quit() # quit the app \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/keycard_empty_metadata_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/keycard_empty_metadata_state.nim new file mode 100644 index 0000000000..620e71f8ba --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/keycard_empty_metadata_state.nim @@ -0,0 +1,20 @@ +type + KeycardEmptyMetadataState* = ref object of State + +proc newKeycardEmptyMetadataState*(flowType: FlowType, backState: State): KeycardEmptyMetadataState = + result = KeycardEmptyMetadataState() + result.setup(flowType, StateType.KeycardEmptyMetadata, backState) + +proc delete*(self: KeycardEmptyMetadataState) = + self.State.delete + +method executeSecondaryCommand*(self: KeycardEmptyMetadataState, controller: Controller) = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method getNextPrimaryState*(self: KeycardEmptyMetadataState, controller: Controller): State = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + return createState(StateType.FactoryResetConfirmation, self.flowType, self) + return nil \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/keycard_empty_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/keycard_empty_state.nim index fe0eac42a9..a212348a7a 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/keycard_empty_state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/keycard_empty_state.nim @@ -9,5 +9,6 @@ proc delete*(self: KeycardEmptyState) = self.State.delete method executePrimaryCommand*(self: KeycardEmptyState, controller: Controller) = - if self.flowType == FlowType.FactoryReset: + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/keycard_inserted_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/keycard_inserted_state.nim new file mode 100644 index 0000000000..f7ebfd4f00 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/keycard_inserted_state.nim @@ -0,0 +1,19 @@ +type + KeycardInsertedState* = ref object of State + +proc newKeycardInsertedState*(flowType: FlowType, backState: State): KeycardInsertedState = + result = KeycardInsertedState() + result.setup(flowType, StateType.KeycardInserted, backState) + +proc delete*(self: KeycardInsertedState) = + self.State.delete + +method getNextSecondaryState*(self: KeycardInsertedState, controller: Controller): State = + if self.flowType == FlowType.SetupNewKeycard: + return createState(StateType.ReadingKeycard, self.flowType, self.getBackState) + return createState(StateType.ReadingKeycard, self.flowType, nil) + +method executePrimaryCommand*(self: KeycardInsertedState, controller: Controller) = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/keycard_metadata_display_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/keycard_metadata_display_state.nim new file mode 100644 index 0000000000..c5ae69666c --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/keycard_metadata_display_state.nim @@ -0,0 +1,20 @@ +type + KeycardMetadataDisplayState* = ref object of State + +proc newKeycardMetadataDisplayState*(flowType: FlowType, backState: State): KeycardMetadataDisplayState = + result = KeycardMetadataDisplayState() + result.setup(flowType, StateType.KeycardMetadataDisplay, backState) + +proc delete*(self: KeycardMetadataDisplayState) = + self.State.delete + +method getNextPrimaryState*(self: KeycardMetadataDisplayState, controller: Controller): State = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + return createState(StateType.FactoryResetConfirmationDisplayMetadata, self.flowType, self) + return nil + +method executeSecondaryCommand*(self: KeycardMetadataDisplayState, controller: Controller) = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/keycard_not_empty_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/keycard_not_empty_state.nim new file mode 100644 index 0000000000..222e03db70 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/keycard_not_empty_state.nim @@ -0,0 +1,22 @@ +type + KeycardNotEmptyState* = ref object of State + +proc newKeycardNotEmptyState*(flowType: FlowType, backState: State): KeycardNotEmptyState = + result = KeycardNotEmptyState() + result.setup(flowType, StateType.KeycardNotEmpty, backState) + +proc delete*(self: KeycardNotEmptyState) = + self.State.delete + +method executePrimaryCommand*(self: KeycardNotEmptyState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.HideKeyPair, add = true)) + controller.runGetMetadataFlow() + +method executeSecondaryCommand*(self: KeycardNotEmptyState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method resolveKeycardNextState*(self: KeycardNotEmptyState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + return ensureReaderAndCardPresenceAndResolveNextState(self, keycardFlowType, keycardEvent, controller) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/max_pin_retries_reached_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/max_pin_retries_reached_state.nim new file mode 100644 index 0000000000..b1059993c2 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/max_pin_retries_reached_state.nim @@ -0,0 +1,20 @@ +type + MaxPinRetriesReachedState* = ref object of State + +proc newMaxPinRetriesReachedState*(flowType: FlowType, backState: State): MaxPinRetriesReachedState = + result = MaxPinRetriesReachedState() + result.setup(flowType, StateType.MaxPinRetriesReached, backState) + +proc delete*(self: MaxPinRetriesReachedState) = + self.State.delete + +method getNextPrimaryState*(self: MaxPinRetriesReachedState, controller: Controller): State = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + return createState(StateType.FactoryResetConfirmation, self.flowType, self) + return nil + +method executeSecondaryCommand*(self: MaxPinRetriesReachedState, controller: Controller) = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/migrating_key_pair_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/migrating_key_pair_state.nim new file mode 100644 index 0000000000..57515eacc8 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/migrating_key_pair_state.nim @@ -0,0 +1,38 @@ +type + MigratingKeyPairState* = ref object of State + migrationSuccess: bool + +proc newMigratingKeyPairState*(flowType: FlowType, backState: State): MigratingKeyPairState = + result = MigratingKeyPairState() + result.setup(flowType, StateType.MigratingKeyPair, backState) + result.migrationSuccess = false + +proc delete*(self: MigratingKeyPairState) = + self.State.delete + +method executePrimaryCommand*(self: MigratingKeyPairState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + # Ran authentication popup and get pass from there... + let password = controller.getPassword() + self.migrationSuccess = controller.verifyPassword(password) + if controller.getSelectedKeyPairIsProfile(): + self.migrationSuccess = self.migrationSuccess and controller.convertToKeycardAccount(password) + if not self.migrationSuccess: + return + controller.runStoreMetadataFlow(controller.getSelectedKeyPairName(), controller.getPin(), + controller.getSelectedKeyPairWalletPaths()) + +method getNextPrimaryState*(self: MigratingKeyPairState, controller: Controller): State = + if self.flowType == FlowType.SetupNewKeycard: + if not self.migrationSuccess: + return createState(StateType.KeyPairMigrateFailure, self.flowType, nil) + +method resolveKeycardNextState*(self: MigratingKeyPairState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + let state = ensureReaderAndCardPresenceAndResolveNextState(self, keycardFlowType, keycardEvent, controller) + if not state.isNil: + return state + if self.flowType == FlowType.SetupNewKeycard: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len == 0: + return createState(StateType.KeyPairMigrateSuccess, self.flowType, nil) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/not_keycard_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/not_keycard_state.nim index 17a8603457..f336b8c129 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/not_keycard_state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/not_keycard_state.nim @@ -9,5 +9,6 @@ proc delete*(self: NotKeycardState) = self.State.delete method executePrimaryCommand*(self: NotKeycardState, controller: Controller) = - if self.flowType == FlowType.FactoryReset: - controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) \ No newline at end of file + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/pin_set_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/pin_set_state.nim new file mode 100644 index 0000000000..17d3b9115f --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/pin_set_state.nim @@ -0,0 +1,21 @@ +type + PinSetState* = ref object of State + +proc newPinSetState*(flowType: FlowType, backState: State): PinSetState = + result = PinSetState() + result.setup(flowType, StateType.PinSet, backState) + +proc delete*(self: PinSetState) = + self.State.delete + +method getNextPrimaryState*(self: PinSetState, controller: Controller): State = + if self.flowType == FlowType.SetupNewKeycard: + if controller.isMnemonicBackedUp() or not controller.getSelectedKeyPairIsProfile(): + return createState(StateType.EnterSeedPhrase, self.flowType, nil) + else: + return createState(StateType.SeedPhraseDisplay, self.flowType, nil) + return nil + +method executeSecondaryCommand*(self: PinSetState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/pin_verified_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/pin_verified_state.nim new file mode 100644 index 0000000000..918c5e3bfc --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/pin_verified_state.nim @@ -0,0 +1,20 @@ +type + PinVerifiedState* = ref object of State + +proc newPinVerifiedState*(flowType: FlowType, backState: State): PinVerifiedState = + result = PinVerifiedState() + result.setup(flowType, StateType.PinVerified, backState) + +proc delete*(self: PinVerifiedState) = + self.State.delete + +method getNextPrimaryState*(self: PinVerifiedState, controller: Controller): State = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + return createState(StateType.KeycardMetadataDisplay, self.flowType, nil) + return nil + +method executeSecondaryCommand*(self: PinVerifiedState, controller: Controller) = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/plugin_reader_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/plugin_reader_state.nim index a235ec25de..a4667de4cd 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/plugin_reader_state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/plugin_reader_state.nim @@ -9,15 +9,10 @@ proc delete*(self: PluginReaderState) = self.State.delete method executePrimaryCommand*(self: PluginReaderState, controller: Controller) = - if self.flowType == FlowType.FactoryReset: + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) method resolveKeycardNextState*(self: PluginReaderState, keycardFlowType: string, keycardEvent: KeycardEvent, controller: Controller): State = - if keycardFlowType == ResponseTypeValueKeycardFlowResult and - keycardEvent.error.len > 0 and - keycardEvent.error == ErrorConnection: - controller.resumeCurrentFlowLater() - return nil - if keycardFlowType == ResponseTypeValueInsertCard: - return createState(StateType.InsertKeycard, self.flowType, nil) \ No newline at end of file + return ensureReaderAndCardPresenceAndResolveNextState(self, keycardFlowType, keycardEvent, controller) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/reading_keycard_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/reading_keycard_state.nim index 4e1c71bd8f..3497289fdd 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/reading_keycard_state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/reading_keycard_state.nim @@ -9,21 +9,16 @@ proc delete*(self: ReadingKeycardState) = self.State.delete method executePrimaryCommand*(self: ReadingKeycardState, controller: Controller) = - if self.flowType == FlowType.FactoryReset: + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) +method getNextSecondaryState*(self: ReadingKeycardState, controller: Controller): State = + let (flowType, flowEvent) = controller.getLastReceivedKeycardData() + # this is used in case a keycard is not inserted in the moment when flow is run (we're animating an insertion) + return ensureReaderAndCardPresenceAndResolveNextState(self, flowType, flowEvent, controller) + method resolveKeycardNextState*(self: ReadingKeycardState, keycardFlowType: string, keycardEvent: KeycardEvent, controller: Controller): State = - if self.flowType == FlowType.FactoryReset: - if keycardFlowType == ResponseTypeValueSwapCard and - keycardEvent.error.len > 0 and - keycardEvent.error == ErrorNotAKeycard: - return createState(StateType.NotKeycard, self.flowType, nil) - if keycardFlowType == ResponseTypeValueKeycardFlowResult and - keycardEvent.error.len > 0: - if keycardEvent.error == ErrorOk: - return createState(StateType.FactoryResetSuccess, self.flowType, nil) - if keycardEvent.error == ErrorNoKeys: - return createState(StateType.KeycardEmpty, self.flowType, nil) - controller.setContainsMetadata(keycardEvent.error != ErrorNoData) - return createState(StateType.RecognizedKeycard, self.flowType, nil) \ No newline at end of file + # this is used in case a keycard is inserted and we jump to the first meaningful screen + return ensureReaderAndCardPresenceAndResolveNextState(self, keycardFlowType, keycardEvent, controller) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/recognized_keycard_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/recognized_keycard_state.nim index b7be867d80..58db5eb4ec 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/recognized_keycard_state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/recognized_keycard_state.nim @@ -9,11 +9,15 @@ proc delete*(self: RecognizedKeycardState) = self.State.delete method executePrimaryCommand*(self: RecognizedKeycardState, controller: Controller) = - if self.flowType == FlowType.FactoryReset: + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) method getNextSecondaryState*(self: RecognizedKeycardState, controller: Controller): State = - if controller.containsMetadata(): - discard # from here we will jump to enter pin view once we add that in keycard settings - else: - return createState(StateType.FactoryResetConfirmation, self.flowType, nil) \ No newline at end of file + if self.flowType == FlowType.FactoryReset: + if controller.containsMetadata(): + return createState(StateType.EnterPin, self.flowType, nil) + else: + return createState(StateType.FactoryResetConfirmation, self.flowType, nil) + if self.flowType == FlowType.SetupNewKeycard: + return createState(StateType.CreatePin, self.flowType, self.getBackState) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/repeat_pin_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/repeat_pin_state.nim new file mode 100644 index 0000000000..6e6af1ada3 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/repeat_pin_state.nim @@ -0,0 +1,34 @@ +type + RepeatPinState* = ref object of State + +proc newRepeatPinState*(flowType: FlowType, backState: State): RepeatPinState = + result = RepeatPinState() + result.setup(flowType, StateType.RepeatPin, backState) + +proc delete*(self: RepeatPinState) = + self.State.delete + +method executeBackCommand*(self: RepeatPinState, controller: Controller) = + controller.setPin("") + controller.setPinMatch(false) + +method executeSecondaryCommand*(self: RepeatPinState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method executeTertiaryCommand*(self: RepeatPinState, controller: Controller) = + if not controller.getPinMatch(): + return + if self.flowType == FlowType.SetupNewKeycard: + controller.storePinToKeycard(controller.getPin(), controller.generateRandomPUK()) + +method resolveKeycardNextState*(self: RepeatPinState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + let state = ensureReaderAndCardPresence(self, keycardFlowType, keycardEvent, controller) + if not state.isNil: + return state + if self.flowType == FlowType.SetupNewKeycard: + if keycardFlowType == ResponseTypeValueEnterMnemonic and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorLoadingKeys: + return createState(StateType.PinSet, self.flowType, nil) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/seed_phrase_display_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/seed_phrase_display_state.nim new file mode 100644 index 0000000000..b598441a12 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/seed_phrase_display_state.nim @@ -0,0 +1,16 @@ +type + SeedPhraseDisplayState* = ref object of State + +proc newSeedPhraseDisplayState*(flowType: FlowType, backState: State): SeedPhraseDisplayState = + result = SeedPhraseDisplayState() + result.setup(flowType, StateType.SeedPhraseDisplay, backState) + +proc delete*(self: SeedPhraseDisplayState) = + self.State.delete + +method executeSecondaryCommand*(self: SeedPhraseDisplayState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method getNextPrimaryState*(self: SeedPhraseDisplayState, controller: Controller): State = + return createState(StateType.SeedPhraseEnterWords, self.flowType, self) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/seed_phrase_enter_words_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/seed_phrase_enter_words_state.nim new file mode 100644 index 0000000000..ed62069a61 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/seed_phrase_enter_words_state.nim @@ -0,0 +1,31 @@ +import strutils + +type + SeedPhraseEnterWordsState* = ref object of State + +proc newSeedPhraseEnterWordsState*(flowType: FlowType, backState: State): SeedPhraseEnterWordsState = + result = SeedPhraseEnterWordsState() + result.setup(flowType, StateType.SeedPhraseEnterWords, backState) + +proc delete*(self: SeedPhraseEnterWordsState) = + self.State.delete + +method executePrimaryCommand*(self: SeedPhraseEnterWordsState, controller: Controller) = + let mnemonic = controller.getMnemonic() + controller.storeSeedPhraseToKeycard(mnemonic.split(" ").len, mnemonic) + +method executeSecondaryCommand*(self: SeedPhraseEnterWordsState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method resolveKeycardNextState*(self: SeedPhraseEnterWordsState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + let state = ensureReaderAndCardPresence(self, keycardFlowType, keycardEvent, controller) + if not state.isNil: + return state + if self.flowType == FlowType.SetupNewKeycard: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.keyUid.len > 0: + controller.setKeyUid(keycardEvent.keyUid) + controller.removeMnemonic() + return createState(StateType.MigratingKeyPair, self.flowType, nil) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/select_existing_key_pair_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/select_existing_key_pair_state.nim new file mode 100644 index 0000000000..d58ee8e2b5 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/select_existing_key_pair_state.nim @@ -0,0 +1,21 @@ +type + SelectExistingKeyPairState* = ref object of State + +proc newSelectExistingKeyPairState*(flowType: FlowType, backState: State): SelectExistingKeyPairState = + result = SelectExistingKeyPairState() + result.setup(flowType, StateType.SelectExistingKeyPair, backState) + +proc delete*(self: SelectExistingKeyPairState) = + self.State.delete + +method executePrimaryCommand*(self: SelectExistingKeyPairState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.runLoadAccountFlow() + +method executeSecondaryCommand*(self: SelectExistingKeyPairState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method resolveKeycardNextState*(self: SelectExistingKeyPairState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + return ensureReaderAndCardPresenceAndResolveNextState(self, keycardFlowType, keycardEvent, controller) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/state.nim b/src/app/modules/shared_modules/keycard_popup/internal/state.nim index eeafe68ed8..306c6a69f8 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/state.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/state.nim @@ -1,24 +1,39 @@ import ../controller from ../../../../../app_service/service/keycard/service import KeycardEvent, KeyDetails +from ../io_interface import FlowType -export KeycardEvent, KeyDetails - -type FlowType* {.pure.} = enum - General = "General" - FactoryReset = "FactoryReset" - +export FlowType, KeycardEvent, KeyDetails type StateType* {.pure.} = enum NoState = "NoState" PluginReader = "PluginReader" ReadingKeycard = "ReadingKeycard" InsertKeycard = "InsertKeycard" + KeycardInserted = "KeycardInserted" + CreatePin = "CreatePin" + RepeatPin = "RepeatPin" + PinSet = "PinSet" + PinVerified = "PinVerified" EnterPin = "EnterPin" + WrongPin = "WrongPin" + MaxPinRetriesReached = "MaxPinRetriesReached" FactoryResetConfirmation = "FactoryResetConfirmation" + FactoryResetConfirmationDisplayMetadata = "FactoryResetConfirmationDisplayMetadata" FactoryResetSuccess = "FactoryResetSuccess" + KeycardEmptyMetadata = "KeycardEmptyMetadata" + KeycardMetadataDisplay = "KeycardMetadataDisplay" KeycardEmpty = "KeycardEmpty" + KeycardNotEmpty = "KeycardNotEmpty" NotKeycard = "NotKeycard" RecognizedKeycard = "RecognizedKeycard" + SelectExistingKeyPair = "SelectExistingKeyPair" + EnterSeedPhrase = "EnterSeedPhrase" + WrongSeedPhrase = "WrongSeedPhrase" + SeedPhraseDisplay = "SeedPhraseDisplay" + SeedPhraseEnterWords = "SeedPhraseEnterWords" + KeyPairMigrateSuccess = "KeyPairMigrateSuccess" + KeyPairMigrateFailure = "KeyPairMigrateFailure" + MigratingKeyPair = "MigratingKeyPair" ## This is the base class for all state we may have in onboarding/login flow. @@ -70,6 +85,10 @@ method getNextPrimaryState*(self: State, controller: Controller): State {.inlin method getNextSecondaryState*(self: State, controller: Controller): State {.inline base.} = return nil +## Returns next state instance in case the "tertiary" action is triggered +method getNextTertiaryState*(self: State, controller: Controller): State {.inline base.} = + return nil + ## This method is executed in case "back" button is clicked method executeBackCommand*(self: State, controller: Controller) {.inline base.} = discard @@ -82,6 +101,10 @@ method executePrimaryCommand*(self: State, controller: Controller) {.inline base method executeSecondaryCommand*(self: State, controller: Controller) {.inline base.} = discard +## This method is executed in case "tertiary" action is triggered +method executeTertiaryCommand*(self: State, controller: Controller) {.inline base.} = + discard + ## This method is used for handling aync responses for keycard related states method resolveKeycardNextState*(self: State, keycardFlowType: string, keycardEvent: KeycardEvent, controller: Controller): State {.inline base.} = diff --git a/src/app/modules/shared_modules/keycard_popup/internal/state_factory.nim b/src/app/modules/shared_modules/keycard_popup/internal/state_factory.nim index 12c9de921f..934068f928 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/state_factory.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/state_factory.nim @@ -1,6 +1,7 @@ -import chronicles +import parseutils, chronicles import ../../../../../app_service/service/keycard/constants import ../controller +from ../../../../../app_service/service/keycard/service import KCSFlowType from ../../../../../app_service/service/keycard/service import PINLengthForStatusApp from ../../../../../app_service/service/keycard/service import PUKLengthForStatusApp import state @@ -8,37 +9,217 @@ import state logScope: topics = "startup-module-state-factory" +# The following constants will be used in bitwise operation +type PredefinedKeycardData* {.pure.} = enum + WronglyInsertedCard = 1 + HideKeyPair = 2 + WrongSeedPhrase = 4 + # Forward declaration proc createState*(stateToBeCreated: StateType, flowType: FlowType, backState: State): State +proc getPredefinedKeycardData*(currValue: string, value: PredefinedKeycardData, add: bool): string +proc ensureReaderAndCardPresence*(state: State, keycardFlowType: string, keycardEvent: KeycardEvent, controller: Controller): State +proc ensureReaderAndCardPresenceAndResolveNextState*(state: State, keycardFlowType: string, keycardEvent: KeycardEvent, controller: Controller): State -include enter_pin_state +include create_pin_state +include enter_pin_state +include enter_seed_phrase_state +include factory_reset_confirmation_displayed_metadata_state include factory_reset_confirmation_state include factory_reset_success_state include insert_keycard_state +include key_pair_migrate_failure_state +include key_pair_migrate_success_state +include keycard_empty_metadata_state include keycard_empty_state +include keycard_inserted_state +include keycard_metadata_display_state +include keycard_not_empty_state +include max_pin_retries_reached_state +include migrating_key_pair_state include not_keycard_state +include pin_set_state +include pin_verified_state include plugin_reader_state include reading_keycard_state include recognized_keycard_state +include repeat_pin_state +include seed_phrase_display_state +include seed_phrase_enter_words_state +include select_existing_key_pair_state +include wrong_pin_state +include wrong_seed_phrase_state + +proc getPredefinedKeycardData*(currValue: string, value: PredefinedKeycardData, add: bool): string = + var currNum: int + try: + if add: + if parseInt(currValue, currNum) == 0: + return $(value.int) + else: + return $(currNum or value.int) + else: + if parseInt(currValue, currNum) == 0: + return "" + else: + return $(currNum and (not value.int)) + except: + return if add: $(value.int) else: "" proc createState*(stateToBeCreated: StateType, flowType: FlowType, backState: State): State = + if stateToBeCreated == StateType.CreatePin: + return newCreatePinState(flowType, backState) if stateToBeCreated == StateType.EnterPin: return newEnterPinState(flowType, backState) + if stateToBeCreated == StateType.EnterSeedPhrase: + return newEnterSeedPhraseState(flowType, backState) + if stateToBeCreated == StateType.FactoryResetConfirmationDisplayMetadata: + return newFactoryResetConfirmationDisplayMetadataState(flowType, backState) if stateToBeCreated == StateType.FactoryResetConfirmation: return newFactoryResetConfirmationState(flowType, backState) if stateToBeCreated == StateType.FactoryResetSuccess: return newFactoryResetSuccessState(flowType, backState) if stateToBeCreated == StateType.InsertKeycard: return newInsertKeycardState(flowType, backState) + if stateToBeCreated == StateType.KeyPairMigrateFailure: + return newKeyPairMigrateFailureState(flowType, backState) + if stateToBeCreated == StateType.KeyPairMigrateSuccess: + return newKeyPairMigrateSuccessState(flowType, backState) + if stateToBeCreated == StateType.KeycardInserted: + return newKeycardInsertedState(flowType, backState) + if stateToBeCreated == StateType.KeycardEmptyMetadata: + return newKeycardEmptyMetadataState(flowType, backState) if stateToBeCreated == StateType.KeycardEmpty: return newKeycardEmptyState(flowType, backState) + if stateToBeCreated == StateType.KeycardMetadataDisplay: + return newKeycardMetadataDisplayState(flowType, backState) + if stateToBeCreated == StateType.KeycardNotEmpty: + return newKeycardNotEmptyState(flowType, backState) + if stateToBeCreated == StateType.MaxPinRetriesReached: + return newMaxPinRetriesReachedState(flowType, backState) + if stateToBeCreated == StateType.MigratingKeyPair: + return newMigratingKeyPairState(flowType, backState) if stateToBeCreated == StateType.NotKeycard: return newNotKeycardState(flowType, backState) + if stateToBeCreated == StateType.PinSet: + return newPinSetState(flowType, backState) + if stateToBeCreated == StateType.PinVerified: + return newPinVerifiedState(flowType, backState) if stateToBeCreated == StateType.PluginReader: return newPluginReaderState(flowType, backState) if stateToBeCreated == StateType.ReadingKeycard: return newReadingKeycardState(flowType, backState) if stateToBeCreated == StateType.RecognizedKeycard: return newRecognizedKeycardState(flowType, backState) + if stateToBeCreated == StateType.RepeatPin: + return newRepeatPinState(flowType, backState) + if stateToBeCreated == StateType.SeedPhraseDisplay: + return newSeedPhraseDisplayState(flowType, backState) + if stateToBeCreated == StateType.SeedPhraseEnterWords: + return newSeedPhraseEnterWordsState(flowType, backState) + if stateToBeCreated == StateType.SelectExistingKeyPair: + return newSelectExistingKeyPairState(flowType, backState) + if stateToBeCreated == StateType.WrongPin: + return newWrongPinState(flowType, backState) + if stateToBeCreated == StateType.WrongSeedPhrase: + return newWrongSeedPhraseState(flowType, backState) - error "No implementation available for state ", state=stateToBeCreated \ No newline at end of file + error "No implementation available for state ", state=stateToBeCreated + +proc ensureReaderAndCardPresence*(state: State, keycardFlowType: string, keycardEvent: KeycardEvent, controller: Controller): State = + ## Handling factory reset flow + if state.flowType == FlowType.FactoryReset: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + if state.stateType == StateType.PluginReader: + return nil + return createState(StateType.PluginReader, state.flowType, nil) + if keycardFlowType == ResponseTypeValueInsertCard and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + if state.stateType == StateType.InsertKeycard: + return nil + return createState(StateType.InsertKeycard, state.flowType, state) + if keycardFlowType == ResponseTypeValueCardInserted: + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.WronglyInsertedCard, add = false)) + return createState(StateType.KeycardInserted, state.flowType, nil) + + ## Handling setup new keycard flow + if state.flowType == FlowType.SetupNewKeycard: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + controller.resumeCurrentFlowLater() + if state.stateType == StateType.PluginReader: + return nil + return createState(StateType.PluginReader, state.flowType, state) + if keycardFlowType == ResponseTypeValueInsertCard and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorConnection: + if state.stateType == StateType.InsertKeycard: + return nil + return createState(StateType.InsertKeycard, state.flowType, state) + if keycardFlowType == ResponseTypeValueCardInserted: + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.WronglyInsertedCard, add = false)) + return createState(StateType.KeycardInserted, state.flowType, state.getBackState) + +proc ensureReaderAndCardPresenceAndResolveNextState*(state: State, keycardFlowType: string, keycardEvent: KeycardEvent, controller: Controller): State = + let ensureState = ensureReaderAndCardPresence(state, keycardFlowType, keycardEvent, controller) + if not ensureState.isNil: + return ensureState + ## Handling factory reset flow + if state.flowType == FlowType.FactoryReset: + if keycardFlowType == ResponseTypeValueEnterPIN: + return createState(StateType.EnterPin, state.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.MaxPinRetriesReached, state.flowType, nil) + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorNotAKeycard: + return createState(StateType.NotKeycard, state.flowType, nil) + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + if keycardEvent.error.len > 0: + if keycardEvent.error == ErrorOk: + return createState(StateType.FactoryResetSuccess, state.flowType, nil) + if keycardEvent.error == ErrorNoKeys: + return createState(StateType.KeycardEmpty, state.flowType, nil) + if keycardEvent.error == ErrorNoData: + return createState(StateType.KeycardEmptyMetadata, state.flowType, nil) + if keycardEvent.error.len == 0: + if keycardEvent.cardMetadata.name.len > 0 and keycardEvent.cardMetadata.walletAccounts.len > 0: + controller.setContainsMetadata(true) + return createState(StateType.RecognizedKeycard, state.flowType, nil) + + ## Handling setup new keycard flow + if state.flowType == FlowType.SetupNewKeycard: + if keycardFlowType == ResponseTypeValueSwapCard and + keycardEvent.error.len > 0: + if keycardEvent.error == ErrorNotAKeycard: + return createState(StateType.NotKeycard, state.flowType, nil) + if keycardEvent.error == ErrorHasKeys: + return createState(StateType.KeycardNotEmpty, state.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPIN: + if controller.getCurrentKeycardServiceFlow() == KCSFlowType.GetMetadata: + return createState(StateType.EnterPin, state.flowType, nil) + return createState(StateType.KeycardNotEmpty, state.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.MaxPinRetriesReached, state.flowType, nil) + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.error.len > 0: + controller.setKeycardData("") + if keycardEvent.error == ErrorOk: + return createState(StateType.FactoryResetSuccess, state.flowType, nil) + if keycardEvent.error == ErrorNoData: + return createState(StateType.KeycardEmptyMetadata, state.flowType, nil) + if keycardEvent.error == ErrorNoKeys: + return createState(StateType.KeycardEmptyMetadata, state.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterNewPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == ErrorRequireInit: + return createState(StateType.RecognizedKeycard, state.flowType, state.getBackState) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/state_wrapper.nim b/src/app/modules/shared_modules/keycard_popup/internal/state_wrapper.nim index cb32bd3678..84e21005ea 100644 --- a/src/app/modules/shared_modules/keycard_popup/internal/state_wrapper.nim +++ b/src/app/modules/shared_modules/keycard_popup/internal/state_wrapper.nim @@ -55,4 +55,8 @@ QtObject: proc secondaryActionClicked*(self: StateWrapper) {.signal.} proc doSecondaryAction*(self: StateWrapper) {.slot.} = - self.secondaryActionClicked() \ No newline at end of file + self.secondaryActionClicked() + + proc tertiaryActionClicked*(self: StateWrapper) {.signal.} + proc doTertiaryAction*(self: StateWrapper) {.slot.} = + self.tertiaryActionClicked() \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/wrong_pin_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/wrong_pin_state.nim new file mode 100644 index 0000000000..faaa994c1c --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/wrong_pin_state.nim @@ -0,0 +1,62 @@ +type + WrongPinState* = ref object of State + +proc newWrongPinState*(flowType: FlowType, backState: State): WrongPinState = + result = WrongPinState() + result.setup(flowType, StateType.WrongPin, backState) + +proc delete*(self: WrongPinState) = + self.State.delete + +method getNextPrimaryState*(self: WrongPinState, controller: Controller): State = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + return createState(StateType.FactoryResetConfirmation, self.flowType, self) + return nil + +method executeSecondaryCommand*(self: WrongPinState, controller: Controller) = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method executeTertiaryCommand*(self: WrongPinState, controller: Controller) = + if self.flowType == FlowType.FactoryReset or + self.flowType == FlowType.SetupNewKeycard: + if controller.getPin().len == PINLengthForStatusApp: + controller.enterKeycardPin(controller.getPin()) + +method resolveKeycardNextState*(self: WrongPinState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + let state = ensureReaderAndCardPresence(self, keycardFlowType, keycardEvent, controller) + if not state.isNil: + return state + if self.flowType == FlowType.FactoryReset: + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPIN: + controller.setKeycardData($keycardEvent.pinRetries) + if keycardEvent.pinRetries > 0: + return self + return createState(StateType.MaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.MaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + controller.setMetadataFromKeycard(keycardEvent.cardMetadata) + return createState(StateType.PinVerified, self.flowType, nil) + if self.flowType == FlowType.SetupNewKeycard: + if keycardFlowType == ResponseTypeValueEnterPIN and + keycardEvent.error.len > 0 and + keycardEvent.error == RequestParamPIN: + controller.setKeycardData($keycardEvent.pinRetries) + if keycardEvent.pinRetries > 0: + return self + return createState(StateType.MaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueEnterPUK and + keycardEvent.error.len == 0: + if keycardEvent.pinRetries == 0 and keycardEvent.pukRetries > 0: + return createState(StateType.MaxPinRetriesReached, self.flowType, nil) + if keycardFlowType == ResponseTypeValueKeycardFlowResult: + controller.setMetadataFromKeycard(keycardEvent.cardMetadata) + return createState(StateType.PinVerified, self.flowType, nil) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/internal/wrong_seed_phrase_state.nim b/src/app/modules/shared_modules/keycard_popup/internal/wrong_seed_phrase_state.nim new file mode 100644 index 0000000000..6d2ac4cd4d --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/internal/wrong_seed_phrase_state.nim @@ -0,0 +1,40 @@ +import os + +type + WrongSeedPhraseState* = ref object of State + verifiedSeedPhrase: bool + +proc newWrongSeedPhraseState*(flowType: FlowType, backState: State): WrongSeedPhraseState = + result = WrongSeedPhraseState() + result.setup(flowType, StateType.WrongSeedPhrase, backState) + result.verifiedSeedPhrase = false + +proc delete*(self: WrongSeedPhraseState) = + self.State.delete + +method executePrimaryCommand*(self: WrongSeedPhraseState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.WrongSeedPhrase, add = false)) + sleep(500) # just to shortly remove text on the UI side + self.verifiedSeedPhrase = controller.validSeedPhrase(controller.getSeedPhrase()) and + controller.seedPhraseRefersToLoggedInUser(controller.getSeedPhrase()) + if self.verifiedSeedPhrase: + controller.storeSeedPhraseToKeycard(controller.getSeedPhraseLength(), controller.getSeedPhrase()) + else: + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.WrongSeedPhrase, add = true)) + +method executeSecondaryCommand*(self: WrongSeedPhraseState, controller: Controller) = + if self.flowType == FlowType.SetupNewKeycard: + controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + +method resolveKeycardNextState*(self: WrongSeedPhraseState, keycardFlowType: string, keycardEvent: KeycardEvent, + controller: Controller): State = + let state = ensureReaderAndCardPresence(self, keycardFlowType, keycardEvent, controller) + if not state.isNil: + return state + if self.flowType == FlowType.SetupNewKeycard: + if keycardFlowType == ResponseTypeValueKeycardFlowResult and + keycardEvent.keyUid.len > 0: + controller.setKeyUid(keycardEvent.keyUid) + controller.setKeycardData(getPredefinedKeycardData(controller.getKeycardData(), PredefinedKeycardData.WrongSeedPhrase, add = false)) + return createState(StateType.MigratingKeyPair, self.flowType, nil) diff --git a/src/app/modules/shared_modules/keycard_popup/io_interface.nim b/src/app/modules/shared_modules/keycard_popup/io_interface.nim index 82523d8c0d..41902122a1 100644 --- a/src/app/modules/shared_modules/keycard_popup/io_interface.nim +++ b/src/app/modules/shared_modules/keycard_popup/io_interface.nim @@ -1,13 +1,20 @@ import NimQml import ../../../../app/core/eventemitter -from ../../../../app_service/service/keycard/service import KeycardEvent, KeyDetails +from ../../../../app_service/service/keycard/service import KeycardEvent, CardMetadata, KeyDetails +import models/key_pair_item +const SignalSharedKeycarModuleDisplayPopup* = "SignalSharedKeycarModuleDisplayPopup" const SignalSharedKeycarModuleFlowTerminated* = "sharedKeycarModuleFlowTerminated" type SharedKeycarModuleFlowTerminatedArgs* = ref object of Args lastStepInTheCurrentFlow*: bool +type FlowType* {.pure.} = enum + General = "General" + FactoryReset = "FactoryReset" + SetupNewKeycard = "SetupNewKeycard" + type AccessInterface* {.pure inheritable.} = ref object of RootObj @@ -17,6 +24,9 @@ method delete*(self: AccessInterface) {.base.} = method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} = raise newException(ValueError, "No implementation available") +method getKeycardData*(self: AccessInterface): string {.base.} = + raise newException(ValueError, "No implementation available") + method setKeycardData*(self: AccessInterface, value: string) {.base.} = raise newException(ValueError, "No implementation available") @@ -29,13 +39,51 @@ method onPrimaryActionClicked*(self: AccessInterface) {.base.} = method onSecondaryActionClicked*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") +method onTertiaryActionClicked*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + method onKeycardResponse*(self: AccessInterface, keycardFlowType: string, keycardEvent: KeycardEvent) {.base.} = raise newException(ValueError, "No implementation available") -method runFactoryResetFlow*(self: AccessInterface) {.base.} = +method runFlow*(self: AccessInterface, flowToRun: FlowType) {.base.} = raise newException(ValueError, "No implementation available") +method setPin*(self: AccessInterface, value: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method setPassword*(self: AccessInterface, value: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method checkRepeatedKeycardPinWhileTyping*(self: AccessInterface, pin: string): bool {.base.} = + raise newException(ValueError, "No implementation available") + +method getMnemonic*(self: AccessInterface): string {.base.} = + raise newException(ValueError, "No implementation available") + +method setSeedPhrase*(self: AccessInterface, value: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method getSeedPhrase*(self: AccessInterface): string {.base.} = + raise newException(ValueError, "No implementation available") + +method validSeedPhrase*(self: AccessInterface, value: string): bool {.base.} = + raise newException(ValueError, "No implementation available") + +method setSelectedKeyPair*(self: AccessInterface, item: KeyPairItem) {.base.} = + raise newException(ValueError, "No implementation available") + +method setKeyPairStoredOnKeycard*(self: AccessInterface, cardMetadata: CardMetadata) {.base.} = + raise newException(ValueError, "No implementation available") + +method loggedInUserUsesBiometricLogin*(self: AccessInterface): bool {.base.} = + raise newException(ValueError, "No implementation available") + +method migratingProfileKeyPair*(self: AccessInterface): bool {.base.} = + raise newException(ValueError, "No implementation available") + +method isProfileKeyPairMigrated*(self: AccessInterface): bool {.base.} = + raise newException(ValueError, "No implementation available") + + type DelegateInterface* = concept c - #c.startupDidLoad() - #c.userLoggedIn() diff --git a/src/app/modules/shared_modules/keycard_popup/models/key_pair_item.nim b/src/app/modules/shared_modules/keycard_popup/models/key_pair_item.nim new file mode 100644 index 0000000000..685707c513 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/models/key_pair_item.nim @@ -0,0 +1,82 @@ +import strformat, marshal + +type + KeyPairType* {.pure.} = enum + Unknown = -1 + Profile + SeedImport + PrivateKeyImport + +type + WalletAccountDetails = tuple + name: string + path: string + address: string + emoji: string + color: string + icon: string + balance: float64 + +type + KeyPairItem* = ref object of RootObj + pubKey: string + name: string + image: string + icon: string + derivedFrom: string + pairType: KeyPairType + accounts: seq[WalletAccountDetails] + +proc initKeyPairItem*( + pubKey: string, + name: string, + image: string, + icon: string, + pairType: KeyPairType, + derivedFrom: string + ): KeyPairItem = + result = KeyPairItem() + result.pubKey = pubKey + result.name = name + result.image = image + result.icon = icon + result.pairType = pairType + result.derivedFrom = derivedFrom + +proc `$`*(self: KeyPairItem): string = + result = fmt"""KeyPairItem[ + pubKey: {self.pubkey}, + name: {self.name}, + image: {self.image}, + icon: {self.icon}, + pairType: {$self.pairType}, + derivedFrom: {self.derivedFrom}, + accounts: {$self.accounts} + ]""" + +proc pubKey*(self: KeyPairItem): string {.inline.} = + self.pubKey + +proc name*(self: KeyPairItem): string {.inline.} = + self.name + +proc image*(self: KeyPairItem): string {.inline.} = + self.image + +proc icon*(self: KeyPairItem): string {.inline.} = + self.icon + +proc pairType*(self: KeyPairItem): KeyPairType {.inline.} = + self.pairType + +proc derivedFrom*(self: KeyPairItem): string {.inline.} = + self.derivedFrom + +proc addAccount*(self: KeyPairItem, name, path, address, emoji, color, icon: string, balance: float64) {.inline.} = + self.accounts.add((name: name, path: path, address: address, emoji: emoji, color: color, icon: icon, balance: balance)) + +proc accounts*(self: KeyPairItem): string {.inline.} = + return $$self.accounts + +proc accountsAsArr*(self: KeyPairItem): seq[WalletAccountDetails] {.inline.} = + return self.accounts \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/models/key_pair_model.nim b/src/app/modules/shared_modules/keycard_popup/models/key_pair_model.nim new file mode 100644 index 0000000000..4f868f4e93 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/models/key_pair_model.nim @@ -0,0 +1,90 @@ +import NimQml, Tables, strformat +import key_pair_item + +type + ModelRole {.pure.} = enum + PubKey = UserRole + 1 + Name + Image + Icon + PairType + Accounts + DerivedFrom + +QtObject: + type + KeyPairModel* = ref object of QAbstractListModel + items: seq[KeyPairItem] + + proc delete(self: KeyPairModel) = + self.items = @[] + self.QAbstractListModel.delete + + proc setup(self: KeyPairModel) = + self.QAbstractListModel.setup + + proc newKeyPairModel*(): KeyPairModel = + new(result, delete) + result.setup + + proc countChanged(self: KeyPairModel) {.signal.} + proc getCount*(self: KeyPairModel): int {.slot.} = + self.items.len + QtProperty[int]count: + read = getCount + notify = countChanged + + proc setItems*(self: KeyPairModel, items: seq[KeyPairItem]) = + self.beginResetModel() + self.items = items + self.endResetModel() + self.countChanged() + + proc `$`*(self: KeyPairModel): string = + for i in 0 ..< self.items.len: + result &= fmt"""KeyPairModel: + [{i}]:({$self.items[i]}) + """ + + method rowCount(self: KeyPairModel, index: QModelIndex = nil): int = + return self.items.len + + method roleNames(self: KeyPairModel): Table[int, string] = + { + ModelRole.PubKey.int: "pubKey", + ModelRole.Name.int: "name", + ModelRole.Image.int: "image", + ModelRole.Icon.int: "icon", + ModelRole.PairType.int: "pairType", + ModelRole.Accounts.int: "accounts", + ModelRole.DerivedFrom.int: "derivedFrom" + }.toTable + + method data(self: KeyPairModel, index: QModelIndex, role: int): QVariant = + if (not index.isValid): + return + if (index.row < 0 or index.row >= self.items.len): + return + let item = self.items[index.row] + let enumRole = role.ModelRole + case enumRole: + of ModelRole.PubKey: + result = newQVariant(item.pubKey) + of ModelRole.Name: + result = newQVariant(item.name) + of ModelRole.Image: + result = newQVariant(item.image) + of ModelRole.Icon: + result = newQVariant(item.icon) + of ModelRole.PairType: + result = newQVariant(item.pairType.int) + of ModelRole.Accounts: + result = newQVariant(item.accounts) + of ModelRole.DerivedFrom: + result = newQVariant(item.derivedFrom) + + proc findItemByDerivedFromAddress*(self: KeyPairModel, address: string): KeyPairItem = + for i in 0 ..< self.items.len: + if(self.items[i].derivedFrom == address): + return self.items[i] + return nil \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/models/key_pair_selected_item.nim b/src/app/modules/shared_modules/keycard_popup/models/key_pair_selected_item.nim new file mode 100644 index 0000000000..9571288c06 --- /dev/null +++ b/src/app/modules/shared_modules/keycard_popup/models/key_pair_selected_item.nim @@ -0,0 +1,75 @@ +import NimQml +import key_pair_item + +QtObject: + type KeyPairSelectedItem* = ref object of QObject + item: KeyPairItem + + proc delete*(self: KeyPairSelectedItem) = + self.QObject.delete + + proc newKeyPairSelectedItem*(): KeyPairSelectedItem = + new(result, delete) + result.QObject.setup + + proc keyPairSelectedItemChanged*(self: KeyPairSelectedItem) {.signal.} + + proc setItem*(self: KeyPairSelectedItem, item: KeyPairItem) = + self.item = item + self.keyPairSelectedItemChanged() + + proc getPubKey*(self: KeyPairSelectedItem): string {.slot.} = + if(self.item.isNil): + return "" + return self.item.pubKey() + QtProperty[string] pubKey: + read = getPubKey + notify = keyPairSelectedItemChanged + + proc getName*(self: KeyPairSelectedItem): string {.slot.} = + if(self.item.isNil): + return "" + return self.item.name() + QtProperty[string] name: + read = getName + notify = keyPairSelectedItemChanged + + proc getImage*(self: KeyPairSelectedItem): string {.slot.} = + if(self.item.isNil): + return "" + return self.item.image() + QtProperty[string] image: + read = getImage + notify = keyPairSelectedItemChanged + + proc getIcon*(self: KeyPairSelectedItem): string {.slot.} = + if(self.item.isNil): + return "" + return self.item.icon() + QtProperty[string] icon: + read = getIcon + notify = keyPairSelectedItemChanged + + proc getPairType*(self: KeyPairSelectedItem): int {.slot.} = + if(self.item.isNil): + return KeyPairType.Profile.int + return self.item.pairType().int + QtProperty[int] pairType: + read = getPairType + notify = keyPairSelectedItemChanged + + proc getDerivedFrom*(self: KeyPairSelectedItem): string {.slot.} = + if(self.item.isNil): + return "" + return self.item.derivedFrom() + QtProperty[string] derivedFrom: + read = getDerivedFrom + notify = keyPairSelectedItemChanged + + proc getAccounts*(self: KeyPairSelectedItem): string {.slot.} = + if(self.item.isNil): + return "" + return self.item.accounts() + QtProperty[string] accounts: + read = getAccounts + notify = keyPairSelectedItemChanged \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/module.nim b/src/app/modules/shared_modules/keycard_popup/module.nim index 7f4954b2f3..dcd766c59f 100644 --- a/src/app/modules/shared_modules/keycard_popup/module.nim +++ b/src/app/modules/shared_modules/keycard_popup/module.nim @@ -1,11 +1,16 @@ -import NimQml, chronicles +import NimQml, random, strutils, marshal, chronicles import io_interface import view, controller import internal/[state, state_factory] +import models/[key_pair_model, key_pair_item] +import ../../../global/global_singleton import ../../../core/eventemitter import ../../../../app_service/service/keycard/service as keycard_service +import ../../../../app_service/service/privacy/service as privacy_service +import ../../../../app_service/service/accounts/service as accounts_service +import ../../../../app_service/service/wallet_account/service as wallet_account_service export io_interface @@ -19,20 +24,24 @@ type viewVariant: QVariant controller: Controller initialized: bool + tmpLocalState: State # used when flow is run, until response arrives to determine next state appropriatelly proc newModule*[T](delegate: T, events: EventEmitter, - keycardService: keycard_service.Service): + keycardService: keycard_service.Service, + privacyService: privacy_service.Service, + accountsService: accounts_service.Service, + walletAccountService: wallet_account_service.Service): Module[T] = result = Module[T]() result.delegate = delegate result.view = view.newView(result) result.viewVariant = newQVariant(result.view) - result.controller = controller.newController(result, events, keycardService) + result.controller = controller.newController(result, events, keycardService, privacyService, accountsService, + walletAccountService) result.initialized = false method delete*[T](self: Module[T]) = - self.controller.disconnect() self.view.delete self.viewVariant.delete self.controller.delete @@ -40,9 +49,54 @@ method delete*[T](self: Module[T]) = method getModuleAsVariant*[T](self: Module[T]): QVariant = return self.viewVariant +method getKeycardData*[T](self: Module[T]): string = + return self.view.getKeycardData() + method setKeycardData*[T](self: Module[T], value: string) = self.view.setKeycardData(value) +method setPin*[T](self: Module[T], value: string) = + self.controller.setPin(value) + +method setPassword*[T](self: Module[T], value: string) = + self.controller.setPassword(value) + +method checkRepeatedKeycardPinWhileTyping*[T](self: Module[T], pin: string): bool = + self.controller.setPinMatch(false) + let storedPin = self.controller.getPin() + if pin.len > storedPin.len: + return false + elif pin.len < storedPin.len: + for i in 0 ..< pin.len: + if pin[i] != storedPin[i]: + return false + return true + else: + let match = pin == storedPin + self.controller.setPinMatch(match) + return match + +method getMnemonic*[T](self: Module[T]): string = + return self.controller.getMnemonic() + +method setSeedPhrase*[T](self: Module[T], value: string) = + self.controller.setSeedPhrase(value) + +method getSeedPhrase*[T](self: Module[T]): string = + return self.controller.getSeedPhrase() + +method validSeedPhrase*[T](self: Module[T], value: string): bool = + return self.controller.validSeedPhrase(value) + +method loggedInUserUsesBiometricLogin*[T](self: Module[T]): bool = + return self.controller.loggedInUserUsesBiometricLogin() + +method migratingProfileKeyPair*[T](self: Module[T]): bool = + return self.controller.getSelectedKeyPairIsProfile() + +method isProfileKeyPairMigrated*[T](self: Module[T]): bool = + return self.controller.getLoggedInAccount().keycardPairing.len > 0 + method onBackActionClicked*[T](self: Module[T]) = let currStateObj = self.view.currentStateObj() if currStateObj.isNil: @@ -81,11 +135,32 @@ method onSecondaryActionClicked*[T](self: Module[T]) = self.view.setCurrentState(nextState) debug "sm_secondary_action - set state", setCurrFlow=nextState.flowType(), setCurrState=nextState.stateType() -method onKeycardResponse*[T](self: Module[T], keycardFlowType: string, keycardEvent: KeycardEvent) = +method onTertiaryActionClicked*[T](self: Module[T]) = let currStateObj = self.view.currentStateObj() if currStateObj.isNil: error "sm_cannot resolve current state" return + debug "sm_tertiary_action", currFlow=currStateObj.flowType(), currState=currStateObj.stateType() + currStateObj.executeTertiaryCommand(self.controller) + let nextState = currStateObj.getNextTertiaryState(self.controller) + if nextState.isNil: + return + self.view.setCurrentState(nextState) + debug "sm_tertiary_action - set state", setCurrFlow=nextState.flowType(), setCurrState=nextState.stateType() + +method onKeycardResponse*[T](self: Module[T], keycardFlowType: string, keycardEvent: KeycardEvent) = + let currStateObj = self.view.currentStateObj() + if currStateObj.isNil: + if self.tmpLocalState.isNil: + error "sm_cannot resolve current state" + return + let nextState = self.tmpLocalState.resolveKeycardNextState(keycardFlowType, keycardEvent, self.controller) + if nextState.isNil: + return + self.view.setCurrentState(nextState) + self.controller.readyToDisplayPopup() + debug "sm_on_keycard_response - from_local - set state", setCurrFlow=nextState.flowType(), setCurrState=nextState.stateType() + return debug "sm_on_keycard_response", currFlow=currStateObj.flowType(), currState=currStateObj.stateType() let nextState = currStateObj.resolveKeycardNextState(keycardFlowType, keycardEvent, self.controller) if nextState.isNil: @@ -93,8 +168,131 @@ method onKeycardResponse*[T](self: Module[T], keycardFlowType: string, keycardEv self.view.setCurrentState(nextState) debug "sm_on_keycard_response - set state", setCurrFlow=nextState.flowType(), setCurrState=nextState.stateType() -method runFactoryResetFlow*[T](self: Module[T]) = +proc prepareKeyPairsModel[T](self: Module[T]) = + let findItemByDerivedFromAddress = proc(items: seq[KeyPairItem], address: string): KeyPairItem = + if address.len == 0: + return nil + for i in 0 ..< items.len: + if(items[i].derivedFrom == address): + return items[i] + return nil + + let countOfKeyPairsForType = proc(items: seq[KeyPairItem], keyPairType: KeyPairType): int = + result = 0 + for i in 0 ..< items.len: + if(items[i].pairType == keyPairType): + result.inc + + let accounts = self.controller.getWalletAccounts() + var items: seq[KeyPairItem] + for a in accounts: + if a.isChat or a.walletType == WalletTypeWatch: + continue + var item = findItemByDerivedFromAddress(items, a.derivedfrom) + if a.walletType == WalletTypeDefaultStatusAccount or a.walletType == WalletTypeGenerated: + if self.isProfileKeyPairMigrated(): + continue + if item.isNil: + item = initKeyPairItem(pubKey = singletonInstance.userProfile.getPubKey(), + name = singletonInstance.userProfile.getName(), + image = singletonInstance.userProfile.getIcon(), + icon = "", + pairType = KeyPairType.Profile, + derivedFrom = a.derivedfrom) + items.insert(item, 0) # Status Account must be at first place + var icon = "" + if a.walletType == WalletTypeDefaultStatusAccount: + icon = "wallet" + items[0].addAccount(a.name, a.path, a.address, a.emoji, a.color, icon, balance = 0.0) + continue + if a.walletType == WalletTypeSeed: + let diffImports = countOfKeyPairsForType(items, KeyPairType.SeedImport) + if item.isNil: + item = initKeyPairItem(pubKey = "", + name = "Seed Phrase " & $(diffImports + 1), # string created here should be transalted, but so far it's like it is + image = "", + icon = "key_pair_seed_phrase", + pairType = KeyPairType.SeedImport, + derivedFrom = a.derivedfrom) + items.add(item) + item.addAccount(a.name, a.path, a.address, a.emoji, a.color, icon = "", balance = 0.0) + continue + if a.walletType == WalletTypeKey: + let diffImports = countOfKeyPairsForType(items, KeyPairType.PrivateKeyImport) + if item.isNil: + item = initKeyPairItem(pubKey = "", + name = "Key " & $(diffImports + 1), # string created here should be transalted, but so far it's like it is + image = "", + icon = "key_pair_private_key", + pairType = KeyPairType.SeedImport, + derivedFrom = a.derivedfrom) + items.add(item) + item.addAccount(a.name, a.path, a.address, a.emoji, a.color, icon = "", balance = 0.0) + continue + self.view.createKeyPairModel(items) + if items.len == 0: + debug "sm_there is no any key pair for the logged in user that is not already migrated to a keycard" + return + self.view.setSelectedKeyPairByTheAddressItIsDerivedFrom(items[0].derivedFrom()) + +method runFlow*[T](self: Module[T], flowToRun: FlowType) = + if flowToRun == FlowType.General: + self.controller.terminateCurrentFlow(lastStepInTheCurrentFlow = false) + error "sm_cannot run an general flow" + return if not self.initialized: self.controller.init() - self.view.setCurrentState(newPluginReaderState(FlowType.FactoryReset, nil)) - self.controller.runGetMetadataFlow() \ No newline at end of file + if flowToRun == FlowType.FactoryReset: + self.prepareKeyPairsModel() + self.tmpLocalState = newReadingKeycardState(flowToRun, nil) + self.controller.runGetMetadataFlow() + return + if flowToRun == FlowType.SetupNewKeycard: + self.prepareKeyPairsModel() + self.view.setCurrentState(newSelectExistingKeyPairState(flowToRun, nil)) + self.controller.readyToDisplayPopup() + return + +method setSelectedKeyPair*[T](self: Module[T], item: KeyPairItem) = + var paths: seq[string] + for a in item.accountsAsArr(): + paths.add(a.path) + self.controller.setSelectedKeyPairIsProfile(item.pairType == KeyPairType.Profile) + self.controller.setSelectedKeyPairName(item.name) + self.controller.setSelectedKeyPairWalletPaths(paths) + +proc generateRandomColor[T](self: Module[T]): string = + let r = rand(0 .. 255) + let g = rand(0 .. 255) + let b = rand(0 .. 255) + return "#" & r.toHex(2) & g.toHex(2) & b.toHex(2) + +proc updateKeyPairItemIfDataAreKnown[T](self: Module[T], address: string, item: var KeyPairItem): bool = + let accounts = self.controller.getWalletAccounts() + for a in accounts: + if a.isChat or a.walletType == WalletTypeWatch or cmpIgnoreCase(a.address, address) != 0: + continue + var icon = "" + if a.walletType == WalletTypeDefaultStatusAccount: + icon = "wallet" + item.addAccount(a.name, a.path, a.address, a.emoji, a.color, icon, balance = 0.0) + return true + return false + +method setKeyPairStoredOnKeycard*[T](self: Module[T], cardMetadata: CardMetadata) = + var item = initKeyPairItem(pubKey = "", + name = cardMetadata.name, + image = "", + icon = "keycard", + pairType = KeyPairType.Unknown, + derivedFrom = "") + var knownKeyPair = true + for wa in cardMetadata.walletAccounts: + if self.updateKeyPairItemIfDataAreKnown(wa.address, item): + continue + let balance = self.controller.getBalanceForAddress(wa.address) + knownKeyPair = false + item.addAccount(name = "", wa.path, wa.address, emoji = "", color = self.generateRandomColor(), icon = "wallet", balance) + self.view.setKeyPairStoredOnKeycardIsKnown(knownKeyPair) + self.view.setKeyPairStoredOnKeycard(item) + \ No newline at end of file diff --git a/src/app/modules/shared_modules/keycard_popup/view.nim b/src/app/modules/shared_modules/keycard_popup/view.nim index ae89abe18d..05ae810bb6 100644 --- a/src/app/modules/shared_modules/keycard_popup/view.nim +++ b/src/app/modules/shared_modules/keycard_popup/view.nim @@ -1,6 +1,7 @@ import NimQml import io_interface import internal/[state, state_wrapper] +import models/[key_pair_model, key_pair_item, key_pair_selected_item] QtObject: type @@ -8,11 +9,30 @@ QtObject: delegate: io_interface.AccessInterface currentState: StateWrapper currentStateVariant: QVariant + keyPairModel: KeyPairModel + keyPairModelVariant: QVariant + selectedKeyPairItem: KeyPairSelectedItem + selectedKeyPairItemVariant: QVariant + keyPairStoredOnKeycardIsKnown: bool + keyPairStoredOnKeycard: KeyPairSelectedItem + keyPairStoredOnKeycardVariant: QVariant keycardData: string # used to temporary store the data coming from keycard, depends on current state different data may be stored proc delete*(self: View) = self.currentStateVariant.delete self.currentState.delete + if not self.keyPairModel.isNil: + self.keyPairModel.delete + if not self.keyPairModelVariant.isNil: + self.keyPairModelVariant.delete + if not self.selectedKeyPairItem.isNil: + self.selectedKeyPairItem.delete + if not self.selectedKeyPairItemVariant.isNil: + self.selectedKeyPairItemVariant.delete + if not self.keyPairStoredOnKeycard.isNil: + self.keyPairStoredOnKeycard.delete + if not self.keyPairStoredOnKeycardVariant.isNil: + self.keyPairStoredOnKeycardVariant.delete self.QObject.delete proc newView*(delegate: io_interface.AccessInterface): View = @@ -25,6 +45,7 @@ QtObject: signalConnect(result.currentState, "backActionClicked()", result, "onBackActionClicked()", 2) signalConnect(result.currentState, "primaryActionClicked()", result, "onPrimaryActionClicked()", 2) signalConnect(result.currentState, "secondaryActionClicked()", result, "onSecondaryActionClicked()", 2) + signalConnect(result.currentState, "tertiaryActionClicked()", result, "onTertiaryActionClicked()", 2) proc currentStateObj*(self: View): State = return self.currentState.getStateObj() @@ -57,5 +78,84 @@ QtObject: proc onSecondaryActionClicked*(self: View) {.slot.} = self.delegate.onSecondaryActionClicked() - proc runFactoryResetFlow*(self: View) {.slot.} = - self.delegate.runFactoryResetFlow() \ No newline at end of file + proc onTertiaryActionClicked*(self: View) {.slot.} = + self.delegate.onTertiaryActionClicked() + + proc keyPairModel*(self: View): KeyPairModel = + return self.keyPairModel + + proc keyPairModelChanged(self: View) {.signal.} + proc getKeyPairModel(self: View): QVariant {.slot.} = + return self.keyPairModelVariant + QtProperty[QVariant] keyPairModel: + read = getKeyPairModel + notify = keyPairModelChanged + + proc createKeyPairModel*(self: View, items: seq[KeyPairItem]) = + if self.keyPairModel.isNil: + self.keyPairModel = newKeyPairModel() + if self.keyPairModelVariant.isNil: + self.keyPairModelVariant = newQVariant(self.keyPairModel) + if self.selectedKeyPairItem.isNil: + self.selectedKeyPairItem = newKeyPairSelectedItem() + if self.selectedKeyPairItemVariant.isNil: + self.selectedKeyPairItemVariant = newQVariant(self.selectedKeyPairItem) + if self.keyPairStoredOnKeycard.isNil: + self.keyPairStoredOnKeycard = newKeyPairSelectedItem() + if self.keyPairStoredOnKeycardVariant.isNil: + self.keyPairStoredOnKeycardVariant = newQVariant(self.keyPairStoredOnKeycard) + self.keyPairModel.setItems(items) + self.keyPairModelChanged() + + proc getSelectedKeyPairItem*(self: View): QVariant {.slot.} = + return self.selectedKeyPairItemVariant + QtProperty[QVariant] selectedKeyPairItem: + read = getSelectedKeyPairItem + proc setSelectedKeyPairByTheAddressItIsDerivedFrom*(self: View, address: string) {.slot.} = + let item = self.keyPairModel.findItemByDerivedFromAddress(address) + self.delegate.setSelectedKeyPair(item) + self.selectedKeyPairItem.setItem(item) + + proc getKeyPairStoredOnKeycardIsKnown*(self: View): bool {.slot.} = + return self.keyPairStoredOnKeycardIsKnown + QtProperty[bool] keyPairStoredOnKeycardIsKnown: + read = getKeyPairStoredOnKeycardIsKnown + proc setKeyPairStoredOnKeycardIsKnown*(self: View, value: bool) = + self.keyPairStoredOnKeycardIsKnown = value + + proc getKeyPairStoredOnKeycard*(self: View): QVariant {.slot.} = + return self.keyPairStoredOnKeycardVariant + QtProperty[QVariant] keyPairStoredOnKeycard: + read = getKeyPairStoredOnKeycard + proc setKeyPairStoredOnKeycard*(self: View, item: KeyPairItem) = + self.keyPairStoredOnKeycard.setItem(item) + + proc setPin*(self: View, value: string) {.slot.} = + self.delegate.setPin(value) + + proc setPassword*(self: View, value: string) {.slot.} = + self.delegate.setPassword(value) + + proc checkRepeatedKeycardPinWhileTyping*(self: View, pin: string): bool {.slot.} = + return self.delegate.checkRepeatedKeycardPinWhileTyping(pin) + + proc getMnemonic*(self: View): string {.slot.} = + return self.delegate.getMnemonic() + + proc setSeedPhrase*(self: View, value: string) {.slot.} = + self.delegate.setSeedPhrase(value) + + proc getSeedPhrase*(self: View): string {.slot.} = + return self.delegate.getSeedPhrase() + + proc validSeedPhrase*(self: View, value: string): bool {.slot.} = + return self.delegate.validSeedPhrase(value) + + proc loggedInUserUsesBiometricLogin*(self: View): bool {.slot.} = + return self.delegate.loggedInUserUsesBiometricLogin() + + proc migratingProfileKeyPair*(self: View): bool {.slot.} = + return self.delegate.migratingProfileKeyPair() + + proc isProfileKeyPairMigrated*(self: View): bool {.slot.} = + return self.delegate.isProfileKeyPairMigrated() \ No newline at end of file diff --git a/src/app_service/service/wallet_account/dto.nim b/src/app_service/service/wallet_account/dto.nim index dd66c7f06c..145eea6d08 100644 --- a/src/app_service/service/wallet_account/dto.nim +++ b/src/app_service/service/wallet_account/dto.nim @@ -2,6 +2,12 @@ import tables, json, sequtils, sugar, strutils include ../../common/json_utils +const WalletTypeDefaultStatusAccount* = "" +const WalletTypeGenerated* = "generated" +const WalletTypeSeed* = "seed" +const WalletTypeWatch* = "watch" +const WalletTypeKey* = "key" + type BalanceDto* = object balance*: float64 currencyBalance*: float64 diff --git a/ui/app/AppLayouts/Profile/ProfileLayout.qml b/ui/app/AppLayouts/Profile/ProfileLayout.qml index 86635ae45e..01c596ea86 100644 --- a/ui/app/AppLayouts/Profile/ProfileLayout.qml +++ b/ui/app/AppLayouts/Profile/ProfileLayout.qml @@ -253,8 +253,8 @@ StatusSectionLayout { implicitWidth: parent.width implicitHeight: parent.height - keycardStore: profileView.store.keycardStore - sectionTitle: profileView.store.getNameForSubsection(Constants.settingsSubsection.keycard) + keycardStore: root.store.keycardStore + sectionTitle: root.store.getNameForSubsection(Constants.settingsSubsection.keycard) contentWidth: d.contentWidth } } diff --git a/ui/app/AppLayouts/Profile/stores/KeycardStore.qml b/ui/app/AppLayouts/Profile/stores/KeycardStore.qml index e4edaec603..fcf07ae1bc 100644 --- a/ui/app/AppLayouts/Profile/stores/KeycardStore.qml +++ b/ui/app/AppLayouts/Profile/stores/KeycardStore.qml @@ -6,4 +6,7 @@ QtObject { property var keycardModule + function runSetupKeycardPopup() { + root.keycardModule.runSetupKeycardPopup() + } } diff --git a/ui/app/AppLayouts/Profile/views/KeycardView.qml b/ui/app/AppLayouts/Profile/views/KeycardView.qml index c8225126e4..d0d975b119 100644 --- a/ui/app/AppLayouts/Profile/views/KeycardView.qml +++ b/ui/app/AppLayouts/Profile/views/KeycardView.qml @@ -12,10 +12,12 @@ import utils 1.0 import shared.panels 1.0 import shared.controls 1.0 import shared.status 1.0 +import shared.popups.keycard 1.0 import "../stores" import "../controls" import "../panels" +import "../popups" SettingsContentBase { id: root @@ -33,6 +35,29 @@ SettingsContentBase { id: contentColumn spacing: Constants.settingsSection.itemSpacing + Connections { + target: root.keycardStore.keycardModule + + onDisplayKeycardSharedModuleFlow: { + keycardPopup.active = true + } + onDestroyKeycardSharedModuleFlow: { + keycardPopup.active = false + } + } + + Loader { + id: keycardPopup + active: false + sourceComponent: KeycardPopup { + sharedKeycardModule: root.keycardStore.keycardModule.keycardSharedModule + } + + onLoaded: { + keycardPopup.item.open() + } + } + Image { Layout.alignment: Qt.AlignCenter Layout.preferredHeight: sourceSize.height @@ -70,8 +95,8 @@ SettingsContentBase { color: Theme.palette.baseColor1 } ] - sensor.onClicked: { - console.warn("TODO: Run Set up Keycard flow...") + onClicked: { + root.keycardStore.runSetupKeycardPopup() } } @@ -92,7 +117,7 @@ SettingsContentBase { color: Theme.palette.baseColor1 } ] - sensor.onClicked: { + onClicked: { console.warn("TODO: Generate a seed phrase...") } } @@ -107,7 +132,7 @@ SettingsContentBase { color: Theme.palette.baseColor1 } ] - sensor.onClicked: { + onClicked: { console.warn("TODO: Import or restore via a seed phrase...") } } @@ -122,7 +147,7 @@ SettingsContentBase { color: Theme.palette.baseColor1 } ] - sensor.onClicked: { + onClicked: { console.warn("TODO: Import from Keycard to Status Desktop...") } } @@ -144,7 +169,7 @@ SettingsContentBase { color: Theme.palette.baseColor1 } ] - sensor.onClicked: { + onClicked: { console.warn("TODO: Check what’s on a Keycard...") } } @@ -159,7 +184,7 @@ SettingsContentBase { color: Theme.palette.baseColor1 } ] - sensor.onClicked: { + onClicked: { console.warn("TODO: Factory reset a Keycard...") } } diff --git a/ui/imports/shared/popups/keycard/KeycardPopup.qml b/ui/imports/shared/popups/keycard/KeycardPopup.qml index d492a02963..f157aa2bc4 100644 --- a/ui/imports/shared/popups/keycard/KeycardPopup.qml +++ b/ui/imports/shared/popups/keycard/KeycardPopup.qml @@ -15,30 +15,91 @@ StatusModal { property var sharedKeycardModule - width: 640 - height: 640 - margins: 8 + width: Constants.keycard.general.popupWidth + height: { + if (!root.sharedKeycardModule.keyPairStoredOnKeycardIsKnown) { + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata) { + return Constants.keycard.general.popupBiggerHeight + } + } + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata) { + return Constants.keycard.general.popupBiggerHeight + } + } + } + return Constants.keycard.general.popupHeight + } + margins: Style.current.halfPadding anchors.centerIn: parent - closePolicy: d.resetInProgress? Popup.NoAutoClose : Popup.CloseOnEscape + closePolicy: d.disablePopupClose? Popup.NoAutoClose : Popup.CloseOnEscape - header.title: qsTr("Factory reset a Keycard") + header.title: { + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + return qsTr("Set up a new Keycard with an existing account") + } + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + return qsTr("Factory reset a Keycard") + } + return "" + } QtObject { id: d - property bool factoryResetConfirmed: false - property bool resetInProgress: d.factoryResetConfirmed && root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard + property bool primaryButtonEnabled: false + property bool seedPhraseRevealed: false + property bool disablePopupClose: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.migratingKeyPair || + (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateSuccess && + root.sharedKeycardModule.migratingProfileKeyPair()) - onResetInProgressChanged: { - hasCloseButton = !resetInProgress + onDisablePopupCloseChanged: { + hasCloseButton = !disablePopupClose } } onClosed: { - // for all states but the `factoryResetConfirmation` cancel the flow is primary action - if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation) - { - root.sharedKeycardModule.currentState.doSecondaryAction() - return + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.selectExistingKeyPair || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardNotEmpty || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmptyMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.createPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.repeatPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinSet || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinVerified || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.maxPinRetriesReached || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetSuccess || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseDisplay || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseEnterWords || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterSeedPhrase || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongSeedPhrase) + { + root.sharedKeycardModule.currentState.doSecondaryAction() + return + } + } + else if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.selectExistingKeyPair || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinVerified || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.maxPinRetriesReached || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmptyMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation) + { + root.sharedKeycardModule.currentState.doSecondaryAction() + return + } } root.sharedKeycardModule.currentState.doPrimaryAction() } @@ -50,19 +111,53 @@ StatusModal { sourceComponent: { if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pluginReader || root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.insertKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardInserted || root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateSuccess || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateFailure || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.migratingKeyPair || root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetSuccess || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmptyMetadata || root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmpty || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardNotEmpty || root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.notKeycard || - root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.recognizedKeycard) + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.maxPinRetriesReached || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.recognizedKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay) { return initComponent } - - if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation) + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata) { return confirmationComponent } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.selectExistingKeyPair) + { + return selectKeyPairComponent + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.createPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.repeatPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinSet || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinVerified) + { + return keycardPinComponent + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterSeedPhrase || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongSeedPhrase) + { + return enterSeedPhraseComponent + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseDisplay) + { + return seedPhraseComponent + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseEnterWords) + { + return enterSeedPhraseWordsComponent + } return undefined } @@ -80,8 +175,72 @@ StatusModal { KeycardConfirmation { sharedKeycardModule: root.sharedKeycardModule + Component.onCompleted: { + d.primaryButtonEnabled = false + } + onConfirmationUpdated: { - d.factoryResetConfirmed = value + d.primaryButtonEnabled = value + } + } + } + + Component { + id: selectKeyPairComponent + SelectKeyPair { + sharedKeycardModule: root.sharedKeycardModule + } + } + + Component { + id: keycardPinComponent + KeycardPin { + sharedKeycardModule: root.sharedKeycardModule + } + } + + Component { + id: enterSeedPhraseComponent + EnterSeedPhrase { + sharedKeycardModule: root.sharedKeycardModule + + Component.onCompleted: { + d.primaryButtonEnabled = false + } + + onValidation: { + d.primaryButtonEnabled = result + } + } + } + + Component { + id: seedPhraseComponent + SeedPhrase { + sharedKeycardModule: root.sharedKeycardModule + + Component.onCompleted: { + hideSeed = !d.seedPhraseRevealed + d.primaryButtonEnabled = Qt.binding(function(){ return d.seedPhraseRevealed }) + } + + onSeedPhraseRevealed: { + d.seedPhraseRevealed = true + } + } + } + + Component { + id: enterSeedPhraseWordsComponent + EnterSeedPhraseWords { + sharedKeycardModule: root.sharedKeycardModule + + Component.onCompleted: { + d.primaryButtonEnabled = false + } + + onValidation: { + d.primaryButtonEnabled = result } } } @@ -91,6 +250,8 @@ StatusModal { StatusBackButton { id: backButton visible: root.sharedKeycardModule.currentState.displayBackButton + height: primaryButton.height + width: primaryButton.height onClicked: { root.sharedKeycardModule.currentState.backAction() } @@ -101,13 +262,62 @@ StatusModal { StatusButton { id: secondaryButton text: { - if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation) - return qsTr("Cancel") + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.selectExistingKeyPair || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardNotEmpty || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmptyMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.maxPinRetriesReached || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetSuccess || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinVerified || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay) { + return qsTr("Cancel") + } + } + else if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinVerified || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.maxPinRetriesReached || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmptyMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation) + return qsTr("Cancel") + } return "" } visible: { - if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation) - return true + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.selectExistingKeyPair || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardNotEmpty || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmptyMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.maxPinRetriesReached || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetSuccess || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinVerified || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay) { + return true + } + } + else if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinVerified || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.maxPinRetriesReached || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmptyMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation) { + return true + } + } return false } highlighted: focus @@ -119,20 +329,130 @@ StatusModal { StatusButton { id: primaryButton text: { - if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation) - return qsTr("Factory reset this Keycard") - if (d.resetInProgress || - root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetSuccess) - return qsTr("Done") - return qsTr("Cancel") + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pluginReader || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.insertKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardInserted || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.recognizedKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.createPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.repeatPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinSet) { + return qsTr("Input seed phrase") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseEnterWords) { + return qsTr("Yes, migrate key pair to this Keycard") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterSeedPhrase) { + return qsTr("Yes, migrate key pair to Keycard") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongSeedPhrase) { + return qsTr("Try entering seed phrase again") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardNotEmpty) { + return qsTr("Check what is stored on this Keycard") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin) { + return qsTr("I don’t know the pin") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata) { + return qsTr("Factory reset this Keycard") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.selectExistingKeyPair || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmptyMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetSuccess || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseDisplay || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinVerified || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay) { + return qsTr("Next") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.maxPinRetriesReached) { + return qsTr("Tmp-Next") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.migratingKeyPair || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateFailure) { + return qsTr("Done") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateSuccess && + root.sharedKeycardModule.migratingProfileKeyPair()) { + return qsTr("Restart app & sign in using your new Keycard") + } + return qsTr("Cancel") + } + else if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin) { + return qsTr("I don’t know the pin") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata) { + return qsTr("Factory reset this Keycard") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinVerified || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmptyMetadata) { + return qsTr("Next") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.maxPinRetriesReached) { + return qsTr("Tmp-Next") + } + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetSuccess) { + return qsTr("Done") + } + return qsTr("Cancel") + } + return "" } enabled: { - if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation) - return d.factoryResetConfirmed - if (d.resetInProgress) - return false + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.migratingKeyPair) { + if (d.disablePopupClose) { + return false + } + } + else if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseDisplay || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseEnterWords || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterSeedPhrase) { + return d.primaryButtonEnabled + } + if ((root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.selectExistingKeyPair && + root.sharedKeycardModule.keyPairModel.count === 0) || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pluginReader || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.insertKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardInserted || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.recognizedKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.createPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.repeatPin) { + return false + } + } + else if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmation || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata) { + return d.primaryButtonEnabled + } + } return true } + icon.name: { + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseEnterWords || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterSeedPhrase) { + if (root.sharedKeycardModule.migratingProfileKeyPair()) { + if (root.sharedKeycardModule.loggedInUserUsesBiometricLogin()) + return "touch-id" + return "password" + } + } + } + return "" + } highlighted: focus onClicked: { diff --git a/ui/imports/shared/popups/keycard/helpers/KeyPairItem.qml b/ui/imports/shared/popups/keycard/helpers/KeyPairItem.qml new file mode 100644 index 0000000000..6dadd6f692 --- /dev/null +++ b/ui/imports/shared/popups/keycard/helpers/KeyPairItem.qml @@ -0,0 +1,117 @@ +import QtQuick 2.14 +import QtQml.Models 2.14 +import QtQuick.Controls 2.14 + +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 + +import utils 1.0 + +StatusListItem { + id: root + + property var sharedKeycardModule + property ButtonGroup buttonGroup + property bool usedAsSelectOption: false + + property string keyPairPubKey: "" + property string keyPairName: "" + property string keyPairIcon: "" + property string keyPairImage: "" + property string keyPairDerivedFrom: "" + property string keyPairAccounts: "" + + color: Style.current.grey + title: root.keyPairName + titleAsideText: Utils.getElidedCompressedPk(root.keyPairPubKey) + + image { + width: 40 + height: 40 + source: root.keyPairImage + } + + icon { + width: root.keyPairIcon? 24 : 40 + height: root.keyPairIcon? 24 : 40 + name: root.keyPairIcon + color: Utils.colorForPubkey(root.keyPairPubKey) + letterSize: Math.max(4, this.image.width / 2.4) + charactersLen: 2 + isLetterIdenticon: !root.keyPairIcon && !this.image.source.toString() + background.color: Theme.palette.primaryColor3 + } + + ringSettings { + ringSpecModel: Utils.getColorHashAsJson(root.keyPairPubKey) + ringPxSize: Math.max(this.icon.width / 24.0) + } + + tagsModel: ListModel{} + + tagsDelegate: StatusListItemTag { + color: model.color + height: Style.current.bigPadding + radius: 6 + closeButtonVisible: false + icon { + emoji: model.emoji + emojiSize: Emoji.size.verySmall + isLetterIdenticon: !!model.emoji + name: model.icon + color: Theme.palette.indirectColor1 + width: 16 + height: 16 + } + title: model.name + titleText.font.pixelSize: 12 + titleText.color: Theme.palette.indirectColor1 + } + + components: [ + StatusRadioButton { + visible: root.usedAsSelectOption + ButtonGroup.group: root.buttonGroup + onCheckedChanged: { + if (!root.usedAsSelectOption) + return + let checkCondition = root.sharedKeycardModule.selectedKeyPairItem.derivedFrom === root.keyPairDerivedFrom + if (checked && checked != checkCondition) { + root.sharedKeycardModule.setSelectedKeyPairByTheAddressItIsDerivedFrom(root.keyPairDerivedFrom) + } + } + Component.onCompleted: { + if (!root.usedAsSelectOption) + return + checked = Qt.binding(function() { + return root.sharedKeycardModule.selectedKeyPairItem.derivedFrom === root.keyPairDerivedFrom + }) + } + } + ] + + Component.onCompleted: { + if (root.keyPairAccounts === "") { + // should never be here, as it's not possible to have keypair item without at least a single account + console.debug("accounts list is empty for selecting keycard pair") + return + } + let obj = JSON.parse(root.keyPairAccounts) + if (obj.error) { + console.debug("error parsing accounts for selecting keycard pair, error: ", obj.error) + return + } + + for (var i=0; i 0 && root.pattern !== "" + d.restart() + } + + onStartImgIndexForTheFirstLoopChanged: { + d.restart() + } + + onEndImgIndexChanged: { + d.restart() + } + + onDurationChanged: { + d.isAnimation = root.duration > 0 && root.pattern !== "" + d.restart() + } + + Image { + id: img + anchors.fill: parent + fillMode: Image.PreserveAspectFit + antialiasing: true + mipmap: true + source: d.isAnimation? + Style.png(root.pattern.arg(img.currentImgIndex)) : + root.source + + property int currentImgIndex: root.startImgIndexForTheFirstLoop + + onCurrentImgIndexChanged: { + if (currentImgIndex == root.endImgIndex) { + + if (d.currentLoop === root.loops && root.loops > -1) { + animation.stop() + root.animationCompleted() + return + } + if (d.currentLoop === 1 && (root.loops === -1 || root.loops > 1)) { + animation.stop() + animation.duration = root.duration / (root.endImgIndex + 1) * (root.endImgIndex - root.startImgIndexForOtherLoops) + animation.from = root.startImgIndexForOtherLoops + animation.to = root.endImgIndex + animation.loops = root.loops == -1? Animation.Infinite : root.loops + animation.start() + } + + d.currentLoop += 1 + } + } + + NumberAnimation on currentImgIndex { + id: animation + from: root.startImgIndexForTheFirstLoop + to: root.endImgIndex + duration: root.duration + } + } +} diff --git a/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml b/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml new file mode 100644 index 0000000000..db3236aecc --- /dev/null +++ b/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml @@ -0,0 +1,328 @@ +import QtQuick 2.14 +import QtQuick.Layouts 1.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 + +import utils 1.0 +import shared.stores 1.0 +import shared.controls 1.0 + +Item { + id: root + + property var sharedKeycardModule + + signal validation(bool result) + + QtObject { + id: d + + property bool allEntriesValid: false + property var mnemonicInput: [] + readonly property var tabs: [12, 18, 24] + readonly property ListModel seedPhrases_en: BIP39_en {} + property bool wrongSeedPhrase: root.sharedKeycardModule.keycardData & Constants.predefinedKeycardData.wrongSeedPhrase + + onWrongSeedPhraseChanged: { + if (wrongSeedPhrase) { + invalidSeedTxt.text = qsTr("The phrase you’ve entered does not match this Keycard’s seed phrase") + invalidSeedTxt.visible = true + } + else { + invalidSeedTxt.text = "" + invalidSeedTxt.visible = false + } + } + + onAllEntriesValidChanged: { + if (d.allEntriesValid) { + let mnemonicString = "" + const sortTable = mnemonicInput.sort((a, b) => a.pos - b.pos) + for (let i = 0; i < mnemonicInput.length; i++) { + d.checkWordExistence(sortTable[i].seed) + mnemonicString += sortTable[i].seed + ((i === (grid.count-1)) ? "" : " ") + } + + if (Utils.isMnemonic(mnemonicString) && root.sharedKeycardModule.validSeedPhrase(mnemonicString)) { + root.sharedKeycardModule.setSeedPhrase(mnemonicString) + } else { + invalidSeedTxt.text = qsTr("Invalid seed phrase") + invalidSeedTxt.visible = true + d.allEntriesValid = false + } + } + root.validation(d.allEntriesValid) + } + + function checkMnemonicLength() { + d.allEntriesValid = d.mnemonicInput.length === d.tabs[switchTabBar.currentIndex] + } + + function checkWordExistence(word) { + d.allEntriesValid = d.allEntriesValid && d.seedPhrases_en.words.includes(word) + if (d.allEntriesValid) { + invalidSeedTxt.text = "" + invalidSeedTxt.visible = false + } + else { + invalidSeedTxt.text = qsTr("The phrase you’ve entered is invalid") + invalidSeedTxt.visible = true + } + } + + function pasteWords () { + const clipboardText = globalUtils.getFromClipboard() + // Split words separated by commas and or blank spaces (spaces, enters, tabs) + const words = clipboardText.split(/[, \s]+/) + + let index = d.tabs.indexOf(words.length) + if (index === -1) { + return false + } + + let timeout = 0 + if (switchTabBar.currentIndex !== index) { + switchTabBar.currentIndex = index + // Set the teimeout to 100 so the grid has time to generate the new items + timeout = 100 + } + + d.mnemonicInput = [] + timer.setTimeout(() => { + // Populate mnemonicInput + for (let i = 0; i < words.length; i++) { + grid.addWord(i + 1, words[i], true) + } + // Populate grid + for (let j = 0; j < grid.count; j++) { + const item = grid.itemAtIndex(j) + if (!item || !item.leftComponentText) { + // The grid has gaps in it and also sometimes doesn't return the item correctly when offscreen + // in those cases, we just add the word in the array but not in the grid. + // The button will still work and import correctly. The Grid itself will be partly empty, but offscreen + // With the re-design of the grid, this should be fixed + continue + } + const pos = item.mnemonicIndex + item.setWord(words[pos - 1]) + } + d.checkMnemonicLength() + }, timeout) + return true + } + } + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Style.current.xlPadding + anchors.bottomMargin: Style.current.halfPadding + anchors.leftMargin: Style.current.xlPadding + anchors.rightMargin: Style.current.xlPadding + spacing: Style.current.padding + clip: true + + StatusBaseText { + id: title + Layout.preferredHeight: Constants.keycard.general.titleHeight + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter key pair seed phrase") + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + } + + Timer { + id: timer + } + + StatusSwitchTabBar { + id: switchTabBar + Layout.alignment: Qt.AlignHCenter + Repeater { + model: d.tabs + StatusSwitchTabButton { + text: qsTr("%1 words").arg(modelData) + id: seedPhraseWords + objectName: `${modelData}SeedButton` + } + } + onCurrentIndexChanged: { + d.mnemonicInput = d.mnemonicInput.filter(function(value) { + return value.pos <= d.tabs[switchTabBar.currentIndex] + }) + d.checkMnemonicLength() + } + } + + StatusGridView { + id: grid + readonly property var wordIndex: [ + ["1", "3", "5", "7", "9", "11", "2", "4", "6", "8", "10", "12"] + ,["1", "4", "7", "10", "13", "16", "2", "5", "8", + "11", "14", "17", "3", "6", "9", "12", "15", "18"] + ,["1", "5", "9", "13", "17", "21", "2", "6", "10", "14", "18", "22", + "3", "7", "11", "15", "19", "23", "4", "8", "12", "16", "20", "24"] + ] + Layout.preferredWidth: parent.width + Layout.preferredHeight: 312 + clip: false + flow: GridView.FlowTopToBottom + cellWidth: (parent.width/(count/6)) + cellHeight: 52 + interactive: false + z: 100000 + cacheBuffer: 9999 + model: switchTabBar.currentItem.text.substring(0,2) + + function addWord(pos, word, ignoreGoingNext = false) { + d.mnemonicInput.push({pos: pos, seed: word.replace(/\s/g, '')}) + + for (let j = 0; j < d.mnemonicInput.length; j++) { + if (d.mnemonicInput[j].pos === pos && d.mnemonicInput[j].seed !== word) { + d.mnemonicInput[j].seed = word + break + } + } + //remove duplicates + const valueArr = d.mnemonicInput.map(item => item.pos) + const isDuplicate = valueArr.some((item, idx) => { + if (valueArr.indexOf(item) !== idx) { + d.mnemonicInput.splice(idx, 1) + } + return valueArr.indexOf(item) !== idx + }) + if (!ignoreGoingNext) { + for (let i = 0; i < grid.count; i++) { + if (grid.itemAtIndex(i).mnemonicIndex !== (pos + 1)) { + continue + } + + grid.currentIndex = grid.itemAtIndex(i).itemIndex + grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus() + + if (grid.currentIndex !== 12) { + continue + } + + grid.positionViewAtEnd() + + if (grid.count === 20) { + grid.contentX = 1500 + } + } + } + d.checkMnemonicLength() + } + + delegate: StatusSeedPhraseInput { + id: seedWordInput + textEdit.input.edit.objectName: `statusSeedPhraseInputField${seedWordInput.leftComponentText}` + width: (grid.cellWidth - 8) + height: (grid.cellHeight - 8) + Behavior on width { NumberAnimation { duration: 180 } } + textEdit.text: { + const pos = seedWordInput.mnemonicIndex + for (let i in d.mnemonicInput) { + const p = d.mnemonicInput[i] + if (p.pos === pos) { + return p.seed + } + } + return "" + } + + readonly property int mnemonicIndex: grid.wordIndex[(grid.count / 6) - 2][index] + + leftComponentText: mnemonicIndex + inputList: d.seedPhrases_en + + property int itemIndex: index + z: (grid.currentIndex === index) ? 150000000 : 0 + onTextChanged: { + d.checkWordExistence(text) + } + onDoneInsertingWord: { + grid.addWord(mnemonicIndex, word) + } + onEditClicked: { + grid.currentIndex = index + grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus() + } + onKeyPressed: { + grid.currentIndex = index + + if (event.key === Qt.Key_Backtab) { + for (let i = 0; i < grid.count; i++) { + if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex - 1) >= 0 ? (mnemonicIndex - 1) : 0)) { + grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.BacktabFocusReason) + textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit + event.accepted = true + break + } + } + } else if (event.key === Qt.Key_Tab) { + for (let i = 0; i < grid.count; i++) { + if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex + 1) <= grid.count ? (mnemonicIndex + 1) : grid.count)) { + grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.TabFocusReason) + textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit + event.accepted = true + break + } + } + } + + if (event.matches(StandardKey.Paste)) { + if (d.pasteWords()) { + // Paste was done by splitting the words + event.accepted = true + } + return + } + + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + event.accepted = true + if (d.allEntriesValid) { + d.sharedKeycardModule.currentState.doPrimaryAction() + return + } + } + + if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) { + const wordIndex = d.mnemonicInput.findIndex(x => x.pos === mnemonicIndex) + if (wordIndex > -1) { + d.mnemonicInput.splice(wordIndex, 1) + d.checkMnemonicLength() + } + } + } + Component.onCompleted: { + const item = grid.itemAtIndex(0) + if (item) { + item.textEdit.input.edit.forceActiveFocus() + } + } + } + } + + StatusBaseText { + id: invalidSeedTxt + Layout.alignment: Qt.AlignHCenter + color: Theme.palette.dangerColor1 + visible: false + } + } + + states: [ + State { + name: Constants.keycardSharedState.enterSeedPhrase + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterSeedPhrase + }, + State { + name: Constants.keycardSharedState.wrongSeedPhrase + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongSeedPhrase + } + ] +} diff --git a/ui/imports/shared/popups/keycard/states/EnterSeedPhraseWords.qml b/ui/imports/shared/popups/keycard/states/EnterSeedPhraseWords.qml new file mode 100644 index 0000000000..b303ae2fb5 --- /dev/null +++ b/ui/imports/shared/popups/keycard/states/EnterSeedPhraseWords.qml @@ -0,0 +1,168 @@ +import QtQuick 2.14 +import QtQuick.Layouts 1.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 + +import utils 1.0 + +Item { + id: root + + property var sharedKeycardModule + + signal validation(bool result) + + QtObject { + id: d + + property bool allEntriesValid: false + readonly property var seedPhrase: root.sharedKeycardModule.getMnemonic().split(" ") + readonly property var wordNumbers: { + let numbers = [] + while (numbers.length < 3) { + let randomNo = Math.floor(Math.random() * 12) + if(numbers.indexOf(randomNo) == -1) + numbers.push(randomNo) + } + numbers.sort((a, b) => { return a < b? -1 : a > b? 1 : 0 }) + return numbers + } + + function processText(text) { + if(text.length === 0) + return "" + if(/(^\s|^\r|^\n)|(\s$|^\r$|^\n$)/.test(text)) { + return text.trim() + } + else if(/\s|\r|\n/.test(text)) { + return "" + } + return text + } + + function updateValidity() { + d.allEntriesValid = word0.valid && word1.valid && word2.valid + root.validation(d.allEntriesValid) + } + } + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Style.current.xlPadding + anchors.bottomMargin: Style.current.halfPadding + anchors.leftMargin: Style.current.xlPadding + anchors.rightMargin: Style.current.xlPadding + spacing: Style.current.padding + clip: true + + StatusBaseText { + id: title + Layout.preferredHeight: Constants.keycard.general.titleHeight + Layout.alignment: Qt.AlignHCenter + text: qsTr("Confirm seed phrase words") + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + } + + StatusInput { + id: word0 + Layout.fillWidth: true + validationMode: StatusInput.ValidationMode.Always + label: qsTr("Word #%1").arg(d.wordNumbers[0] + 1) + placeholderText: qsTr("Enter word") + validators: [ + StatusValidator { + validate: function (t) { return (d.seedPhrase[d.wordNumbers[0]] === word0.text); } + errorMessage: (word0.text.length) > 0 ? qsTr("This word doesn’t match") : "" + } + ] + + input.acceptReturn: true + input.tabNavItem: word1.input.edit + + onTextChanged: { + text = d.processText(text) + d.updateValidity() + } + + onKeyPressed: { + if (d.allEntriesValid && + (input.edit.keyEvent === Qt.Key_Return || + input.edit.keyEvent === Qt.Key_Enter)) { + event.accepted = true + root.sharedKeycardModule.currentState.doPrimaryAction() + } + } + } + + StatusInput { + id: word1 + Layout.fillWidth: true + validationMode: StatusInput.ValidationMode.Always + label: qsTr("Word #%1").arg(d.wordNumbers[1] + 1) + placeholderText: qsTr("Enter word") + validators: [ + StatusValidator { + validate: function (t) { return (d.seedPhrase[d.wordNumbers[1]] === word1.text); } + errorMessage: (word1.text.length) > 0 ? qsTr("This word doesn’t match") : "" + } + ] + + input.acceptReturn: true + input.tabNavItem: word2.input.edit + + onTextChanged: { + text = d.processText(text) + d.updateValidity() + } + + onKeyPressed: { + if (d.allEntriesValid && + (input.edit.keyEvent === Qt.Key_Return || + input.edit.keyEvent === Qt.Key_Enter)) { + event.accepted = true + root.sharedKeycardModule.currentState.doPrimaryAction() + } + } + } + + StatusInput { + id: word2 + Layout.fillWidth: true + validationMode: StatusInput.ValidationMode.Always + label: qsTr("Word #%1").arg(d.wordNumbers[2] + 1) + placeholderText: qsTr("Enter word") + validators: [ + StatusValidator { + validate: function (t) { return (d.seedPhrase[d.wordNumbers[2]] === word2.text); } + errorMessage: (word2.text.length) > 0 ? qsTr("This word doesn’t match") : "" + } + ] + + input.acceptReturn: true + + onTextChanged: { + text = d.processText(text) + d.updateValidity() + } + + onKeyPressed: { + if (d.allEntriesValid && + (input.edit.keyEvent === Qt.Key_Return || + input.edit.keyEvent === Qt.Key_Enter)) { + event.accepted = true + root.sharedKeycardModule.currentState.doPrimaryAction() + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + } +} diff --git a/ui/imports/shared/popups/keycard/states/KeycardConfirmation.qml b/ui/imports/shared/popups/keycard/states/KeycardConfirmation.qml index 32b488c67e..067d8ac0d4 100644 --- a/ui/imports/shared/popups/keycard/states/KeycardConfirmation.qml +++ b/ui/imports/shared/popups/keycard/states/KeycardConfirmation.qml @@ -8,6 +8,8 @@ import StatusQ.Controls 0.1 import utils 1.0 +import "../helpers" + Item { id: root @@ -15,42 +17,119 @@ Item { signal confirmationUpdated(bool value) - ColumnLayout { - anchors.centerIn: parent - spacing: Style.current.padding + Component { + id: knownKeyPairComponent + KeyPairItem { + keyPairPubKey: root.sharedKeycardModule.keyPairStoredOnKeycard.pubKey + keyPairName: root.sharedKeycardModule.keyPairStoredOnKeycard.name + keyPairIcon: root.sharedKeycardModule.keyPairStoredOnKeycard.icon + keyPairImage: root.sharedKeycardModule.keyPairStoredOnKeycard.image + keyPairDerivedFrom: root.sharedKeycardModule.keyPairStoredOnKeycard.derivedFrom + keyPairAccounts: root.sharedKeycardModule.keyPairStoredOnKeycard.accounts + } + } - Image { + Component { + id: unknownKeyPairCompontnt + KeyPairUnknownItem { + keyPairPubKey: root.sharedKeycardModule.keyPairStoredOnKeycard.pubKey + keyPairName: root.sharedKeycardModule.keyPairStoredOnKeycard.name + keyPairIcon: root.sharedKeycardModule.keyPairStoredOnKeycard.icon + keyPairImage: root.sharedKeycardModule.keyPairStoredOnKeycard.image + keyPairDerivedFrom: root.sharedKeycardModule.keyPairStoredOnKeycard.derivedFrom + keyPairAccounts: root.sharedKeycardModule.keyPairStoredOnKeycard.accounts + } + } + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Style.current.xlPadding + anchors.bottomMargin: Style.current.halfPadding + anchors.leftMargin: Style.current.xlPadding + anchors.rightMargin: Style.current.xlPadding + spacing: Style.current.padding + clip: true + + KeycardImage { id: image Layout.alignment: Qt.AlignHCenter Layout.preferredHeight: Constants.keycard.shared.imageHeight Layout.preferredWidth: Constants.keycard.shared.imageWidth - fillMode: Image.PreserveAspectFit - antialiasing: true - mipmap: true - source: Style.png("keycard/popup_card_red_sprayed@2x") + pattern: "keycard/strong_error/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 18 + endImgIndex: 29 + duration: 1300 + loops: -1 } StatusBaseText { id: title Layout.alignment: Qt.AlignHCenter horizontalAlignment: Text.AlignHCenter + Layout.preferredHeight: Constants.keycard.general.titleHeight wrapMode: Text.WordWrap text: qsTr("A factory reset will delete the key on this Keycard.\nAre you sure you want to do this?") - font.pixelSize: Constants.keycard.general.fontSize3 + font.pixelSize: Constants.keycard.general.fontSize2 color: Theme.palette.dangerColor1 } StatusCheckBox { id: confirmation - Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: Constants.keycard.general.messageHeight + Layout.alignment: Qt.AlignCenter leftSide: false spacing: Style.current.smallPadding - font.pixelSize: Constants.keycard.general.fontSize3 + font.pixelSize: Constants.keycard.general.fontSize2 text: qsTr("I understand the key pair on this Keycard will be deleted") onCheckedChanged: { root.confirmationUpdated(checked) } } + + Loader { + id: loader + Layout.preferredWidth: parent.width + active: { + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata) { + return true + } + } + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata) { + return true + } + } + return false + } + + sourceComponent: { + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata) { + if (root.sharedKeycardModule.keyPairStoredOnKeycardIsKnown) { + return knownKeyPairComponent + } + return unknownKeyPairCompontnt + } + } + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetConfirmationDisplayMetadata) { + if (root.sharedKeycardModule.keyPairStoredOnKeycardIsKnown) { + return knownKeyPairComponent + } + return unknownKeyPairCompontnt + } + } + } + } + + Item { + visible: !loader.active + Layout.fillWidth: true + Layout.fillHeight: visible? true : false + } } } diff --git a/ui/imports/shared/popups/keycard/states/KeycardInit.qml b/ui/imports/shared/popups/keycard/states/KeycardInit.qml index cff244f63a..831870b764 100644 --- a/ui/imports/shared/popups/keycard/states/KeycardInit.qml +++ b/ui/imports/shared/popups/keycard/states/KeycardInit.qml @@ -6,13 +6,32 @@ import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Components 0.1 +///////// Remove Later//////////// +import StatusQ.Popups 0.1 +import StatusQ.Controls 0.1 +////////////////////////////////// + import utils 1.0 +import "../helpers" + Item { id: root property var sharedKeycardModule + Component.onCompleted: { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.migratingKeyPair) { + passwordPopup.open() + } + } + + QtObject { + id: d + + property bool hideKeyPair: root.sharedKeycardModule.keycardData & Constants.predefinedKeycardData.hideKeyPair + } + Timer { id: timer interval: 1000 @@ -22,23 +41,100 @@ Item { } } - ColumnLayout { + ///////// Remove Later//////////// + StatusModal { + id: passwordPopup + width: 300 + height: 200 anchors.centerIn: parent - spacing: Style.current.padding + header.title: qsTr("Temporary Popup") + closePolicy: Popup.NoAutoClose + contentItem: Item { + StatusInput { + id: password + width: parent.width - Style.current.padding * 2 + anchors.centerIn: parent + input.clearable: true + placeholderText: qsTr("Enter password...") + } + } + rightButtons: [ + StatusButton { + id: primaryButton + text: qsTr("Next") + onClicked: { + root.sharedKeycardModule.setPassword(password.text) + passwordPopup.close() + root.sharedKeycardModule.currentState.doPrimaryAction() + } + } + ] + } + ////////////////////////////////// - Image { + Component { + id: keyPairComponent + KeyPairItem { + keyPairPubKey: root.sharedKeycardModule.selectedKeyPairItem.pubKey + keyPairName: root.sharedKeycardModule.selectedKeyPairItem.name + keyPairIcon: root.sharedKeycardModule.selectedKeyPairItem.icon + keyPairImage: root.sharedKeycardModule.selectedKeyPairItem.image + keyPairDerivedFrom: root.sharedKeycardModule.selectedKeyPairItem.derivedFrom + keyPairAccounts: root.sharedKeycardModule.selectedKeyPairItem.accounts + } + } + + Component { + id: knownKeyPairComponent + KeyPairItem { + keyPairPubKey: root.sharedKeycardModule.keyPairStoredOnKeycard.pubKey + keyPairName: root.sharedKeycardModule.keyPairStoredOnKeycard.name + keyPairIcon: root.sharedKeycardModule.keyPairStoredOnKeycard.icon + keyPairImage: root.sharedKeycardModule.keyPairStoredOnKeycard.image + keyPairDerivedFrom: root.sharedKeycardModule.keyPairStoredOnKeycard.derivedFrom + keyPairAccounts: root.sharedKeycardModule.keyPairStoredOnKeycard.accounts + } + } + + Component { + id: unknownKeyPairCompontnt + KeyPairUnknownItem { + keyPairPubKey: root.sharedKeycardModule.keyPairStoredOnKeycard.pubKey + keyPairName: root.sharedKeycardModule.keyPairStoredOnKeycard.name + keyPairIcon: root.sharedKeycardModule.keyPairStoredOnKeycard.icon + keyPairImage: root.sharedKeycardModule.keyPairStoredOnKeycard.image + keyPairDerivedFrom: root.sharedKeycardModule.keyPairStoredOnKeycard.derivedFrom + keyPairAccounts: root.sharedKeycardModule.keyPairStoredOnKeycard.accounts + } + } + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Style.current.xlPadding + anchors.bottomMargin: Style.current.halfPadding + anchors.leftMargin: Style.current.xlPadding + anchors.rightMargin: Style.current.xlPadding + spacing: Style.current.padding + clip: true + + KeycardImage { id: image Layout.alignment: Qt.AlignHCenter Layout.preferredHeight: Constants.keycard.shared.imageHeight Layout.preferredWidth: Constants.keycard.shared.imageWidth - fillMode: Image.PreserveAspectFit - antialiasing: true - mipmap: true + + onAnimationCompleted: { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardInserted || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard) { + root.sharedKeycardModule.currentState.doSecondaryAction() + } + } } Row { spacing: Style.current.halfPadding Layout.alignment: Qt.AlignCenter + Layout.preferredHeight: Constants.keycard.general.titleHeight StatusIcon { id: icon @@ -50,7 +146,8 @@ Item { } StatusLoadingIndicator { id: loading - visible: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard + visible: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.migratingKeyPair } StatusBaseText { id: title @@ -60,10 +157,71 @@ Item { StatusBaseText { id: message - Layout.alignment: Qt.AlignHCenter + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: parent.width + Layout.preferredHeight: Constants.keycard.general.messageHeight horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap } + + Loader { + id: loader + Layout.preferredWidth: parent.width + active: { + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if((root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pluginReader && !d.hideKeyPair) || + (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.insertKeycard && !d.hideKeyPair) || + (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardInserted && !d.hideKeyPair) || + (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard && !d.hideKeyPair) || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.recognizedKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateSuccess || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateFailure || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay) { + return true + } + } + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + if(root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay) { + return true + } + } + return false + } + + sourceComponent: { + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard) { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay) { + if (root.sharedKeycardModule.keyPairStoredOnKeycardIsKnown) { + return knownKeyPairComponent + } + return unknownKeyPairCompontnt + } + if ((root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pluginReader && !d.hideKeyPair) || + (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.insertKeycard && !d.hideKeyPair) || + (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardInserted && !d.hideKeyPair) || + (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard && !d.hideKeyPair) || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.recognizedKeycard || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateSuccess || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateFailure) { + return keyPairComponent + } + } + if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.factoryReset) { + if(root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay) { + if (root.sharedKeycardModule.keyPairStoredOnKeycardIsKnown) { + return knownKeyPairComponent + } + return unknownKeyPairCompontnt + } + } + } + } + + Item { + visible: !loader.active + Layout.fillWidth: true + Layout.fillHeight: this.visible? true : false + } } states: [ @@ -79,12 +237,12 @@ Item { } PropertyChanges { target: image - source: Style.png("keycard/popup_card_reader@2x") + source: Style.png("keycard/empty-reader") + pattern: "" } PropertyChanges { target: message text: "" - visible: false } }, State { @@ -99,16 +257,48 @@ Item { } PropertyChanges { target: image - source: Style.png("keycard/popup_insert_card@2x") + pattern: "keycard/card_insert/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 0 + endImgIndex: 16 + duration: 1000 + loops: 1 } PropertyChanges { target: message - visible: root.sharedKeycardModule.keycardData !== "" - text: qsTr("Check the card, it might be wrongly inserted") - font.pixelSize: Constants.keycard.general.fontSize3 + text: root.sharedKeycardModule.keycardData & Constants.predefinedKeycardData.wronglyInsertedCard? + qsTr("Check the card, it might be wrongly inserted") : + "" + font.pixelSize: Constants.keycard.general.fontSize2 color: Theme.palette.baseColor1 } }, + State { + name: Constants.keycardSharedState.keycardInserted + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardInserted + PropertyChanges { + target: title + text: qsTr("Keycard inserted...") + font.weight: Font.Bold + font.pixelSize: Constants.keycard.general.fontSize1 + color: Theme.palette.directColor1 + } + PropertyChanges { + target: image + pattern: "keycard/card_inserted/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 0 + endImgIndex: 29 + duration: 1000 + loops: 1 + } + PropertyChanges { + target: message + text: "" + } + }, State { name: Constants.keycardSharedState.readingKeycard when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.readingKeycard @@ -121,12 +311,17 @@ Item { } PropertyChanges { target: image - source: Style.png("keycard/popup_card_yellow@2x") + pattern: "keycard/warning/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 0 + endImgIndex: 55 + duration: 3000 + loops: 1 } PropertyChanges { target: message text: "" - visible: false } }, State { @@ -135,20 +330,74 @@ Item { PropertyChanges { target: title text: qsTr("This is not a Keycard") - font.pixelSize: Constants.keycard.general.fontSize2 + font.pixelSize: Constants.keycard.general.fontSize1 font.weight: Font.Bold color: Theme.palette.dangerColor1 } PropertyChanges { target: image - source: Style.png("keycard/popup_card_red_wrong@2x") + pattern: "keycard/strong_error/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 18 + endImgIndex: 29 + duration: 1300 + loops: -1 } PropertyChanges { target: message text: qsTr("The card inserted is not a recognised Keycard,\nplease remove and try and again") - font.pixelSize: Constants.keycard.general.fontSize3 + font.pixelSize: Constants.keycard.general.fontSize2 color: Theme.palette.dangerColor1 - visible: true + } + }, + State { + name: Constants.keycardSharedState.maxPinRetriesReached + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.maxPinRetriesReached + PropertyChanges { + target: title + text: qsTr("Keycard locked") + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.dangerColor1 + } + PropertyChanges { + target: image + pattern: "keycard/strong_error/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 18 + endImgIndex: 29 + duration: 1300 + loops: -1 + } + PropertyChanges { + target: message + text: qsTr("Pin entered incorrectly too many times") + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.dangerColor1 + } + }, + State { + name: Constants.keycardSharedState.keycardEmptyMetadata + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmptyMetadata + PropertyChanges { + target: title + text: qsTr("This Keycard has empty metadata") + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + } + PropertyChanges { + target: image + source: Style.png("keycard/card-inserted") + pattern: "" + } + PropertyChanges { + target: message + text: qsTr("This Keycard already stores keys\nbut doesn't store any metadata") + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.directColor1 } }, State { @@ -157,20 +406,45 @@ Item { PropertyChanges { target: title text: qsTr("Keycard is empty") - font.pixelSize: Constants.keycard.general.fontSize2 + font.pixelSize: Constants.keycard.general.fontSize1 font.weight: Font.Bold color: Theme.palette.directColor1 } PropertyChanges { target: image - source: Style.png("keycard/popup_card_dark@2x") + source: Style.png("keycard/card-empty") + pattern: "" } PropertyChanges { target: message text: qsTr("There is no key pair on this Keycard") - font.pixelSize: Constants.keycard.general.fontSize3 + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.directColor1 + } + }, + State { + name: Constants.keycardSharedState.keycardNotEmpty + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardNotEmpty + PropertyChanges { + target: title + text: qsTr("This Keycard already stores keys") + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + } + PropertyChanges { + target: image + source: Style.png("keycard/card-inserted") + pattern: "" + } + PropertyChanges { + target: message + text: root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard? + qsTr("To migrate %1 on to this Keycard, you\nwill need to perform a factory reset first") + .arg(root.sharedKeycardModule.selectedKeyPairItem.name) : + "" + font.pixelSize: Constants.keycard.general.fontSize2 color: Theme.palette.directColor1 - visible: true } }, State { @@ -185,12 +459,17 @@ Item { } PropertyChanges { target: image - source: Style.png("keycard/popup_card_green@2x") + pattern: "keycard/success/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 0 + endImgIndex: 29 + duration: 1300 + loops: 1 } PropertyChanges { target: message text: "" - visible: false } }, State { @@ -198,21 +477,127 @@ Item { when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.factoryResetSuccess PropertyChanges { target: title - text: qsTr("Keycard successfully factory reset") + text: root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard? + qsTr("Your Keycard has been reset") : + qsTr("Keycard successfully factory reset") font.pixelSize: Constants.keycard.general.fontSize1 font.weight: Font.Bold color: Theme.palette.directColor1 } PropertyChanges { target: image - source: Style.png("keycard/popup_card_green_checked@2x") + pattern: "keycard/strong_success/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 0 + endImgIndex: 20 + duration: 1000 + loops: 1 } PropertyChanges { target: message - text: qsTr("You can now use this Keycard as if it\nwas a brand new empty Keycard") - font.pixelSize: Constants.keycard.general.fontSize3 + text: root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard? + qsTr("You can now create a new key pair on this Keycard") : + qsTr("You can now use this Keycard as if it\nwas a brand new empty Keycard") + font.pixelSize: Constants.keycard.general.fontSize2 color: Theme.palette.directColor1 - visible: true + } + }, + State { + name: Constants.keycardSharedState.migratingKeyPair + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.migratingKeyPair + PropertyChanges { + target: title + text: qsTr("Migrating key pair to Keycard") + font.pixelSize: Constants.keycard.general.fontSize2 + font.weight: Font.Bold + color: Theme.palette.baseColor1 + } + PropertyChanges { + target: image + pattern: "keycard/warning/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 0 + endImgIndex: 55 + duration: 3000 + loops: -1 + } + PropertyChanges { + target: message + text: "" + } + }, + State { + name: Constants.keycardSharedState.keyPairMigrateSuccess + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateSuccess + PropertyChanges { + target: title + text: qsTr("Key pair successfully migrated") + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + } + PropertyChanges { + target: image + pattern: "keycard/strong_success/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 0 + endImgIndex: 20 + duration: 1000 + loops: 1 + } + PropertyChanges { + target: message + text: qsTr("To complete migration close Status and log in with your new Keycard") + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.directColor1 + } + }, + State { + name: Constants.keycardSharedState.keyPairMigrateFailure + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keyPairMigrateFailure + PropertyChanges { + target: title + text: qsTr("Key pair failed to migrated") + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + } + PropertyChanges { + target: image + pattern: "keycard/strong_error/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 18 + endImgIndex: 29 + duration: 1300 + loops: 1 + } + PropertyChanges { + target: message + text: "" + } + }, + State { + name: Constants.keycardSharedState.keycardMetadataDisplay + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardMetadataDisplay + PropertyChanges { + target: title + text: qsTr("Accounts on this Keycard") + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + } + PropertyChanges { + target: image + source: Style.png("keycard/card-inserted") + pattern: "" + } + PropertyChanges { + target: message + text: "" } } ] diff --git a/ui/imports/shared/popups/keycard/states/KeycardPin.qml b/ui/imports/shared/popups/keycard/states/KeycardPin.qml new file mode 100644 index 0000000000..a1ad7eebec --- /dev/null +++ b/ui/imports/shared/popups/keycard/states/KeycardPin.qml @@ -0,0 +1,292 @@ +import QtQuick 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls 2.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 + +import utils 1.0 + +import "../helpers" + +Item { + id: root + + property var sharedKeycardModule + + property int remainingAttempts: parseInt(root.sharedKeycardModule.keycardData, 10) + + onRemainingAttemptsChanged: { + if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin) { + pinInputField.statesInitialization() + pinInputField.forceFocus() + } + } + + onStateChanged: { + if(state === Constants.keycardSharedState.pinSet || + state === Constants.keycardSharedState.pinVerified) { + pinInputField.setPin("123456") // we are free to set fake pin in this case + } else { + pinInputField.statesInitialization() + pinInputField.forceFocus() + } + } + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Style.current.xlPadding + anchors.bottomMargin: Style.current.halfPadding + anchors.leftMargin: Style.current.xlPadding + anchors.rightMargin: Style.current.xlPadding + spacing: Style.current.padding + clip: true + + KeycardImage { + id: image + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: Constants.keycard.shared.imageHeight + Layout.preferredWidth: Constants.keycard.shared.imageWidth + } + + StatusBaseText { + id: title + Layout.alignment: Qt.AlignCenter + font.weight: Font.Bold + } + + StatusPinInput { + id: pinInputField + Layout.alignment: Qt.AlignHCenter + validator: StatusIntValidator{bottom: 0; top: 999999;} + pinLen: Constants.keycard.general.keycardPinLength + enabled: root.sharedKeycardModule.currentState.stateType !== Constants.keycardSharedState.pinSet && + root.sharedKeycardModule.currentState.stateType !== Constants.keycardSharedState.pinVerified + + onPinInputChanged: { + if (root.state !== Constants.keycardSharedState.wrongPin) { + image.source = Style.png("keycard/enter-pin-%1".arg(pinInput.length)) + } + if(pinInput.length == 0) { + return + } + if(root.state === Constants.keycardSharedState.createPin || + root.state === Constants.keycardSharedState.enterPin || + root.state === Constants.keycardSharedState.wrongPin) { + root.sharedKeycardModule.setPin(pinInput) + root.sharedKeycardModule.currentState.doTertiaryAction() + } + else if(root.state === Constants.keycardSharedState.repeatPin) { + let pinsMatch = root.sharedKeycardModule.checkRepeatedKeycardPinWhileTyping(pinInput) + if (pinsMatch) { + info.text = qsTr("It is very important that you do not loose this PIN") + root.sharedKeycardModule.currentState.doTertiaryAction() + } else { + info.text = qsTr("PINs don't match") + image.source = Style.png("keycard/plain-error") + } + } + } + } + + StatusBaseText { + id: info + Layout.alignment: Qt.AlignCenter + wrapMode: Text.WordWrap + visible: text !== "" + } + + StatusBaseText { + id: message + Layout.alignment: Qt.AlignCenter + wrapMode: Text.WordWrap + visible: text !== "" + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + + Loader { + Layout.preferredWidth: parent.width + active: root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.setupNewKeycard && + (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.createPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.repeatPin || + root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinSet) + + sourceComponent: KeyPairItem { + keyPairPubKey: root.sharedKeycardModule.selectedKeyPairItem.pubKey + keyPairName: root.sharedKeycardModule.selectedKeyPairItem.name + keyPairIcon: root.sharedKeycardModule.selectedKeyPairItem.icon + keyPairImage: root.sharedKeycardModule.selectedKeyPairItem.image + keyPairDerivedFrom: root.sharedKeycardModule.selectedKeyPairItem.derivedFrom + keyPairAccounts: root.sharedKeycardModule.selectedKeyPairItem.accounts + } + } + } + + states: [ + State { + name: Constants.keycardSharedState.enterPin + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.enterPin + PropertyChanges { + target: image + source: Style.png("keycard/card-empty") + pattern: "" + } + PropertyChanges { + target: title + text: qsTr("Enter this Keycard’s PIN") + font.pixelSize: Constants.keycard.general.fontSize1 + color: Theme.palette.directColor1 + } + PropertyChanges { + target: info + text: "" + } + PropertyChanges { + target: message + text: "" + } + }, + State { + name: Constants.keycardSharedState.wrongPin + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongPin + PropertyChanges { + target: image + source: Style.png("keycard/plain-error") + pattern: "" + } + PropertyChanges { + target: title + text: qsTr("Enter Keycard PIN") + font.pixelSize: Constants.keycard.general.fontSize1 + color: Theme.palette.directColor1 + } + PropertyChanges { + target: info + text: qsTr("PIN incorrect") + color: Theme.palette.dangerColor1 + font.pixelSize: Constants.keycard.general.fontSize3 + } + PropertyChanges { + target: message + text: qsTr("%n attempt(s) remaining", "", root.remainingAttempts) + color: root.remainingAttempts === 1? + Theme.palette.dangerColor1 : + Theme.palette.baseColor1 + font.pixelSize: Constants.keycard.general.fontSize3 + } + }, + State { + name: Constants.keycardSharedState.createPin + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.createPin + PropertyChanges { + target: image + source: Style.png("keycard/enter-pin-0") + pattern: "" + } + PropertyChanges { + target: title + text: qsTr("Choose a Keycard PIN") + font.pixelSize: Constants.keycard.general.fontSize1 + color: Theme.palette.directColor1 + } + PropertyChanges { + target: info + text: qsTr("It is very important that you do not loose this PIN") + color: Theme.palette.dangerColor1 + font.pixelSize: Constants.keycard.general.fontSize3 + } + PropertyChanges { + target: message + text: "" + } + }, + State { + name: Constants.keycardSharedState.repeatPin + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.repeatPin + PropertyChanges { + target: image + source: Style.png("keycard/enter-pin-0") + pattern: "" + } + PropertyChanges { + target: title + text: qsTr("Repeat Keycard PIN") + font.pixelSize: Constants.keycard.general.fontSize1 + color: Theme.palette.directColor1 + } + PropertyChanges { + target: info + text: qsTr("It is very important that you do not loose this PIN") + color: Theme.palette.dangerColor1 + font.pixelSize: Constants.keycard.general.fontSize3 + } + PropertyChanges { + target: message + text: "" + } + }, + State { + name: Constants.keycardSharedState.pinSet + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinSet + PropertyChanges { + target: image + pattern: "keycard/strong_success/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 0 + endImgIndex: 20 + duration: 1300 + loops: 1 + } + PropertyChanges { + target: title + text: qsTr("Keycard PIN set") + font.pixelSize: Constants.keycard.general.fontSize1 + color: Theme.palette.directColor1 + } + PropertyChanges { + target: info + text: "" + } + PropertyChanges { + target: message + text: "" + } + }, + State { + name: Constants.keycardSharedState.pinVerified + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.pinVerified + PropertyChanges { + target: image + pattern: "keycard/strong_success/img-%1" + source: "" + startImgIndexForTheFirstLoop: 0 + startImgIndexForOtherLoops: 0 + endImgIndex: 20 + duration: 1300 + loops: 1 + } + PropertyChanges { + target: title + text: qsTr("Keycard PIN verified!") + font.pixelSize: Constants.keycard.general.fontSize1 + color: Theme.palette.directColor1 + } + PropertyChanges { + target: info + text: "" + } + PropertyChanges { + target: message + text: "" + } + } + ] +} diff --git a/ui/imports/shared/popups/keycard/states/SeedPhrase.qml b/ui/imports/shared/popups/keycard/states/SeedPhrase.qml new file mode 100644 index 0000000000..a1281ad53f --- /dev/null +++ b/ui/imports/shared/popups/keycard/states/SeedPhrase.qml @@ -0,0 +1,125 @@ +import QtQuick 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls 2.14 +import QtGraphicalEffects 1.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 + +import utils 1.0 + +Item { + id: root + + property var sharedKeycardModule + property bool hideSeed: true + + signal seedPhraseRevealed() + + QtObject { + id: d + + readonly property var seedPhrase: root.sharedKeycardModule.getMnemonic().split(" ") + } + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Style.current.xlPadding + anchors.bottomMargin: Style.current.halfPadding + anchors.leftMargin: Style.current.xlPadding + anchors.rightMargin: Style.current.xlPadding + spacing: Style.current.padding + clip: true + + StatusBaseText { + id: title + Layout.preferredHeight: Constants.keycard.general.titleHeight + Layout.alignment: Qt.AlignCenter + wrapMode: Text.WordWrap + } + + StatusBaseText { + id: message + Layout.preferredHeight: Constants.keycard.general.messageHeight + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + Item { + Layout.preferredWidth: parent.width + Layout.fillHeight: true + + StatusGridView { + id: grid + anchors.fill: parent + visible: !root.hideSeed + cellWidth: parent.width * 0.5 + cellHeight: 48 + interactive: false + model: 12 + readonly property var wordIndex: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"] + readonly property int spacing: 4 + delegate: StatusSeedPhraseInput { + width: (grid.cellWidth - grid.spacing) + height: (grid.cellHeight - grid.spacing) + textEdit.input.edit.enabled: false + text: { + const idx = parseInt(leftComponentText) - 1; + if (!d.seedPhrase || idx < 0 || idx > d.seedPhrase.length - 1) + return ""; + return d.seedPhrase[idx]; + } + leftComponentText: grid.wordIndex[index] + } + } + + GaussianBlur { + id: blur + anchors.fill: grid + visible: root.hideSeed + source: grid + radius: 16 + samples: 16 + transparentBorder: true + } + + StatusButton { + anchors.centerIn: parent + visible: root.hideSeed + type: StatusBaseButton.Type.Primary + icon.name: "view" + text: qsTr("Reveal seed phrase") + onClicked: { + root.hideSeed = false; + root.seedPhraseRevealed() + } + } + } + } + + states: [ + State { + name: Constants.keycardSharedState.seedPhraseDisplay + when: root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.seedPhraseDisplay + PropertyChanges { + target: title + text: qsTr("Write down your seed phrase") + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + } + PropertyChanges { + target: message + text: qsTr("The next screen contains your seed phrase.
Anyone who sees it can use it to access to your funds.") + font.pixelSize: Constants.keycard.general.fontSize2 + wrapMode: Text.WordWrap + textFormat: Text.RichText + color: Theme.palette.dangerColor1 + } + } + ] +} diff --git a/ui/imports/shared/popups/keycard/states/SelectKeyPair.qml b/ui/imports/shared/popups/keycard/states/SelectKeyPair.qml new file mode 100644 index 0000000000..66036577fe --- /dev/null +++ b/ui/imports/shared/popups/keycard/states/SelectKeyPair.qml @@ -0,0 +1,109 @@ +import QtQuick 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls 2.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 + +import utils 1.0 +import shared.status 1.0 + +import "../helpers" + +Item { + id: root + + property var sharedKeycardModule + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Style.current.xlPadding + anchors.bottomMargin: Style.current.halfPadding + anchors.leftMargin: Style.current.xlPadding + anchors.rightMargin: Style.current.xlPadding + spacing: Style.current.padding + clip: true + + ButtonGroup { + id: keyPairsButtonGroup + } + + StatusBaseText { + id: title + Layout.alignment: Qt.AlignHCenter + text: qsTr("Select a key pair") + font.pixelSize: Constants.keycard.general.fontSize1 + font.weight: Font.Bold + color: Theme.palette.directColor1 + wrapMode: Text.WordWrap + } + + StatusBaseText { + id: subTitle + Layout.alignment: Qt.AlignHCenter + text: qsTr("Select which key pair you’d like to move to this Keycard") + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.current.halfPadding + Layout.fillHeight: root.sharedKeycardModule.keyPairModel.count === 0 + } + + StatusBaseText { + visible: !root.sharedKeycardModule.isProfileKeyPairMigrated() + Layout.preferredWidth: parent.width - 2 * Style.current.padding + Layout.leftMargin: Style.current.padding + Layout.alignment: Qt.AlignLeft + text: qsTr("Profile key pair") + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + } + + KeyPairList { + visible: !root.sharedKeycardModule.isProfileKeyPairMigrated() + Layout.fillWidth: true + Layout.preferredHeight: 100 + Layout.fillHeight: visible && root.sharedKeycardModule.keyPairModel.count === 1 + Layout.alignment: Qt.AlignLeft + Layout.preferredWidth: parent.width + + sharedKeycardModule: root.sharedKeycardModule + filterProfilePair: true + keyPairModel: root.sharedKeycardModule.keyPairModel + buttonGroup: keyPairsButtonGroup + } + + StatusBaseText { + visible: root.sharedKeycardModule.isProfileKeyPairMigrated() && root.sharedKeycardModule.keyPairModel.count > 0 || + !root.sharedKeycardModule.isProfileKeyPairMigrated() && root.sharedKeycardModule.keyPairModel.count > 1 + Layout.preferredWidth: parent.width - 2 * Style.current.padding + Layout.leftMargin: Style.current.padding + Layout.alignment: Qt.AlignLeft + text: qsTr("Other key pairs") + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.baseColor1 + wrapMode: Text.WordWrap + } + + KeyPairList { + visible: root.sharedKeycardModule.isProfileKeyPairMigrated() && root.sharedKeycardModule.keyPairModel.count > 0 || + !root.sharedKeycardModule.isProfileKeyPairMigrated() && root.sharedKeycardModule.keyPairModel.count > 1 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignLeft + Layout.preferredWidth: parent.width + + sharedKeycardModule: root.sharedKeycardModule + keyPairModel: root.sharedKeycardModule.keyPairModel + buttonGroup: keyPairsButtonGroup + } + } +} diff --git a/ui/imports/shared/stores/BIP39_en.qml b/ui/imports/shared/stores/BIP39_en.qml index 47754a37c6..fa6941cc73 100644 --- a/ui/imports/shared/stores/BIP39_en.qml +++ b/ui/imports/shared/stores/BIP39_en.qml @@ -1,15 +1,19 @@ import QtQuick 2.13 ListModel { + id: root + + property var words: [] + Component.onCompleted: { var xhr = new XMLHttpRequest(); xhr.open("GET", "english.txt"); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { - var words = xhr.responseText.split('\n'); - for (var i = 0; i < words.length; i++) { - if (words[i] !== "") { - insert(count, {"seedWord": words[i]}); + root.words = xhr.responseText.split('\n'); + for (var i = 0; i < root.words.length; i++) { + if (root.words[i] !== "") { + insert(count, {"seedWord": root.words[i]}); } } } diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 56e886123f..676ebb4f25 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -67,9 +67,16 @@ QtObject { readonly property string loginKeycardEmpty: "LoginKeycardEmpty" } + readonly property QtObject predefinedKeycardData: QtObject { + readonly property int wronglyInsertedCard: 1 + readonly property int hideKeyPair: 2 + readonly property int wrongSeedPhrase: 4 + } + readonly property QtObject keycardSharedFlow: QtObject { readonly property string general: "General" readonly property string factoryReset: "FactoryReset" + readonly property string setupNewKeycard: "SetupNewKeycard" } readonly property QtObject keycardSharedState: QtObject { @@ -77,12 +84,31 @@ QtObject { readonly property string pluginReader: "PluginReader" readonly property string readingKeycard: "ReadingKeycard" readonly property string insertKeycard: "InsertKeycard" + readonly property string keycardInserted: "KeycardInserted" + readonly property string createPin: "CreatePin" + readonly property string repeatPin: "RepeatPin" + readonly property string pinSet: "PinSet" + readonly property string pinVerified: "PinVerified" readonly property string enterPin: "EnterPin" + readonly property string wrongPin: "WrongPin" + readonly property string maxPinRetriesReached: "MaxPinRetriesReached" readonly property string factoryResetConfirmation: "FactoryResetConfirmation" + readonly property string factoryResetConfirmationDisplayMetadata: "FactoryResetConfirmationDisplayMetadata" readonly property string factoryResetSuccess: "FactoryResetSuccess" + readonly property string keycardEmptyMetadata: "KeycardEmptyMetadata" + readonly property string keycardMetadataDisplay: "KeycardMetadataDisplay" readonly property string keycardEmpty: "KeycardEmpty" + readonly property string keycardNotEmpty: "KeycardNotEmpty" readonly property string notKeycard: "NotKeycard" readonly property string recognizedKeycard: "RecognizedKeycard" + readonly property string selectExistingKeyPair: "SelectExistingKeyPair" + readonly property string enterSeedPhrase: "EnterSeedPhrase" + readonly property string wrongSeedPhrase: "WrongSeedPhrase" + readonly property string seedPhraseDisplay: "SeedPhraseDisplay" + readonly property string seedPhraseEnterWords: "SeedPhraseEnterWords" + readonly property string keyPairMigrateSuccess: "KeyPairMigrateSuccess" + readonly property string keyPairMigrateFailure: "KeyPairMigrateFailure" + readonly property string migratingKeyPair: "MigratingKeyPair" } readonly property QtObject keychain: QtObject { @@ -339,6 +365,17 @@ QtObject { readonly property int pukCellHeight: 60 readonly property int sharedFlowImageWidth: 240 readonly property int sharedFlowImageHeight: 240 + readonly property int popupWidth: 640 + readonly property int popupHeight: 640 + readonly property int popupBiggerHeight: 766 + readonly property int titleHeight: 44 + readonly property int messageHeight: 48 + } + + readonly property QtObject keyPairType: QtObject { + readonly property int profile: 0 + readonly property int seedImport: 1 + readonly property int privateKeyImport: 2 } readonly property QtObject shared: QtObject {