diff --git a/src/app/modules/shared_modules/keypair_import/controller.nim b/src/app/modules/shared_modules/keypair_import/controller.nim index eb35a30730..52c44aa6ce 100644 --- a/src/app/modules/shared_modules/keypair_import/controller.nim +++ b/src/app/modules/shared_modules/keypair_import/controller.nim @@ -5,6 +5,7 @@ import app/core/eventemitter import app_service/service/accounts/service as accounts_service import app_service/service/wallet_account/service as wallet_account_service +import app/modules/shared_models/[keypair_item] import app/modules/shared_modules/keycard_popup/io_interface as keycard_shared_module logScope: @@ -93,4 +94,10 @@ proc makePrivateKeyKeypairFullyOperable*(self: Controller, keyUid, privateKey, p return self.walletAccountService.makePrivateKeyKeypairFullyOperable(keyUid, privateKey, password, doPasswordHashing) proc makeSeedPhraseKeypairFullyOperable*(self: Controller, keyUid, mnemonic, password: string, doPasswordHashing: bool): string = - return self.walletAccountService.makeSeedPhraseKeypairFullyOperable(keyUid, mnemonic, password, doPasswordHashing) \ No newline at end of file + return self.walletAccountService.makeSeedPhraseKeypairFullyOperable(keyUid, mnemonic, password, doPasswordHashing) + +proc getKeypairs*(self: Controller): seq[wallet_account_service.KeypairDto] = + return self.walletAccountService.getKeypairs() + +proc getSelectedKeypair*(self: Controller): KeyPairItem = + return self.delegate.getSelectedKeypair() \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/internal/scan_qr_state.nim b/src/app/modules/shared_modules/keypair_import/internal/scan_qr_state.nim new file mode 100644 index 0000000000..c854cf27b4 --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/internal/scan_qr_state.nim @@ -0,0 +1,12 @@ +type + ScanQrState* = ref object of State + +proc newScanQrState*(backState: State): ScanQrState = + result = ScanQrState() + result.setup(StateType.ScanQr, backState) + +proc delete*(self: ScanQrState) = + self.State.delete + +method getNextPrimaryState*(self: ScanQrState, controller: Controller): State = + discard \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/internal/select_import_method_state.nim b/src/app/modules/shared_modules/keypair_import/internal/select_import_method_state.nim new file mode 100644 index 0000000000..f173ffdb45 --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/internal/select_import_method_state.nim @@ -0,0 +1,20 @@ +type + SelectImportMethodState* = ref object of State + +proc newSelectImportMethodState*(backState: State): SelectImportMethodState = + result = SelectImportMethodState() + result.setup(StateType.SelectImportMethod, backState) + +proc delete*(self: SelectImportMethodState) = + self.State.delete + +method getNextPrimaryState*(self: SelectImportMethodState, controller: Controller): State = + return createState(StateType.ScanQr, self) + +method getNextSecondaryState*(self: SelectImportMethodState, controller: Controller): State = + let kp = controller.getSelectedKeypair() + if kp.getPairType() == KeyPairType.SeedImport.int: + return createState(StateType.ImportSeedPhrase, self) + if kp.getPairType() == KeyPairType.PrivateKeyImport.int: + return createState(StateType.ImportPrivateKey, self) + error "ki_unsupported keypair type" \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/internal/select_keypair_state.nim b/src/app/modules/shared_modules/keypair_import/internal/select_keypair_state.nim new file mode 100644 index 0000000000..22fd371b85 --- /dev/null +++ b/src/app/modules/shared_modules/keypair_import/internal/select_keypair_state.nim @@ -0,0 +1,12 @@ +type + SelectKeypairState* = ref object of State + +proc newSelectKeypairState*(backState: State): SelectKeypairState = + result = SelectKeypairState() + result.setup(StateType.SelectKeypair, backState) + +proc delete*(self: SelectKeypairState) = + self.State.delete + +method getNextPrimaryState*(self: SelectKeypairState, controller: Controller): State = + return createState(StateType.SelectImportMethod, self) \ No newline at end of file diff --git a/src/app/modules/shared_modules/keypair_import/internal/state.nim b/src/app/modules/shared_modules/keypair_import/internal/state.nim index fc4199d1d7..720d956f08 100644 --- a/src/app/modules/shared_modules/keypair_import/internal/state.nim +++ b/src/app/modules/shared_modules/keypair_import/internal/state.nim @@ -2,7 +2,9 @@ import ../controller type StateType* {.pure.} = enum NoState = "NoState" - Main = "Main" + SelectKeypair = "SelectKeypair" + SelectImportMethod = "SelectImportMethod" + ScanQr = "ScanQr" ImportSeedPhrase = "ImportSeedPhrase" ImportPrivateKey = "ImportPrivateKey" diff --git a/src/app/modules/shared_modules/keypair_import/internal/state_factory.nim b/src/app/modules/shared_modules/keypair_import/internal/state_factory.nim index 9ebf4e8e84..27e1721f69 100644 --- a/src/app/modules/shared_modules/keypair_import/internal/state_factory.nim +++ b/src/app/modules/shared_modules/keypair_import/internal/state_factory.nim @@ -1,5 +1,6 @@ import chronicles import ../controller +import app/modules/shared_models/[keypair_item] import state @@ -9,10 +10,19 @@ logScope: # Forward declaration proc createState*(stateToBeCreated: StateType, backState: State): State +include select_keypair_state +include select_import_method_state +include scan_qr_state include import_private_key_state include import_seed_phrase_state proc createState*(stateToBeCreated: StateType, backState: State): State = + if stateToBeCreated == StateType.SelectKeypair: + return newSelectKeypairState(backState) + if stateToBeCreated == StateType.SelectImportMethod: + return newSelectImportMethodState(backState) + if stateToBeCreated == StateType.ScanQr: + return newScanQrState(backState) if stateToBeCreated == StateType.ImportPrivateKey: return newImportPrivateKeyState(backState) if stateToBeCreated == StateType.ImportSeedPhrase: diff --git a/src/app/modules/shared_modules/keypair_import/io_interface.nim b/src/app/modules/shared_modules/keypair_import/io_interface.nim index 6a4c2faeba..ff7e69a656 100644 --- a/src/app/modules/shared_modules/keypair_import/io_interface.nim +++ b/src/app/modules/shared_modules/keypair_import/io_interface.nim @@ -1,10 +1,13 @@ import Tables, NimQml +import app/modules/shared_models/[keypair_item] import app_service/service/wallet_account/dto/derived_address_dto type ImportOption* {.pure.}= enum - SeedPhrase = 1, - PrivateKey = 2 + SelectKeypair = 1 + SeedPhrase + PrivateKey + QrCode type AccessInterface* {.pure inheritable.} = ref object of RootObj @@ -27,6 +30,9 @@ method onBackActionClicked*(self: AccessInterface) {.base.} = method onPrimaryActionClicked*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") +method onSecondaryActionClicked*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + method onCancelActionClicked*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") @@ -45,6 +51,12 @@ method validSeedPhrase*(self: AccessInterface, seedPhrase: string): bool {.base. method onAddressDetailsFetched*(self: AccessInterface, derivedAddresses: seq[DerivedAddressDto], error: string) {.base.} = raise newException(ValueError, "No implementation available") +method getSelectedKeypair*(self: AccessInterface): KeyPairItem {.base.} = + raise newException(ValueError, "No implementation available") + +method setSelectedKeyPairByKeyUid*(self: AccessInterface, keyUid: string) {.base.} = + raise newException(ValueError, "No implementation available") + type DelegateInterface* = concept c c.onKeypairImportModuleLoaded() diff --git a/src/app/modules/shared_modules/keypair_import/module.nim b/src/app/modules/shared_modules/keypair_import/module.nim index 338f5b8a67..f2d0bc70e0 100644 --- a/src/app/modules/shared_modules/keypair_import/module.nim +++ b/src/app/modules/shared_modules/keypair_import/module.nim @@ -7,7 +7,7 @@ import internal/[state, state_factory] import app/global/global_singleton import app/core/eventemitter import app/modules/shared/keypairs -import app/modules/shared_models/[derived_address_model] +import app/modules/shared_models/[keypair_model, derived_address_model] import app_service/service/accounts/service as accounts_service import app_service/service/wallet_account/service as wallet_account_service @@ -48,21 +48,27 @@ method getModuleAsVariant*[T](self: Module[T]): QVariant = method load*[T](self: Module[T], keyUid: string, importOption: ImportOption) = self.controller.init() - let keypair = self.controller.getKeypairByKeyUid(keyUid) - if keypair.isNil: - error "trying to import an unknown keypair" - self.closeKeypairImportPopup() - return - let keypairItem = buildKeypairItem(keypair, areTestNetworksEnabled = false) # testnetworks are irrelevant in this context - if keypairItem.isNil: - error "cannot generate keypair item for provided keypair" - self.closeKeypairImportPopup() - return - self.view.setSelectedKeypair(keypairItem) - if importOption == ImportOption.PrivateKey: - self.view.setCurrentState(newImportPrivateKeyState(nil)) - elif importOption == ImportOption.SeedPhrase: - self.view.setCurrentState(newImportSeedPhraseState(nil)) + if importOption == ImportOption.SelectKeypair: + let items = keypairs.buildKeyPairsList(self.controller.getKeypairs(), excludeAlreadyMigratedPairs = true, + excludePrivateKeyKeypairs = false) + self.view.createKeypairModel(items) + self.view.setCurrentState(newSelectKeypairState(nil)) + else: + let keypair = self.controller.getKeypairByKeyUid(keyUid) + if keypair.isNil: + error "ki_trying to import an unknown keypair" + self.closeKeypairImportPopup() + return + let keypairItem = buildKeypairItem(keypair, areTestNetworksEnabled = false) # testnetworks are irrelevant in this context + if keypairItem.isNil: + error "ki_cannot generate keypair item for provided keypair" + self.closeKeypairImportPopup() + return + self.view.setSelectedKeypairItem(keypairItem) + if importOption == ImportOption.PrivateKey: + self.view.setCurrentState(newImportPrivateKeyState(nil)) + elif importOption == ImportOption.SeedPhrase: + self.view.setCurrentState(newImportSeedPhraseState(nil)) self.delegate.onKeypairImportModuleLoaded() method onBackActionClicked*[T](self: Module[T]) = @@ -99,16 +105,40 @@ method onPrimaryActionClicked*[T](self: Module[T]) = self.view.setCurrentState(nextState) debug "ki_primary_action - set state", setCurrState=nextState.stateType() +method onSecondaryActionClicked*[T](self: Module[T]) = + let currStateObj = self.view.currentStateObj() + if currStateObj.isNil: + error "ki_cannot resolve current state" + return + debug "ki_secondary_action", currState=currStateObj.stateType() + currStateObj.executePreSecondaryStateCommand(self.controller) + let nextState = currStateObj.getNextSecondaryState(self.controller) + if nextState.isNil: + return + self.view.setCurrentState(nextState) + debug "ki_secondary_action - set state", setCurrState=nextState.stateType() + proc authenticateLoggedInUser[T](self: Module[T]) = self.controller.authenticateLoggedInUser() +method getSelectedKeypair*[T](self: Module[T]): KeyPairItem = + return self.view.getSelectedKeypair() + +method setSelectedKeyPairByKeyUid*[T](self: Module[T], keyUid: string) = + let item = self.view.keypairModel().findItemByKeyUid(keyUid) + if item.isNil: + error "ki_cannot generate keypair item for provided keypair" + self.closeKeypairImportPopup() + return + self.view.setSelectedKeypairItem(item) + method changePrivateKey*[T](self: Module[T], privateKey: string) = self.view.setPrivateKeyAccAddress(newDerivedAddressItem()) if privateKey.len == 0: return let genAccDto = self.controller.createAccountFromPrivateKey(privateKey) if genAccDto.address.len == 0: - error "unable to resolve an address from the provided private key" + error "ki_unable to resolve an address from the provided private key" return let kp = self.view.getSelectedKeypair() if kp.isNil: @@ -116,7 +146,7 @@ method changePrivateKey*[T](self: Module[T], privateKey: string) = return if kp.getKeyUid() != genAccDto.keyUid: self.view.setEnteredPrivateKeyMatchTheKeypair(false) - error "entered private key doesn't refer to a keyapir being imported" + error "ki_entered private key doesn't refer to a keyapir being imported" return self.view.setEnteredPrivateKeyMatchTheKeypair(true) self.view.setPrivateKeyAccAddress(newDerivedAddressItem(order = 0, address = genAccDto.address, publicKey = genAccDto.publicKey)) @@ -127,7 +157,7 @@ method changeSeedPhrase*[T](self: Module[T], seedPhrase: string) = return let genAccDto = self.controller.createAccountFromSeedPhrase(seedPhrase) if seedPhrase.len > 0 and genAccDto.address.len == 0: - error "unable to create an account from the provided seed phrase" + error "ki_unable to create an account from the provided seed phrase" return method validSeedPhrase*[T](self: Module[T], seedPhrase: string): bool = diff --git a/src/app/modules/shared_modules/keypair_import/view.nim b/src/app/modules/shared_modules/keypair_import/view.nim index 127335f91b..65f6c39ab9 100644 --- a/src/app/modules/shared_modules/keypair_import/view.nim +++ b/src/app/modules/shared_modules/keypair_import/view.nim @@ -1,7 +1,7 @@ import NimQml import io_interface import internal/[state, state_wrapper] -import app/modules/shared_models/[keypair_item, derived_address_model] +import app/modules/shared_models/[keypair_model, derived_address_model] QtObject: type @@ -9,6 +9,8 @@ QtObject: delegate: io_interface.AccessInterface currentState: StateWrapper currentStateVariant: QVariant + keypairModel: KeyPairModel + keypairModelVariant: QVariant selectedKeypair: KeyPairItem selectedKeypairVariant: QVariant privateKeyAccAddress: DerivedAddressItem @@ -20,6 +22,10 @@ QtObject: self.currentState.delete self.selectedKeypair.delete self.selectedKeypairVariant.delete + if not self.keypairModel.isNil: + self.keypairModel.delete + if not self.keypairModelVariant.isNil: + self.keypairModelVariant.delete self.QObject.delete proc newView*(delegate: io_interface.AccessInterface): View = @@ -37,6 +43,7 @@ QtObject: signalConnect(result.currentState, "backActionClicked()", result, "onBackActionClicked()", 2) signalConnect(result.currentState, "cancelActionClicked()", result, "onCancelActionClicked()", 2) signalConnect(result.currentState, "primaryActionClicked()", result, "onPrimaryActionClicked()", 2) + signalConnect(result.currentState, "secondaryActionClicked()", result, "onSecondaryActionClicked()", 2) proc currentStateObj*(self: View): State = return self.currentState.getStateObj() @@ -57,6 +64,9 @@ QtObject: proc onPrimaryActionClicked*(self: View) {.slot.} = self.delegate.onPrimaryActionClicked() + proc onSecondaryActionClicked*(self: View) {.slot.} = + self.delegate.onSecondaryActionClicked() + proc getSelectedKeypair*(self: View): KeyPairItem = return self.selectedKeypair proc getSelectedKeypairAsVariant*(self: View): QVariant {.slot.} = @@ -64,9 +74,12 @@ QtObject: QtProperty[QVariant] selectedKeypair: read = getSelectedKeypairAsVariant - proc setSelectedKeypair*(self: View, item: KeyPairItem) = + proc setSelectedKeypairItem*(self: View, item: KeyPairItem) = self.selectedKeypair.setItem(item) + proc setSelectedKeyPair*(self: View, keyUid: string) {.slot.} = + self.delegate.setSelectedKeyPairByKeyUid(keyUid) + proc getPrivateKeyAccAddress*(self: View): DerivedAddressItem = return self.privateKeyAccAddress @@ -100,3 +113,23 @@ QtObject: proc setEnteredPrivateKeyMatchTheKeypair*(self: View, value: bool) = self.enteredPrivateKeyMatchTheKeypair = value self.enteredPrivateKeyMatchTheKeypairChanged() + + proc keypairModel*(self: View): KeyPairModel = + return self.keypairModel + + proc keypairModelChanged(self: View) {.signal.} + proc getKeypairModel(self: View): QVariant {.slot.} = + if self.keypairModelVariant.isNil: + return newQVariant() + 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) + self.keypairModel.setItems(items) + self.keypairModelChanged() \ No newline at end of file diff --git a/src/app_service/service/wallet_account/dto/keypair_dto.nim b/src/app_service/service/wallet_account/dto/keypair_dto.nim index f6ca0fc9cf..354ebda597 100644 --- a/src/app_service/service/wallet_account/dto/keypair_dto.nim +++ b/src/app_service/service/wallet_account/dto/keypair_dto.nim @@ -10,8 +10,7 @@ const KeypairTypeProfile* = "profile" const KeypairTypeSeed* = "seed" const KeypairTypeKey* = "key" -const SyncedFromBackup* = "backup" # means a account is coming from backed up data -const SyncedFromLocalPairing* = "local-pairing" # means a account is coming from another device when user is reocovering Status account +const SyncedFromBackup* = "backup" # means a keypair is coming from backed up data type KeypairDto* = ref object of RootObj diff --git a/src/app_service/service/wallet_account/service_account.nim b/src/app_service/service/wallet_account/service_account.nim index 5d486c0b5f..0688b800b5 100644 --- a/src/app_service/service/wallet_account/service_account.nim +++ b/src/app_service/service/wallet_account/service_account.nim @@ -591,6 +591,10 @@ proc handleWalletAccount(self: Service, account: WalletAccountDto, notify: bool proc handleKeypair(self: Service, keypair: KeypairDto) = let localKp = self.getKeypairByKeyUid(keypair.keyUid) if not localKp.isNil: + # sotore only keypair fields which may change + localKp.name = keypair.name + localKp.lastUsedDerivationIndex = keypair.lastUsedDerivationIndex + localKp.syncedFrom = keypair.syncedFrom # - first remove removed accounts from the UI for localAcc in localKp.accounts: let accAddress = localAcc.address diff --git a/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml b/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml index f916570285..a286f52a38 100644 --- a/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml +++ b/ui/app/AppLayouts/Profile/popups/WalletKeypairAccountMenu.qml @@ -20,10 +20,12 @@ StatusMenu { enabled: !!root.keyPair && root.keyPair.pairType !== Constants.keypair.type.profile && !root.keyPair.migratedToKeycard && - root.keyPair.operability === Constants.keypair.operability.fullyOperable + root.keyPair.operability !== Constants.keypair.operability.nonOperable icon.name: "qr" icon.color: Theme.palette.primaryColor1 onTriggered: { + // in this case we need to check if any account of a keypair is partially operable and not migrated to a keycard + // and if so we need to create a keystore for them first and then proceed with qr code console.warn("TODO: show encrypted QR") } } @@ -46,6 +48,7 @@ StatusMenu { text: enabled? qsTr("Import keypair from device via encrypted QR") : "" enabled: !!root.keyPair && root.keyPair.pairType !== Constants.keypair.type.profile && + !root.keyPair.migratedToKeycard && root.keyPair.operability === Constants.keypair.operability.nonOperable && root.keyPair.syncedFrom !== Constants.keypair.syncedFrom.backup icon.name: "qr-scan" @@ -58,6 +61,7 @@ StatusMenu { StatusAction { text: enabled? root.keyPair.pairType === Constants.keypair.type.privateKeyImport? qsTr("Import via entering private key") : qsTr("Import via entering seed phrase") : "" enabled: !!root.keyPair && + !root.keyPair.migratedToKeycard && root.keyPair.operability === Constants.keypair.operability.nonOperable && (root.keyPair.pairType === Constants.keypair.type.seedImport || root.keyPair.pairType === Constants.keypair.type.privateKeyImport) diff --git a/ui/app/AppLayouts/Profile/views/wallet/MainView.qml b/ui/app/AppLayouts/Profile/views/wallet/MainView.qml index 80a51b4c33..961d0024f1 100644 --- a/ui/app/AppLayouts/Profile/views/wallet/MainView.qml +++ b/ui/app/AppLayouts/Profile/views/wallet/MainView.qml @@ -130,6 +130,51 @@ Column { } } + Rectangle { + id: importMissingKeypairs + visible: importMissingKeypairs.unimportedKeypairs > 0 + height: 102 + width: parent.width + color: Theme.palette.transparent + radius: 8 + border.width: 1 + border.color: Theme.palette.baseColor5 + + readonly property int unimportedKeypairs: { + let total = 0 + for (var i = 0; i < keypairsRepeater.count; i++) { + let kp = keypairsRepeater.itemAt(i).keyPair + if (kp.migratedToKeycard || + kp.operability === Constants.keypair.operability.fullyOperable || + kp.operability === Constants.keypair.operability.partiallyOperable) { + continue + } + total++ + } + return total + } + + Column { + anchors.fill: parent + padding: 16 + spacing: 8 + + StatusBaseText { + text: qsTr("%n keypair(s) require import to use on this device", "", importMissingKeypairs.unimportedKeypairs) + font.pixelSize: 15 + } + + StatusButton { + text: qsTr("Import missing keypairs") + type: StatusBaseButton.Type.Warning + icon.name: "download" + onClicked: { + root.walletStore.runKeypairImportPopup("", Constants.keypairImportPopup.importOption.selectKeypair) + } + } + } + } + Spacer { width: parent.width } @@ -138,6 +183,7 @@ Column { width: parent.width spacing: 24 Repeater { + id: keypairsRepeater objectName: "generatedAccounts" model: walletStore.originModel delegate: WalletKeyPairDelegate { diff --git a/ui/imports/shared/popups/keycard/helpers/KeyPairItem.qml b/ui/imports/shared/popups/keycard/helpers/KeyPairItem.qml index 8281f97a36..882287c38e 100644 --- a/ui/imports/shared/popups/keycard/helpers/KeyPairItem.qml +++ b/ui/imports/shared/popups/keycard/helpers/KeyPairItem.qml @@ -2,6 +2,7 @@ import QtQuick 2.14 import QtQml.Models 2.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 @@ -12,12 +13,12 @@ import utils 1.0 StatusListItem { id: root - property var sharedKeycardModule property ButtonGroup buttonGroup property bool usedAsSelectOption: false property bool tagClickable: false property bool tagDisplayRemoveAccountButton: false property bool canBeSelected: true + property bool displayRadioButtonForSelection: true property int keyPairType: Constants.keycard.keyPairType.unknown property string keyPairKeyUid: "" @@ -115,23 +116,38 @@ StatusListItem { property list components: [ StatusRadioButton { id: radioButton - visible: root.usedAsSelectOption + visible: root.usedAsSelectOption && root.displayRadioButtonForSelection ButtonGroup.group: root.buttonGroup onCheckedChanged: { - if (!root.usedAsSelectOption || !root.canBeSelected) - return - if (checked) { - root.sharedKeycardModule.setSelectedKeyPair(root.keyPairKeyUid) - root.keyPairSelected() + d.doAction(checked) + } + }, + StatusIcon { + visible: root.usedAsSelectOption && !root.displayRadioButtonForSelection + icon: "next" + color: Theme.palette.baseColor1 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + d.doAction(true) } } } ] + + function doAction(checked){ + if (!root.usedAsSelectOption || !root.canBeSelected) + return + if (checked) { + root.keyPairSelected() + } + } } onClicked: { if (!root.usedAsSelectOption || !root.canBeSelected) return - radioButton.checked = true + d.doAction(!root.displayRadioButtonForSelection || radioButton.checked) } } diff --git a/ui/imports/shared/popups/keycard/helpers/KeyPairList.qml b/ui/imports/shared/popups/keycard/helpers/KeyPairList.qml index 2ba820ac3e..a4cd7fb252 100644 --- a/ui/imports/shared/popups/keycard/helpers/KeyPairList.qml +++ b/ui/imports/shared/popups/keycard/helpers/KeyPairList.qml @@ -8,25 +8,18 @@ import SortFilterProxyModel 0.2 Item { id: root - property var sharedKeycardModule - property bool filterProfilePair: false property var keyPairModel property ButtonGroup buttonGroup + property bool disableSelectionForKeypairsWithNonDefaultDerivationPath: true + property bool displayRadioButtonForSelection: true + property string optionLabel: "" + property alias modelFilters: proxyModel.filters - signal keyPairSelected() - - QtObject { - id: d - readonly property string profilePairTypeValue: Constants.keycard.keyPairType.profile - } + signal keyPairSelected(string keyUid) SortFilterProxyModel { id: proxyModel sourceModel: root.keyPairModel - filters: ExpressionFilter { - expression: model.keyPair.pairType == d.profilePairTypeValue - inverted: !root.filterProfilePair - } } ListView { @@ -37,10 +30,12 @@ Item { delegate: KeyPairItem { width: ListView.view.width - sharedKeycardModule: root.sharedKeycardModule + label: root.optionLabel buttonGroup: root.buttonGroup usedAsSelectOption: true - canBeSelected: !model.keyPair.containsPathOutOfTheDefaultStatusDerivationTree() + canBeSelected: !root.disableSelectionForKeypairsWithNonDefaultDerivationPath || + !model.keyPair.containsPathOutOfTheDefaultStatusDerivationTree() + displayRadioButtonForSelection: root.displayRadioButtonForSelection keyPairType: model.keyPair.pairType keyPairKeyUid: model.keyPair.keyUid @@ -51,7 +46,7 @@ Item { keyPairAccounts: model.keyPair.accounts onKeyPairSelected: { - root.keyPairSelected() + root.keyPairSelected(model.keyPair.keyUid) } } } diff --git a/ui/imports/shared/popups/keycard/states/SelectKeyPair.qml b/ui/imports/shared/popups/keycard/states/SelectKeyPair.qml index 0dce3eb02d..d8ae15c123 100644 --- a/ui/imports/shared/popups/keycard/states/SelectKeyPair.qml +++ b/ui/imports/shared/popups/keycard/states/SelectKeyPair.qml @@ -11,6 +11,8 @@ import StatusQ.Controls 0.1 import utils 1.0 import shared.status 1.0 +import SortFilterProxyModel 0.2 + import "../helpers" Item { @@ -20,6 +22,11 @@ Item { signal keyPairSelected() + QtObject { + id: d + readonly property string profilePairTypeValue: Constants.keycard.keyPairType.profile + } + ColumnLayout { anchors.fill: parent anchors.topMargin: Style.current.xlPadding @@ -77,12 +84,14 @@ Item { Layout.alignment: Qt.AlignLeft Layout.preferredWidth: parent.width - sharedKeycardModule: root.sharedKeycardModule - filterProfilePair: true + modelFilters: ExpressionFilter { + expression: model.keyPair.pairType == d.profilePairTypeValue + } keyPairModel: root.sharedKeycardModule.keyPairModel buttonGroup: keyPairsButtonGroup onKeyPairSelected: { + root.sharedKeycardModule.setSelectedKeyPair(keyUid) root.keyPairSelected() } } @@ -107,11 +116,15 @@ Item { Layout.alignment: Qt.AlignLeft Layout.preferredWidth: parent.width - sharedKeycardModule: root.sharedKeycardModule + modelFilters: ExpressionFilter { + expression: model.keyPair.pairType == d.profilePairTypeValue + inverted: true + } keyPairModel: root.sharedKeycardModule.keyPairModel buttonGroup: keyPairsButtonGroup onKeyPairSelected: { + root.sharedKeycardModule.setSelectedKeyPair(keyUid) root.keyPairSelected() } } diff --git a/ui/imports/shared/popups/keypairimport/KeypairImportPopup.qml b/ui/imports/shared/popups/keypairimport/KeypairImportPopup.qml index 8058a7753e..52069921f2 100644 --- a/ui/imports/shared/popups/keypairimport/KeypairImportPopup.qml +++ b/ui/imports/shared/popups/keypairimport/KeypairImportPopup.qml @@ -8,6 +8,7 @@ import StatusQ.Controls 0.1 import utils 1.0 import "./stores" +import "./states" import "../common" StatusModal { @@ -20,6 +21,15 @@ StatusModal { closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside headerSettings.title: { + switch (root.store.currentState.stateType) { + case Constants.keypairImportPopup.state.selectKeypair: + return qsTr("Import missing keypairs") + case Constants.keypairImportPopup.state.selectImportMethod: + return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name) + case Constants.keypairImportPopup.state.scanQr: + return qsTr("Scan encrypted QR") + } + return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name) } @@ -46,6 +56,12 @@ StatusModal { width: parent.width sourceComponent: { switch (root.store.currentState.stateType) { + case Constants.keypairImportPopup.state.selectKeypair: + return selectKeypairComponent + case Constants.keypairImportPopup.state.selectImportMethod: + return selectImportMethodComponent + case Constants.keypairImportPopup.state.scanQr: + return scanQrComponent case Constants.keypairImportPopup.state.importPrivateKey: return keypairImportPrivateKeyComponent case Constants.keypairImportPopup.state.importSeedPhrase: @@ -60,6 +76,28 @@ StatusModal { } } + Component { + id: selectKeypairComponent + SelectKeypair { + height: Constants.keypairImportPopup.contentHeight + store: root.store + } + } + + Component { + id: selectImportMethodComponent + SelectImportMethod { + height: Constants.keypairImportPopup.contentHeight + store: root.store + } + } + + Component { + id: scanQrComponent + Item { + } + } + Component { id: keypairImportPrivateKeyComponent EnterPrivateKey { @@ -97,6 +135,8 @@ StatusModal { text: { switch (root.store.currentState.stateType) { + case Constants.keypairImportPopup.state.scanQr: + return qsTr("Done") case Constants.keypairImportPopup.state.importPrivateKey: case Constants.keypairImportPopup.state.importSeedPhrase: return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name) diff --git a/ui/imports/shared/popups/keypairimport/states/SelectImportMethod.qml b/ui/imports/shared/popups/keypairimport/states/SelectImportMethod.qml new file mode 100644 index 0000000000..e7759d033a --- /dev/null +++ b/ui/imports/shared/popups/keypairimport/states/SelectImportMethod.qml @@ -0,0 +1,100 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 + +import utils 1.0 + +import SortFilterProxyModel 0.2 + +import "../stores" + +Item { + id: root + + property KeypairImportStore store + + implicitHeight: layout.implicitHeight + + ColumnLayout { + id: layout + anchors.fill: parent + anchors.margins: 16 + spacing: 16 + + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Import method") + font.pixelSize: Constants.keypairImportPopup.labelFontSize1 + color: Theme.palette.baseColor1 + } + + StatusListItem { + title: qsTr("Import via scanning encrypted QR") + + asset { + width: 24 + height: 24 + name: "qr" + } + + components: [ + StatusIcon { + icon: "next" + color: Theme.palette.baseColor1 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.store.currentState.doPrimaryAction() + } + } + } + ] + + onClicked: { + root.store.currentState.doPrimaryAction() + } + } + + StatusListItem { + title: root.store.selectedKeypair.pairType === Constants.keypair.type.seedImport? + qsTr("Import via entering seed phrase") : + qsTr("Import via entering private key") + + asset { + width: 24 + height: 24 + name: root.store.selectedKeypair.pairType === Constants.keypair.type.seedImport? + "key_pair_seed_phrase" : + "objects" + } + + components: [ + StatusIcon { + icon: "next" + color: Theme.palette.baseColor1 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.store.currentState.doSecondaryAction() + } + } + } + ] + + onClicked: { + root.store.currentState.doSecondaryAction() + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + } +} diff --git a/ui/imports/shared/popups/keypairimport/states/SelectKeypair.qml b/ui/imports/shared/popups/keypairimport/states/SelectKeypair.qml new file mode 100644 index 0000000000..2032f0d333 --- /dev/null +++ b/ui/imports/shared/popups/keypairimport/states/SelectKeypair.qml @@ -0,0 +1,64 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +import StatusQ.Core 0.1 + +import shared.popups.keycard.helpers 1.0 + +import utils 1.0 + +import SortFilterProxyModel 0.2 + +import "../stores" + +Item { + id: root + + property KeypairImportStore store + + implicitHeight: layout.implicitHeight + + QtObject { + id: d + + readonly property string fullyOperableValue: Constants.keypair.operability.fullyOperable + readonly property string partiallyOperableValue: Constants.keypair.operability.partiallyOperable + } + + ColumnLayout { + id: layout + anchors.fill: parent + anchors.margins: 16 + spacing: 16 + + StatusBaseText { + Layout.fillWidth: true + text: qsTr("To use the associated accounts on this device, you need to import their keypairs.") + font.pixelSize: Constants.keypairImportPopup.labelFontSize1 + wrapMode: Text.WordWrap + } + + KeyPairList { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignLeft + + optionLabel: qsTr("import") + disableSelectionForKeypairsWithNonDefaultDerivationPath: false + displayRadioButtonForSelection: false + modelFilters: ExpressionFilter { + expression: model.keyPair.migratedToKeycard || + model.keyPair.operability == d.fullyOperableValue || + model.keyPair.operability == d.partiallyOperableValue + inverted: true + } + keyPairModel: root.store.keypairImportModule.keypairModel + + onKeyPairSelected: { + root.store.keypairImportModule.setSelectedKeyPair(keyUid) + root.store.currentState.doPrimaryAction() + } + } + } +} diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 8c7e0ba0dc..c732e7f872 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -487,8 +487,7 @@ QtObject { } readonly property QtObject syncedFrom: QtObject { - readonly property string backup: "backup" // means a account is coming from backed up data - readonly property string localPairing: "local-pairing" // means a account is coming from another device when user is reocovering Status account + readonly property string backup: "backup" // means an account is coming from backed up data } } @@ -743,14 +742,20 @@ QtObject { readonly property int popupWidth: 480 readonly property int contentHeight: 626 readonly property int footerButtonsHeight: 44 + readonly property int labelFontSize1: 15 readonly property QtObject importOption: QtObject { - readonly property int seedPhrase: 1 - readonly property int privateKey: 2 + readonly property int selectKeypair: 1 + readonly property int seedPhrase: 2 + readonly property int privateKey: 3 + readonly property int qrCode: 4 } readonly property QtObject state: QtObject { readonly property string noState: "NoState" + readonly property string selectKeypair: "SelectKeypair" + readonly property string selectImportMethod: "SelectImportMethod" + readonly property string scanQr: "ScanQr" readonly property string importSeedPhrase: "ImportSeedPhrase" readonly property string importPrivateKey: "ImportPrivateKey" } diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index 0cf37dfa91..215e7be40c 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -856,8 +856,7 @@ QtObject { return qsTr("Restored from backup. Re-enter private key to use.") } } - if (keypair.syncedFrom !== "" && - keypair.syncedFrom !== Constants.keypair.syncedFrom.localPairing) { + if (keypair.syncedFrom !== "") { return qsTr("Synced from %1").arg(keypair.syncedFrom) } } diff --git a/vendor/status-go b/vendor/status-go index b4b0d26aa4..6ee7038809 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit b4b0d26aa4c3f11ee66e3aeb404834cd8c5e48c8 +Subproject commit 6ee70388093f674c9d98e8d03f452ec4542d5c2f