feat(@desktop/syncing): make a not operable account fully operable, part 2

- handles import keypairs (without syncing via qr)

Closes the second part of #11779
This commit is contained in:
Sale Djenic 2023-08-09 12:41:55 +02:00 committed by saledjenic
parent 7d4df690c5
commit 4c6af4f1ad
22 changed files with 484 additions and 61 deletions

View File

@ -5,6 +5,7 @@ import app/core/eventemitter
import app_service/service/accounts/service as accounts_service import app_service/service/accounts/service as accounts_service
import app_service/service/wallet_account/service as wallet_account_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 import app/modules/shared_modules/keycard_popup/io_interface as keycard_shared_module
logScope: logScope:
@ -94,3 +95,9 @@ proc makePrivateKeyKeypairFullyOperable*(self: Controller, keyUid, privateKey, p
proc makeSeedPhraseKeypairFullyOperable*(self: Controller, keyUid, mnemonic, password: string, doPasswordHashing: bool): string = proc makeSeedPhraseKeypairFullyOperable*(self: Controller, keyUid, mnemonic, password: string, doPasswordHashing: bool): string =
return self.walletAccountService.makeSeedPhraseKeypairFullyOperable(keyUid, mnemonic, password, doPasswordHashing) 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()

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -2,7 +2,9 @@ import ../controller
type StateType* {.pure.} = enum type StateType* {.pure.} = enum
NoState = "NoState" NoState = "NoState"
Main = "Main" SelectKeypair = "SelectKeypair"
SelectImportMethod = "SelectImportMethod"
ScanQr = "ScanQr"
ImportSeedPhrase = "ImportSeedPhrase" ImportSeedPhrase = "ImportSeedPhrase"
ImportPrivateKey = "ImportPrivateKey" ImportPrivateKey = "ImportPrivateKey"

View File

@ -1,5 +1,6 @@
import chronicles import chronicles
import ../controller import ../controller
import app/modules/shared_models/[keypair_item]
import state import state
@ -9,10 +10,19 @@ logScope:
# Forward declaration # Forward declaration
proc createState*(stateToBeCreated: StateType, backState: State): State 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_private_key_state
include import_seed_phrase_state include import_seed_phrase_state
proc createState*(stateToBeCreated: StateType, backState: State): 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: if stateToBeCreated == StateType.ImportPrivateKey:
return newImportPrivateKeyState(backState) return newImportPrivateKeyState(backState)
if stateToBeCreated == StateType.ImportSeedPhrase: if stateToBeCreated == StateType.ImportSeedPhrase:

View File

@ -1,10 +1,13 @@
import Tables, NimQml import Tables, NimQml
import app/modules/shared_models/[keypair_item]
import app_service/service/wallet_account/dto/derived_address_dto import app_service/service/wallet_account/dto/derived_address_dto
type ImportOption* {.pure.}= enum type ImportOption* {.pure.}= enum
SeedPhrase = 1, SelectKeypair = 1
PrivateKey = 2 SeedPhrase
PrivateKey
QrCode
type type
AccessInterface* {.pure inheritable.} = ref object of RootObj AccessInterface* {.pure inheritable.} = ref object of RootObj
@ -27,6 +30,9 @@ method onBackActionClicked*(self: AccessInterface) {.base.} =
method onPrimaryActionClicked*(self: AccessInterface) {.base.} = method onPrimaryActionClicked*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method onSecondaryActionClicked*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onCancelActionClicked*(self: AccessInterface) {.base.} = method onCancelActionClicked*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available") 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.} = method onAddressDetailsFetched*(self: AccessInterface, derivedAddresses: seq[DerivedAddressDto], error: string) {.base.} =
raise newException(ValueError, "No implementation available") 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 type
DelegateInterface* = concept c DelegateInterface* = concept c
c.onKeypairImportModuleLoaded() c.onKeypairImportModuleLoaded()

View File

@ -7,7 +7,7 @@ import internal/[state, state_factory]
import app/global/global_singleton import app/global/global_singleton
import app/core/eventemitter import app/core/eventemitter
import app/modules/shared/keypairs 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/accounts/service as accounts_service
import app_service/service/wallet_account/service as wallet_account_service import app_service/service/wallet_account/service as wallet_account_service
@ -48,17 +48,23 @@ method getModuleAsVariant*[T](self: Module[T]): QVariant =
method load*[T](self: Module[T], keyUid: string, importOption: ImportOption) = method load*[T](self: Module[T], keyUid: string, importOption: ImportOption) =
self.controller.init() self.controller.init()
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) let keypair = self.controller.getKeypairByKeyUid(keyUid)
if keypair.isNil: if keypair.isNil:
error "trying to import an unknown keypair" error "ki_trying to import an unknown keypair"
self.closeKeypairImportPopup() self.closeKeypairImportPopup()
return return
let keypairItem = buildKeypairItem(keypair, areTestNetworksEnabled = false) # testnetworks are irrelevant in this context let keypairItem = buildKeypairItem(keypair, areTestNetworksEnabled = false) # testnetworks are irrelevant in this context
if keypairItem.isNil: if keypairItem.isNil:
error "cannot generate keypair item for provided keypair" error "ki_cannot generate keypair item for provided keypair"
self.closeKeypairImportPopup() self.closeKeypairImportPopup()
return return
self.view.setSelectedKeypair(keypairItem) self.view.setSelectedKeypairItem(keypairItem)
if importOption == ImportOption.PrivateKey: if importOption == ImportOption.PrivateKey:
self.view.setCurrentState(newImportPrivateKeyState(nil)) self.view.setCurrentState(newImportPrivateKeyState(nil))
elif importOption == ImportOption.SeedPhrase: elif importOption == ImportOption.SeedPhrase:
@ -99,16 +105,40 @@ method onPrimaryActionClicked*[T](self: Module[T]) =
self.view.setCurrentState(nextState) self.view.setCurrentState(nextState)
debug "ki_primary_action - set state", setCurrState=nextState.stateType() 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]) = proc authenticateLoggedInUser[T](self: Module[T]) =
self.controller.authenticateLoggedInUser() 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) = method changePrivateKey*[T](self: Module[T], privateKey: string) =
self.view.setPrivateKeyAccAddress(newDerivedAddressItem()) self.view.setPrivateKeyAccAddress(newDerivedAddressItem())
if privateKey.len == 0: if privateKey.len == 0:
return return
let genAccDto = self.controller.createAccountFromPrivateKey(privateKey) let genAccDto = self.controller.createAccountFromPrivateKey(privateKey)
if genAccDto.address.len == 0: 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 return
let kp = self.view.getSelectedKeypair() let kp = self.view.getSelectedKeypair()
if kp.isNil: if kp.isNil:
@ -116,7 +146,7 @@ method changePrivateKey*[T](self: Module[T], privateKey: string) =
return return
if kp.getKeyUid() != genAccDto.keyUid: if kp.getKeyUid() != genAccDto.keyUid:
self.view.setEnteredPrivateKeyMatchTheKeypair(false) 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 return
self.view.setEnteredPrivateKeyMatchTheKeypair(true) self.view.setEnteredPrivateKeyMatchTheKeypair(true)
self.view.setPrivateKeyAccAddress(newDerivedAddressItem(order = 0, address = genAccDto.address, publicKey = genAccDto.publicKey)) 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 return
let genAccDto = self.controller.createAccountFromSeedPhrase(seedPhrase) let genAccDto = self.controller.createAccountFromSeedPhrase(seedPhrase)
if seedPhrase.len > 0 and genAccDto.address.len == 0: 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 return
method validSeedPhrase*[T](self: Module[T], seedPhrase: string): bool = method validSeedPhrase*[T](self: Module[T], seedPhrase: string): bool =

View File

@ -1,7 +1,7 @@
import NimQml import NimQml
import io_interface import io_interface
import internal/[state, state_wrapper] 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: QtObject:
type type
@ -9,6 +9,8 @@ QtObject:
delegate: io_interface.AccessInterface delegate: io_interface.AccessInterface
currentState: StateWrapper currentState: StateWrapper
currentStateVariant: QVariant currentStateVariant: QVariant
keypairModel: KeyPairModel
keypairModelVariant: QVariant
selectedKeypair: KeyPairItem selectedKeypair: KeyPairItem
selectedKeypairVariant: QVariant selectedKeypairVariant: QVariant
privateKeyAccAddress: DerivedAddressItem privateKeyAccAddress: DerivedAddressItem
@ -20,6 +22,10 @@ QtObject:
self.currentState.delete self.currentState.delete
self.selectedKeypair.delete self.selectedKeypair.delete
self.selectedKeypairVariant.delete self.selectedKeypairVariant.delete
if not self.keypairModel.isNil:
self.keypairModel.delete
if not self.keypairModelVariant.isNil:
self.keypairModelVariant.delete
self.QObject.delete self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface): View = proc newView*(delegate: io_interface.AccessInterface): View =
@ -37,6 +43,7 @@ QtObject:
signalConnect(result.currentState, "backActionClicked()", result, "onBackActionClicked()", 2) signalConnect(result.currentState, "backActionClicked()", result, "onBackActionClicked()", 2)
signalConnect(result.currentState, "cancelActionClicked()", result, "onCancelActionClicked()", 2) signalConnect(result.currentState, "cancelActionClicked()", result, "onCancelActionClicked()", 2)
signalConnect(result.currentState, "primaryActionClicked()", result, "onPrimaryActionClicked()", 2) signalConnect(result.currentState, "primaryActionClicked()", result, "onPrimaryActionClicked()", 2)
signalConnect(result.currentState, "secondaryActionClicked()", result, "onSecondaryActionClicked()", 2)
proc currentStateObj*(self: View): State = proc currentStateObj*(self: View): State =
return self.currentState.getStateObj() return self.currentState.getStateObj()
@ -57,6 +64,9 @@ QtObject:
proc onPrimaryActionClicked*(self: View) {.slot.} = proc onPrimaryActionClicked*(self: View) {.slot.} =
self.delegate.onPrimaryActionClicked() self.delegate.onPrimaryActionClicked()
proc onSecondaryActionClicked*(self: View) {.slot.} =
self.delegate.onSecondaryActionClicked()
proc getSelectedKeypair*(self: View): KeyPairItem = proc getSelectedKeypair*(self: View): KeyPairItem =
return self.selectedKeypair return self.selectedKeypair
proc getSelectedKeypairAsVariant*(self: View): QVariant {.slot.} = proc getSelectedKeypairAsVariant*(self: View): QVariant {.slot.} =
@ -64,9 +74,12 @@ QtObject:
QtProperty[QVariant] selectedKeypair: QtProperty[QVariant] selectedKeypair:
read = getSelectedKeypairAsVariant read = getSelectedKeypairAsVariant
proc setSelectedKeypair*(self: View, item: KeyPairItem) = proc setSelectedKeypairItem*(self: View, item: KeyPairItem) =
self.selectedKeypair.setItem(item) self.selectedKeypair.setItem(item)
proc setSelectedKeyPair*(self: View, keyUid: string) {.slot.} =
self.delegate.setSelectedKeyPairByKeyUid(keyUid)
proc getPrivateKeyAccAddress*(self: View): DerivedAddressItem = proc getPrivateKeyAccAddress*(self: View): DerivedAddressItem =
return self.privateKeyAccAddress return self.privateKeyAccAddress
@ -100,3 +113,23 @@ QtObject:
proc setEnteredPrivateKeyMatchTheKeypair*(self: View, value: bool) = proc setEnteredPrivateKeyMatchTheKeypair*(self: View, value: bool) =
self.enteredPrivateKeyMatchTheKeypair = value self.enteredPrivateKeyMatchTheKeypair = value
self.enteredPrivateKeyMatchTheKeypairChanged() 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()

View File

@ -10,8 +10,7 @@ const KeypairTypeProfile* = "profile"
const KeypairTypeSeed* = "seed" const KeypairTypeSeed* = "seed"
const KeypairTypeKey* = "key" const KeypairTypeKey* = "key"
const SyncedFromBackup* = "backup" # means a account is coming from backed up data const SyncedFromBackup* = "backup" # means a keypair is coming from backed up data
const SyncedFromLocalPairing* = "local-pairing" # means a account is coming from another device when user is reocovering Status account
type type
KeypairDto* = ref object of RootObj KeypairDto* = ref object of RootObj

View File

@ -591,6 +591,10 @@ proc handleWalletAccount(self: Service, account: WalletAccountDto, notify: bool
proc handleKeypair(self: Service, keypair: KeypairDto) = proc handleKeypair(self: Service, keypair: KeypairDto) =
let localKp = self.getKeypairByKeyUid(keypair.keyUid) let localKp = self.getKeypairByKeyUid(keypair.keyUid)
if not localKp.isNil: 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 # - first remove removed accounts from the UI
for localAcc in localKp.accounts: for localAcc in localKp.accounts:
let accAddress = localAcc.address let accAddress = localAcc.address

View File

@ -20,10 +20,12 @@ StatusMenu {
enabled: !!root.keyPair && enabled: !!root.keyPair &&
root.keyPair.pairType !== Constants.keypair.type.profile && root.keyPair.pairType !== Constants.keypair.type.profile &&
!root.keyPair.migratedToKeycard && !root.keyPair.migratedToKeycard &&
root.keyPair.operability === Constants.keypair.operability.fullyOperable root.keyPair.operability !== Constants.keypair.operability.nonOperable
icon.name: "qr" icon.name: "qr"
icon.color: Theme.palette.primaryColor1 icon.color: Theme.palette.primaryColor1
onTriggered: { 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") console.warn("TODO: show encrypted QR")
} }
} }
@ -46,6 +48,7 @@ StatusMenu {
text: enabled? qsTr("Import keypair from device via encrypted QR") : "" text: enabled? qsTr("Import keypair from device via encrypted QR") : ""
enabled: !!root.keyPair && enabled: !!root.keyPair &&
root.keyPair.pairType !== Constants.keypair.type.profile && root.keyPair.pairType !== Constants.keypair.type.profile &&
!root.keyPair.migratedToKeycard &&
root.keyPair.operability === Constants.keypair.operability.nonOperable && root.keyPair.operability === Constants.keypair.operability.nonOperable &&
root.keyPair.syncedFrom !== Constants.keypair.syncedFrom.backup root.keyPair.syncedFrom !== Constants.keypair.syncedFrom.backup
icon.name: "qr-scan" icon.name: "qr-scan"
@ -58,6 +61,7 @@ StatusMenu {
StatusAction { StatusAction {
text: enabled? root.keyPair.pairType === Constants.keypair.type.privateKeyImport? qsTr("Import via entering private key") : qsTr("Import via entering seed phrase") : "" text: enabled? root.keyPair.pairType === Constants.keypair.type.privateKeyImport? qsTr("Import via entering private key") : qsTr("Import via entering seed phrase") : ""
enabled: !!root.keyPair && enabled: !!root.keyPair &&
!root.keyPair.migratedToKeycard &&
root.keyPair.operability === Constants.keypair.operability.nonOperable && root.keyPair.operability === Constants.keypair.operability.nonOperable &&
(root.keyPair.pairType === Constants.keypair.type.seedImport || (root.keyPair.pairType === Constants.keypair.type.seedImport ||
root.keyPair.pairType === Constants.keypair.type.privateKeyImport) root.keyPair.pairType === Constants.keypair.type.privateKeyImport)

View File

@ -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 { Spacer {
width: parent.width width: parent.width
} }
@ -138,6 +183,7 @@ Column {
width: parent.width width: parent.width
spacing: 24 spacing: 24
Repeater { Repeater {
id: keypairsRepeater
objectName: "generatedAccounts" objectName: "generatedAccounts"
model: walletStore.originModel model: walletStore.originModel
delegate: WalletKeyPairDelegate { delegate: WalletKeyPairDelegate {

View File

@ -2,6 +2,7 @@ import QtQuick 2.14
import QtQml.Models 2.14 import QtQml.Models 2.14
import QtQuick.Controls 2.14 import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 import StatusQ.Core.Utils 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
@ -12,12 +13,12 @@ import utils 1.0
StatusListItem { StatusListItem {
id: root id: root
property var sharedKeycardModule
property ButtonGroup buttonGroup property ButtonGroup buttonGroup
property bool usedAsSelectOption: false property bool usedAsSelectOption: false
property bool tagClickable: false property bool tagClickable: false
property bool tagDisplayRemoveAccountButton: false property bool tagDisplayRemoveAccountButton: false
property bool canBeSelected: true property bool canBeSelected: true
property bool displayRadioButtonForSelection: true
property int keyPairType: Constants.keycard.keyPairType.unknown property int keyPairType: Constants.keycard.keyPairType.unknown
property string keyPairKeyUid: "" property string keyPairKeyUid: ""
@ -115,23 +116,38 @@ StatusListItem {
property list<Item> components: [ property list<Item> components: [
StatusRadioButton { StatusRadioButton {
id: radioButton id: radioButton
visible: root.usedAsSelectOption visible: root.usedAsSelectOption && root.displayRadioButtonForSelection
ButtonGroup.group: root.buttonGroup ButtonGroup.group: root.buttonGroup
onCheckedChanged: { onCheckedChanged: {
if (!root.usedAsSelectOption || !root.canBeSelected) d.doAction(checked)
return }
if (checked) { },
root.sharedKeycardModule.setSelectedKeyPair(root.keyPairKeyUid) StatusIcon {
root.keyPairSelected() 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: { onClicked: {
if (!root.usedAsSelectOption || !root.canBeSelected) if (!root.usedAsSelectOption || !root.canBeSelected)
return return
radioButton.checked = true d.doAction(!root.displayRadioButtonForSelection || radioButton.checked)
} }
} }

View File

@ -8,25 +8,18 @@ import SortFilterProxyModel 0.2
Item { Item {
id: root id: root
property var sharedKeycardModule
property bool filterProfilePair: false
property var keyPairModel property var keyPairModel
property ButtonGroup buttonGroup property ButtonGroup buttonGroup
property bool disableSelectionForKeypairsWithNonDefaultDerivationPath: true
property bool displayRadioButtonForSelection: true
property string optionLabel: ""
property alias modelFilters: proxyModel.filters
signal keyPairSelected() signal keyPairSelected(string keyUid)
QtObject {
id: d
readonly property string profilePairTypeValue: Constants.keycard.keyPairType.profile
}
SortFilterProxyModel { SortFilterProxyModel {
id: proxyModel id: proxyModel
sourceModel: root.keyPairModel sourceModel: root.keyPairModel
filters: ExpressionFilter {
expression: model.keyPair.pairType == d.profilePairTypeValue
inverted: !root.filterProfilePair
}
} }
ListView { ListView {
@ -37,10 +30,12 @@ Item {
delegate: KeyPairItem { delegate: KeyPairItem {
width: ListView.view.width width: ListView.view.width
sharedKeycardModule: root.sharedKeycardModule label: root.optionLabel
buttonGroup: root.buttonGroup buttonGroup: root.buttonGroup
usedAsSelectOption: true usedAsSelectOption: true
canBeSelected: !model.keyPair.containsPathOutOfTheDefaultStatusDerivationTree() canBeSelected: !root.disableSelectionForKeypairsWithNonDefaultDerivationPath ||
!model.keyPair.containsPathOutOfTheDefaultStatusDerivationTree()
displayRadioButtonForSelection: root.displayRadioButtonForSelection
keyPairType: model.keyPair.pairType keyPairType: model.keyPair.pairType
keyPairKeyUid: model.keyPair.keyUid keyPairKeyUid: model.keyPair.keyUid
@ -51,7 +46,7 @@ Item {
keyPairAccounts: model.keyPair.accounts keyPairAccounts: model.keyPair.accounts
onKeyPairSelected: { onKeyPairSelected: {
root.keyPairSelected() root.keyPairSelected(model.keyPair.keyUid)
} }
} }
} }

View File

@ -11,6 +11,8 @@ import StatusQ.Controls 0.1
import utils 1.0 import utils 1.0
import shared.status 1.0 import shared.status 1.0
import SortFilterProxyModel 0.2
import "../helpers" import "../helpers"
Item { Item {
@ -20,6 +22,11 @@ Item {
signal keyPairSelected() signal keyPairSelected()
QtObject {
id: d
readonly property string profilePairTypeValue: Constants.keycard.keyPairType.profile
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: Style.current.xlPadding anchors.topMargin: Style.current.xlPadding
@ -77,12 +84,14 @@ Item {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
sharedKeycardModule: root.sharedKeycardModule modelFilters: ExpressionFilter {
filterProfilePair: true expression: model.keyPair.pairType == d.profilePairTypeValue
}
keyPairModel: root.sharedKeycardModule.keyPairModel keyPairModel: root.sharedKeycardModule.keyPairModel
buttonGroup: keyPairsButtonGroup buttonGroup: keyPairsButtonGroup
onKeyPairSelected: { onKeyPairSelected: {
root.sharedKeycardModule.setSelectedKeyPair(keyUid)
root.keyPairSelected() root.keyPairSelected()
} }
} }
@ -107,11 +116,15 @@ Item {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
sharedKeycardModule: root.sharedKeycardModule modelFilters: ExpressionFilter {
expression: model.keyPair.pairType == d.profilePairTypeValue
inverted: true
}
keyPairModel: root.sharedKeycardModule.keyPairModel keyPairModel: root.sharedKeycardModule.keyPairModel
buttonGroup: keyPairsButtonGroup buttonGroup: keyPairsButtonGroup
onKeyPairSelected: { onKeyPairSelected: {
root.sharedKeycardModule.setSelectedKeyPair(keyUid)
root.keyPairSelected() root.keyPairSelected()
} }
} }

View File

@ -8,6 +8,7 @@ import StatusQ.Controls 0.1
import utils 1.0 import utils 1.0
import "./stores" import "./stores"
import "./states"
import "../common" import "../common"
StatusModal { StatusModal {
@ -20,6 +21,15 @@ StatusModal {
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
headerSettings.title: { 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) return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name)
} }
@ -46,6 +56,12 @@ StatusModal {
width: parent.width width: parent.width
sourceComponent: { sourceComponent: {
switch (root.store.currentState.stateType) { 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: case Constants.keypairImportPopup.state.importPrivateKey:
return keypairImportPrivateKeyComponent return keypairImportPrivateKeyComponent
case Constants.keypairImportPopup.state.importSeedPhrase: 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 { Component {
id: keypairImportPrivateKeyComponent id: keypairImportPrivateKeyComponent
EnterPrivateKey { EnterPrivateKey {
@ -97,6 +135,8 @@ StatusModal {
text: { text: {
switch (root.store.currentState.stateType) { switch (root.store.currentState.stateType) {
case Constants.keypairImportPopup.state.scanQr:
return qsTr("Done")
case Constants.keypairImportPopup.state.importPrivateKey: case Constants.keypairImportPopup.state.importPrivateKey:
case Constants.keypairImportPopup.state.importSeedPhrase: case Constants.keypairImportPopup.state.importSeedPhrase:
return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name) return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name)

View File

@ -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
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -487,8 +487,7 @@ QtObject {
} }
readonly property QtObject syncedFrom: QtObject { readonly property QtObject syncedFrom: QtObject {
readonly property string backup: "backup" // means a account is coming from backed up data readonly property string backup: "backup" // means an 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
} }
} }
@ -743,14 +742,20 @@ QtObject {
readonly property int popupWidth: 480 readonly property int popupWidth: 480
readonly property int contentHeight: 626 readonly property int contentHeight: 626
readonly property int footerButtonsHeight: 44 readonly property int footerButtonsHeight: 44
readonly property int labelFontSize1: 15
readonly property QtObject importOption: QtObject { readonly property QtObject importOption: QtObject {
readonly property int seedPhrase: 1 readonly property int selectKeypair: 1
readonly property int privateKey: 2 readonly property int seedPhrase: 2
readonly property int privateKey: 3
readonly property int qrCode: 4
} }
readonly property QtObject state: QtObject { readonly property QtObject state: QtObject {
readonly property string noState: "NoState" 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 importSeedPhrase: "ImportSeedPhrase"
readonly property string importPrivateKey: "ImportPrivateKey" readonly property string importPrivateKey: "ImportPrivateKey"
} }

View File

@ -856,8 +856,7 @@ QtObject {
return qsTr("Restored from backup. Re-enter private key to use.") return qsTr("Restored from backup. Re-enter private key to use.")
} }
} }
if (keypair.syncedFrom !== "" && if (keypair.syncedFrom !== "") {
keypair.syncedFrom !== Constants.keypair.syncedFrom.localPairing) {
return qsTr("Synced from %1").arg(keypair.syncedFrom) return qsTr("Synced from %1").arg(keypair.syncedFrom)
} }
} }

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit b4b0d26aa4c3f11ee66e3aeb404834cd8c5e48c8 Subproject commit 6ee70388093f674c9d98e8d03f452ec4542d5c2f