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

Closes the third part of #11779
This commit is contained in:
Sale Djenic 2023-08-21 12:58:21 +02:00 committed by saledjenic
parent 85d4bfdfea
commit a1bf7bed19
46 changed files with 763 additions and 198 deletions

View File

@ -414,6 +414,13 @@ proc connectForNotificationsOnly[T](self: Module[T]) =
let args = KeypairArgs(e)
self.view.showToastKeypairRemoved(args.keyPairName)
self.events.on(SIGNAL_IMPORTED_KEYPAIRS) do(e:Args):
let args = KeypairsArgs(e)
var kpName: string
if args.keypairs.len > 0:
kpName = args.keypairs[0].name
self.view.showToastKeypairsImported(kpName, args.keypairs.len, args.error)
method load*[T](
self: Module[T],
events: EventEmitter,

View File

@ -56,6 +56,8 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE) do(e: Args):
let args = LocalPairingStatus(e)
if args.pairingType != PairingType.AppSync:
return
self.delegate.onLocalPairingStatusUpdate(args)

View File

@ -111,7 +111,8 @@ proc newModule*(delegate: delegate_interface.AccessInterface,
result.keycardModule = keycard_module.newModule(result, events, keycardService, settingsService, networkService,
privacyService, accountsService, walletAccountService, keychainService)
result.walletModule = wallet_module.newModule(result, events, accountsService, walletAccountService, settingsService, networkService)
result.walletModule = wallet_module.newModule(result, events, accountsService, walletAccountService, settingsService,
networkService, devicesService)
singletonInstance.engine.setRootContextProperty("profileSectionModule", result.viewVariant)

View File

@ -145,9 +145,12 @@ method load*(self: Module) =
let areTestNetworksEnabled = self.controller.areTestNetworksEnabled()
self.view.onUpdatedAccount(walletAccountToWalletAccountItem(args.account, keycardAccount, areTestNetworksEnabled))
self.events.on(SIGNAL_KEYPAIR_OPERABILITY_CHANGED) do(e:Args):
let args = KeypairArgs(e)
self.view.onUpdatedKeypairOperability(args.keypair.keyUid, AccountFullyOperable)
self.events.on(SIGNAL_IMPORTED_KEYPAIRS) do(e:Args):
let args = KeypairsArgs(e)
if args.error.len != 0:
return
for kp in args.keypairs:
self.view.onUpdatedKeypairOperability(kp.keyUid, AccountFullyOperable)
self.events.on(SIGNAL_NEW_KEYCARD_SET) do(e: Args):
let args = KeycardArgs(e)

View File

@ -46,7 +46,7 @@ method onKeypairImportModuleLoaded*(self: AccessInterface) {.base.} =
method destroyKeypairImportPopup*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method runKeypairImportPopup*(self: AccessInterface, keyUid: string, importOption: ImportOption) {.base.} =
method runKeypairImportPopup*(self: AccessInterface, keyUid: string, mode: ImportKeypairModuleMode) {.base.} =
raise newException(ValueError, "No implementation available")
method hasPairedDevices*(self: AccessInterface): bool {.base.} =

View File

@ -32,6 +32,7 @@ type
moduleLoaded: bool
accountsService: accounts_service.Service
walletAccountService: wallet_account_service.Service
devicesService: devices_service.Service
accountsModule: accounts_module.AccessInterface
networksModule: networks_module.AccessInterface
keypairImportModule: keypair_import_module.AccessInterface
@ -43,6 +44,7 @@ proc newModule*(
walletAccountService: wallet_account_service.Service,
settingsService: settings_service.Service,
networkService: network_service.Service,
devicesService: devices_service.Service
): Module =
result = Module()
result.delegate = delegate
@ -53,6 +55,7 @@ proc newModule*(
result.moduleLoaded = false
result.accountsService = accountsService
result.walletAccountService = walletAccountService
result.devicesService = devicesService
result.accountsModule = accounts_module.newModule(result, events, walletAccountService, networkService)
result.networksModule = networks_module.newModule(result, events, networkService, walletAccountService, settingsService)
@ -111,9 +114,10 @@ method destroyKeypairImportPopup*(self: Module) =
self.keypairImportModule.delete
self.keypairImportModule = nil
method runKeypairImportPopup*(self: Module, keyUid: string, importOption: ImportOption) =
self.keypairImportModule = keypair_import_module.newModule(self, self.events, self.accountsService, self.walletAccountService)
self.keypairImportModule.load(keyUid, importOption)
method runKeypairImportPopup*(self: Module, keyUid: string, mode: ImportKeypairModuleMode) =
self.keypairImportModule = keypair_import_module.newModule(self, self.events, self.accountsService,
self.walletAccountService, self.devicesService)
self.keypairImportModule.load(keyUid, mode)
method getKeypairImportModule*(self: Module): QVariant =
if self.keypairImportModule.isNil:

View File

@ -1,7 +1,7 @@
import NimQml
import ./io_interface
from app/modules/shared_modules/keypair_import/module import ImportOption
from app/modules/shared_modules/keypair_import/module import ImportKeypairModuleMode
QtObject:
type
@ -31,8 +31,8 @@ QtObject:
QtProperty[QVariant] collectiblesModel:
read = getCollectiblesModel
proc runKeypairImportPopup*(self: View, keyUid: string, importOption: int) {.slot.} =
self.delegate.runKeypairImportPopup(keyUid, ImportOption(importOption))
proc runKeypairImportPopup*(self: View, keyUid: string, mode: int) {.slot.} =
self.delegate.runKeypairImportPopup(keyUid, ImportKeypairModuleMode(mode))
proc getKeypairImportModule(self: View): QVariant {.slot.} =
return self.delegate.getKeypairImportModule()

View File

@ -277,3 +277,4 @@ QtObject:
proc showNetworkEndpointUpdated*(self: View, name: string, isTest: bool) {.signal.}
proc showIncludeWatchOnlyAccountUpdated*(self: View, includeWatchOnly: bool) {.signal.}
proc showToastKeypairRemoved*(self: View, keypairName: string) {.signal.}
proc showToastKeypairsImported*(self: View, keypairName: string, keypairsCount: int, error: string) {.signal.}

View File

@ -1,9 +1,11 @@
import times, chronicles
import uuids
import io_interface
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_service/service/devices/service as devices_service
import app/modules/shared_models/[keypair_item]
import app/modules/shared_modules/keycard_popup/io_interface as keycard_shared_module
@ -19,6 +21,8 @@ type
events: EventEmitter
accountsService: accounts_service.Service
walletAccountService: wallet_account_service.Service
devicesService: devices_service.Service
connectionIds: seq[UUID]
uniqueFetchingDetailsId: string
tmpPrivateKey: string
tmpSeedPhrase: string
@ -27,29 +31,37 @@ type
proc newController*(delegate: io_interface.AccessInterface,
events: EventEmitter,
accountsService: accounts_service.Service,
walletAccountService: wallet_account_service.Service):
walletAccountService: wallet_account_service.Service,
devicesService: devices_service.Service):
Controller =
result = Controller()
result.delegate = delegate
result.events = events
result.accountsService = accountsService
result.walletAccountService = walletAccountService
result.devicesService = devicesService
proc disconnectAll*(self: Controller) =
for id in self.connectionIds:
self.events.disconnect(id)
proc delete*(self: Controller) =
discard
self.disconnectAll()
proc init*(self: Controller) =
self.events.on(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args):
var handlerId = self.events.onWithUUID(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args):
let args = SharedKeycarModuleArgs(e)
if args.uniqueIdentifier != UNIQUE_WALLET_SECTION_KEYPAIR_IMPORT_MODULE_IDENTIFIER:
return
self.delegate.onUserAuthenticated(args.pin, args.password, args.keyUid)
self.connectionIds.add(handlerId)
self.events.on(SIGNAL_WALLET_ACCOUNT_ADDRESS_DETAILS_FETCHED) do(e:Args):
handlerId = self.events.onWithUUID(SIGNAL_WALLET_ACCOUNT_ADDRESS_DETAILS_FETCHED) do(e:Args):
var args = DerivedAddressesArgs(e)
if args.uniqueId != self.uniqueFetchingDetailsId:
return
self.delegate.onAddressDetailsFetched(args.derivedAddresses, args.error)
self.connectionIds.add(handlerId)
proc closeKeypairImportPopup*(self: Controller) =
self.delegate.closeKeypairImportPopup()
@ -100,4 +112,19 @@ proc getKeypairs*(self: Controller): seq[wallet_account_service.KeypairDto] =
return self.walletAccountService.getKeypairs()
proc getSelectedKeypair*(self: Controller): KeyPairItem =
return self.delegate.getSelectedKeypair()
return self.delegate.getSelectedKeypair()
proc clearSelectedKeypair*(self: Controller) =
self.delegate.clearSelectedKeypair()
proc setConnectionString*(self: Controller, connectionString: string) =
self.delegate.setConnectionString(connectionString)
proc generateConnectionStringForExportingKeypairsKeystores*(self: Controller, keyUids: seq[string], password: string): tuple[res: string, err: string] =
return self.devicesService.generateConnectionStringForExportingKeypairsKeystores(keyUids, password)
proc validateConnectionString*(self: Controller, connectionString: string): string =
return self.devicesService.validateConnectionString(connectionString)
proc inputConnectionStringForImportingKeypairsKeystores*(self: Controller, keyUids: seq[string], connectionString: string, password: string): string =
return self.devicesService.inputConnectionStringForImportingKeypairsKeystores(keyUids, connectionString, password)

View File

@ -0,0 +1,9 @@
type
DisplayInstructionsState* = ref object of State
proc newDisplayInstructionsState*(backState: State): DisplayInstructionsState =
result = DisplayInstructionsState()
result.setup(StateType.DisplayInstructions, backState)
proc delete*(self: DisplayInstructionsState) =
self.State.delete

View File

@ -0,0 +1,12 @@
type
ExportKeypairState* = ref object of State
proc newExportKeypairState*(backState: State): ExportKeypairState =
result = ExportKeypairState()
result.setup(StateType.ExportKeypair, backState)
proc delete*(self: ExportKeypairState) =
self.State.delete
method executePrePrimaryStateCommand*(self: ExportKeypairState, controller: Controller) =
controller.closeKeypairImportPopup()

View File

@ -0,0 +1,21 @@
type
ImportQrState* = ref object of State
proc newImportQrState*(backState: State): ImportQrState =
result = ImportQrState()
result.setup(StateType.ImportQr, backState)
proc delete*(self: ImportQrState) =
self.State.delete
method executePreBackStateCommand*(self: ImportQrState, controller: Controller) =
controller.setConnectionString("")
method getNextPrimaryState*(self: ImportQrState, controller: Controller): State =
controller.authenticateLoggedInUser()
method executePreSecondaryStateCommand*(self: ImportQrState, controller: Controller) =
controller.setConnectionString("")
method getNextSecondaryState*(self: ImportQrState, controller: Controller): State =
return createState(StateType.DisplayInstructions, self)

View File

@ -1,12 +0,0 @@
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

@ -8,8 +8,11 @@ proc newSelectImportMethodState*(backState: State): SelectImportMethodState =
proc delete*(self: SelectImportMethodState) =
self.State.delete
method executePreBackStateCommand*(self: SelectImportMethodState, controller: Controller) =
controller.clearSelectedKeypair()
method getNextPrimaryState*(self: SelectImportMethodState, controller: Controller): State =
return createState(StateType.ScanQr, self)
return createState(StateType.ImportQr, self)
method getNextSecondaryState*(self: SelectImportMethodState, controller: Controller): State =
let kp = controller.getSelectedKeypair()

View File

@ -8,5 +8,11 @@ proc newSelectKeypairState*(backState: State): SelectKeypairState =
proc delete*(self: SelectKeypairState) =
self.State.delete
method executePreBackStateCommand*(self: SelectKeypairState, controller: Controller) =
controller.clearSelectedKeypair()
method getNextPrimaryState*(self: SelectKeypairState, controller: Controller): State =
return createState(StateType.SelectImportMethod, self)
return createState(StateType.SelectImportMethod, self)
method getNextSecondaryState*(self: SelectKeypairState, controller: Controller): State =
return createState(StateType.ImportQr, self)

View File

@ -4,9 +4,11 @@ type StateType* {.pure.} = enum
NoState = "NoState"
SelectKeypair = "SelectKeypair"
SelectImportMethod = "SelectImportMethod"
ScanQr = "ScanQr"
ExportKeypair = "ExportKeypair"
ImportQr = "ImportQr"
ImportSeedPhrase = "ImportSeedPhrase"
ImportPrivateKey = "ImportPrivateKey"
DisplayInstructions = "DisplayInstructions"
## This is the base class for all states

View File

@ -12,20 +12,26 @@ proc createState*(stateToBeCreated: StateType, backState: State): State
include select_keypair_state
include select_import_method_state
include scan_qr_state
include export_keypair_state
include import_qr_state
include import_private_key_state
include import_seed_phrase_state
include display_instructions_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.ExportKeypair:
return newExportKeypairState(backState)
if stateToBeCreated == StateType.ImportQr:
return newImportQrState(backState)
if stateToBeCreated == StateType.ImportPrivateKey:
return newImportPrivateKeyState(backState)
if stateToBeCreated == StateType.ImportSeedPhrase:
return newImportSeedPhraseState(backState)
if stateToBeCreated == StateType.DisplayInstructions:
return newDisplayInstructionsState(backState)
error "Keypair import - no implementation available for state", state=stateToBeCreated

View File

@ -3,11 +3,12 @@ import Tables, NimQml
import app/modules/shared_models/[keypair_item]
import app_service/service/wallet_account/dto/derived_address_dto
type ImportOption* {.pure.}= enum
type ImportKeypairModuleMode* {.pure.}= enum
SelectKeypair = 1
SeedPhrase
PrivateKey
QrCode
ImportViaSeedPhrase
ImportViaPrivateKey
ImportViaQr
ExportKeypairQr
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
@ -15,7 +16,7 @@ type
method delete*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method load*(self: AccessInterface, keyUid: string, importOption: ImportOption) {.base.} =
method load*(self: AccessInterface, keyUid: string, mode: ImportKeypairModuleMode) {.base.} =
raise newException(ValueError, "No implementation available")
method closeKeypairImportPopup*(self: AccessInterface) {.base.} =
@ -54,9 +55,22 @@ method onAddressDetailsFetched*(self: AccessInterface, derivedAddresses: seq[Der
method getSelectedKeypair*(self: AccessInterface): KeyPairItem {.base.} =
raise newException(ValueError, "No implementation available")
method clearSelectedKeypair*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method setConnectionString*(self: AccessInterface, connectionString: string) {.base.} =
raise newException(ValueError, "No implementation available")
method setSelectedKeyPairByKeyUid*(self: AccessInterface, keyUid: string) {.base.} =
raise newException(ValueError, "No implementation available")
method generateConnectionStringForExporting*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method validateConnectionString*(self: AccessInterface, connectionString: string): string {.base.} =
raise newException(ValueError, "No implementation available")
type
DelegateInterface* = concept c
c.onKeypairImportModuleLoaded()

View File

@ -10,6 +10,7 @@ import app/modules/shared/keypairs
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
import app_service/service/devices/service as devices_service
export io_interface
@ -23,17 +24,19 @@ type
view: View
viewVariant: QVariant
controller: Controller
tmpPassword: string
proc newModule*[T](delegate: T,
events: EventEmitter,
accountsService: accounts_service.Service,
walletAccountService: wallet_account_service.Service):
walletAccountService: wallet_account_service.Service,
devicesService: devices_service.Service):
Module[T] =
result = Module[T]()
result.delegate = delegate
result.view = newView(result)
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events, accountsService, walletAccountService)
result.controller = controller.newController(result, events, accountsService, walletAccountService, devicesService)
method delete*[T](self: Module[T]) =
self.view.delete
@ -46,14 +49,23 @@ method closeKeypairImportPopup*[T](self: Module[T]) =
method getModuleAsVariant*[T](self: Module[T]): QVariant =
return self.viewVariant
method load*[T](self: Module[T], keyUid: string, importOption: ImportOption) =
method load*[T](self: Module[T], keyUid: string, mode: ImportKeypairModuleMode) =
self.controller.init()
if importOption == ImportOption.SelectKeypair:
if mode == ImportKeypairModuleMode.SelectKeypair:
let items = keypairs.buildKeyPairsList(self.controller.getKeypairs(), excludeAlreadyMigratedPairs = true,
excludePrivateKeyKeypairs = false)
self.view.createKeypairModel(items)
self.view.setCurrentState(newSelectKeypairState(nil))
else:
self.view.setSelectedKeypairItem(newKeyPairItem(keyUid = keyUid))
if mode == ImportKeypairModuleMode.ExportKeypairQr:
self.view.setCurrentState(newExportKeypairState(nil))
self.controller.authenticateLoggedInUser()
return
elif mode == ImportKeypairModuleMode.ImportViaQr:
self.view.setCurrentState(newImportQrState(nil))
self.delegate.onKeypairImportModuleLoaded()
return
let keypair = self.controller.getKeypairByKeyUid(keyUid)
if keypair.isNil:
error "ki_trying to import an unknown keypair"
@ -65,9 +77,9 @@ method load*[T](self: Module[T], keyUid: string, importOption: ImportOption) =
self.closeKeypairImportPopup()
return
self.view.setSelectedKeypairItem(keypairItem)
if importOption == ImportOption.PrivateKey:
if mode == ImportKeypairModuleMode.ImportViaPrivateKey:
self.view.setCurrentState(newImportPrivateKeyState(nil))
elif importOption == ImportOption.SeedPhrase:
elif mode == ImportKeypairModuleMode.ImportViaSeedPhrase:
self.view.setCurrentState(newImportSeedPhraseState(nil))
self.delegate.onKeypairImportModuleLoaded()
@ -118,12 +130,12 @@ method onSecondaryActionClicked*[T](self: Module[T]) =
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 clearSelectedKeypair*[T](self: Module[T]) =
self.view.setSelectedKeypairItem(newKeyPairItem())
method setSelectedKeyPairByKeyUid*[T](self: Module[T], keyUid: string) =
let item = self.view.keypairModel().findItemByKeyUid(keyUid)
if item.isNil:
@ -191,6 +203,30 @@ method onAddressDetailsFetched*[T](self: Module[T], derivedAddresses: seq[Derive
return
error "ki_unknown error, since the length of the response is not expected", length=derivedAddresses.len
method setConnectionString*[T](self: Module[T], connectionString: string) =
self.view.setConnectionString(connectionString)
method generateConnectionStringForExporting*[T](self: Module[T]) =
self.view.setConnectionString("")
self.view.setConnectionStringError("")
let currStateObj = self.view.currentStateObj()
if currStateObj.isNil:
error "ki_cannot resolve current state"
return
if currStateObj.stateType() == StateType.ExportKeypair:
var keyUids: seq[string]
let keyUid = self.view.getSelectedKeypair().getKeyUid()
if keyUid.len > 0:
keyUids.add(keyUid)
let (connectionString, err) = self.controller.generateConnectionStringForExportingKeypairsKeystores(keyUids, self.tmpPassword)
if err.len > 0:
self.view.setConnectionStringError(err)
return
self.view.setConnectionString(connectionString)
method validateConnectionString*[T](self: Module[T], connectionString: string): string =
return self.controller.validateConnectionString(connectionString)
method onUserAuthenticated*[T](self: Module[T], pin: string, password: string, keyUid: string) =
if password.len == 0:
info "ki_unsuccessful authentication"
@ -199,13 +235,27 @@ method onUserAuthenticated*[T](self: Module[T], pin: string, password: string, k
if currStateObj.isNil:
error "ki_cannot resolve current state"
return
if currStateObj.stateType() == StateType.ExportKeypair:
self.tmpPassword = password
self.generateConnectionStringForExporting()
self.delegate.onKeypairImportModuleLoaded()
return
if currStateObj.stateType() == StateType.ImportQr:
var keyUids: seq[string]
let keyUid = self.view.getSelectedKeypair().getKeyUid()
if keyUid.len > 0:
keyUids.add(keyUid)
let res = self.controller.inputConnectionStringForImportingKeypairsKeystores(keyUids, self.view.getConnectionString(), password)
if res.len > 0:
error "ki_unable to make a keypair operable", errDesription=res
return
if currStateObj.stateType() == StateType.ImportPrivateKey:
let res = self.controller.makePrivateKeyKeypairFullyOperable(self.controller.getGeneratedAccount().keyUid,
self.controller.getPrivateKey(),
password,
doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser())
if res.len > 0:
error "ki_unable to make a keypair operable"
error "ki_unable to make a keypair operable", errDesription=res
return
if currStateObj.stateType() == StateType.ImportSeedPhrase:
let res = self.controller.makeSeedPhraseKeypairFullyOperable(self.controller.getGeneratedAccount().keyUid,
@ -213,6 +263,6 @@ method onUserAuthenticated*[T](self: Module[T], pin: string, password: string, k
password,
doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser())
if res.len > 0:
error "ki_unable to make a keypair operable"
error "ki_unable to make a keypair operable", errDesription=res
return
self.closeKeypairImportPopup()
self.closeKeypairImportPopup()

View File

@ -16,6 +16,8 @@ QtObject:
privateKeyAccAddress: DerivedAddressItem
privateKeyAccAddressVariant: QVariant
enteredPrivateKeyMatchTheKeypair: bool
connectionString: string
connectionStringError: string
proc delete*(self: View) =
self.currentStateVariant.delete
@ -132,4 +134,31 @@ QtObject:
if self.keypairModelVariant.isNil:
self.keypairModelVariant = newQVariant(self.keypairModel)
self.keypairModel.setItems(items)
self.keypairModelChanged()
self.keypairModelChanged()
proc connectionStringChanged(self: View) {.signal.}
proc getConnectionString*(self: View): string {.slot.} =
return self.connectionString
proc setConnectionString*(self: View, connectionScreen: string) {.slot.} =
self.connectionString = connectionScreen
self.connectionStringChanged()
QtProperty[string] connectionString:
read = getConnectionString
write = setConnectionString
notify = connectionStringChanged
proc connectionStringErrorChanged(self: View) {.signal.}
proc getConnectionStringError*(self: View): string {.slot.} =
return self.connectionStringError
QtProperty[string] connectionStringError:
read = getConnectionStringError
notify = connectionStringErrorChanged
proc setConnectionStringError*(self: View, error: string) =
self.connectionStringError = error
self.connectionStringErrorChanged()
proc generateConnectionStringForExporting*(self: View) {.slot.} =
self.delegate.generateConnectionStringForExporting()
proc validateConnectionString*(self: View, connectionString: string): string {.slot.} =
return self.delegate.validateConnectionString(connectionString)

View File

@ -168,7 +168,10 @@ proc init*(self: Controller) =
self.connectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE) do(e: Args):
self.localPairingStatus = LocalPairingStatus(e)
let args = LocalPairingStatus(e)
if args.pairingType != PairingType.AppSync:
return
self.localPairingStatus = args
self.delegate.onLocalPairingStatusUpdate(self.localPairingStatus)
self.connectionIds.add(handlerId)

View File

@ -13,6 +13,19 @@ Item {
implicitWidth: layout.implicitWidth
implicitHeight: layout.implicitHeight
Timer {
id: nextStateDelay
property string connectionString
interval: 1000
repeat: false
onTriggered: {
root.startupStore.setConnectionString(nextStateDelay.connectionString)
root.startupStore.doPrimaryAction()
}
}
SyncingEnterCode {
id: layout
@ -26,8 +39,8 @@ Item {
}
onProceed: {
root.startupStore.setConnectionString(connectionString)
root.startupStore.doPrimaryAction()
nextStateDelay.connectionString = connectionString
nextStateDelay.start()
}
onDisplayInstructions: {

View File

@ -15,12 +15,15 @@ Rectangle {
id: root
property var keyPair
property bool hasPairedDevices
property var getNetworkShortNames: function(chainIds){}
property string userProfilePublicKey
property bool includeWatchOnlyAccount
signal goToAccountView(var account)
signal toggleIncludeWatchOnlyAccount()
signal runExportQrFlow()
signal runImportViaQrFlow()
signal runImportViaSeedPhraseFlow()
signal runImportViaPrivateKeyFlow()
signal runRenameKeypairFlow()
@ -82,6 +85,9 @@ Rectangle {
menuLoader.active = false
}
keyPair: root.keyPair
hasPairedDevices: root.hasPairedDevices
onRunExportQrFlow: root.runExportQrFlow()
onRunImportViaQrFlow: root.runImportViaQrFlow()
onRunImportViaSeedPhraseFlow: root.runImportViaSeedPhraseFlow()
onRunImportViaPrivateKeyFlow: root.runImportViaPrivateKeyFlow()
onRunRenameKeypairFlow: root.runRenameKeypairFlow()

View File

@ -196,8 +196,12 @@ StatusDialog {
id: displaySyncCodeView
anchors.fill: parent
visible: displaySyncCodeState.active
connectionStringLabel: qsTr("Sync code")
connectionString: d.connectionString
secondsTimeout: 5 * 60
importCodeInstructions: qsTr("On your other device, navigate to the Syncing<br>screen and select Enter Sync Code.")
codeExpiredMessage: qsTr("Your QR and Sync Code have expired.")
onRequestConnectionString: {
d.generateConnectionString()
}
@ -225,7 +229,8 @@ StatusDialog {
anchors.fill: parent
visible: errorState.active
primaryText: qsTr("Failed to generate sync code")
secondaryText: d.errorMessage
secondaryText: qsTr("Failed to start pairing server")
errorDetails: d.errorMessage
}
}

View File

@ -9,24 +9,26 @@ StatusMenu {
id: root
property var keyPair
property bool hasPairedDevices: false
signal runExportQrFlow()
signal runImportViaQrFlow()
signal runImportViaSeedPhraseFlow()
signal runImportViaPrivateKeyFlow()
signal runRenameKeypairFlow()
signal runRemoveKeypairFlow()
StatusAction {
text: enabled? qsTr("Show encrypted QR of keypairs on device") : ""
enabled: !!root.keyPair &&
text: enabled? qsTr("Show encrypted QR on device") : ""
enabled: root.hasPairedDevices &&
!!root.keyPair &&
root.keyPair.pairType !== Constants.keypair.type.profile &&
!root.keyPair.migratedToKeycard &&
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")
root.runExportQrFlow()
}
}
@ -46,15 +48,15 @@ StatusMenu {
StatusAction {
text: enabled? qsTr("Import keypair from device via encrypted QR") : ""
enabled: !!root.keyPair &&
enabled: root.hasPairedDevices &&
!!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
root.keyPair.operability === Constants.keypair.operability.nonOperable
icon.name: "qr-scan"
icon.color: Theme.palette.primaryColor1
onTriggered: {
console.warn("TODO: run import via encrypted QR")
root.runImportViaQrFlow()
}
}

View File

@ -33,7 +33,7 @@ QtObject {
}
property string userProfilePublicKey: userProfile.pubKey
function deleteAccount(address) {
return accountsModule.deleteAccount(address)
}
@ -67,8 +67,8 @@ QtObject {
walletSection.runAddAccountPopup(false)
}
function runKeypairImportPopup(keyUid, importOption) {
root.walletModule.runKeypairImportPopup(keyUid, importOption)
function runKeypairImportPopup(keyUid, mode) {
root.walletModule.runKeypairImportPopup(keyUid, mode)
}
function evaluateRpcEndPoint(url) {

View File

@ -291,6 +291,7 @@ ColumnLayout {
WalletKeypairAccountMenu {
id: keycardMenu
keyPair: root.keyPair
hasPairedDevices: root.walletStore.walletModule.hasPairedDevices
onRunRenameKeypairFlow: root.runRenameKeypairFlow()
onRunRemoveKeypairFlow: root.runRemoveKeypairFlow()
}

View File

@ -42,6 +42,40 @@ Column {
}
}
QtObject {
id: d
readonly property int unimportedNonProfileKeypairs: {
let total = 0
for (var i = 0; i < keypairsRepeater.count; i++) {
let kp = keypairsRepeater.itemAt(i).keyPair
if (kp.migratedToKeycard ||
kp.pairType === Constants.keypair.type.profile ||
kp.pairType === Constants.keypair.type.watchOnly ||
kp.operability === Constants.keypair.operability.fullyOperable ||
kp.operability === Constants.keypair.operability.partiallyOperable) {
continue
}
total++
}
return total
}
readonly property int allNonProfileKeypairsMigratedToAKeycard: {
for (var i = 0; i < keypairsRepeater.count; i++) {
let kp = keypairsRepeater.itemAt(i).keyPair
if (!kp.migratedToKeycard &&
kp.pairType !== Constants.keypair.type.profile &&
kp.pairType !== Constants.keypair.type.watchOnly &&
kp.operability !== Constants.keypair.operability.nonOperable) {
return false
}
}
return true
}
}
component Spacer: Item {
height: 8
}
@ -102,7 +136,8 @@ Column {
}
Rectangle {
visible: root.walletStore.walletModule.hasPairedDevices
visible: root.walletStore.walletModule.hasPairedDevices &&
!d.allNonProfileKeypairsMigratedToAKeycard
height: 102
width: parent.width
color: Theme.palette.transparent
@ -124,15 +159,15 @@ Column {
text: qsTr("Show encrypted QR of keypairs on device")
icon.name: "qr"
onClicked: {
console.warn("TODO: run generate qr code flow...")
root.walletStore.runKeypairImportPopup("", Constants.keypairImportPopup.mode.exportKeypairQr)
}
}
}
}
Rectangle {
id: importMissingKeypairs
visible: importMissingKeypairs.unimportedKeypairs > 0
visible: root.walletStore.walletModule.hasPairedDevices &&
d.unimportedNonProfileKeypairs > 0
height: 102
width: parent.width
color: Theme.palette.transparent
@ -140,27 +175,13 @@ Column {
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)
text: qsTr("%n keypair(s) require import to use on this device", "", d.unimportedNonProfileKeypairs)
font.pixelSize: 15
}
@ -169,7 +190,7 @@ Column {
type: StatusBaseButton.Type.Warning
icon.name: "download"
onClicked: {
root.walletStore.runKeypairImportPopup("", Constants.keypairImportPopup.importOption.selectKeypair)
root.walletStore.runKeypairImportPopup("", Constants.keypairImportPopup.mode.selectKeypair)
}
}
}
@ -189,6 +210,7 @@ Column {
delegate: WalletKeyPairDelegate {
width: parent.width
keyPair: model.keyPair
hasPairedDevices: root.walletStore.walletModule.hasPairedDevices
getNetworkShortNames: walletStore.getNetworkShortNames
userProfilePublicKey: walletStore.userProfilePublicKey
includeWatchOnlyAccount: walletStore.includeWatchOnlyAccount
@ -197,10 +219,16 @@ Column {
onRunRenameKeypairFlow: root.runRenameKeypairFlow(model)
onRunRemoveKeypairFlow: root.runRemoveKeypairFlow(model)
onRunImportViaSeedPhraseFlow: {
root.walletStore.runKeypairImportPopup(model.keyPair.keyUid, Constants.keypairImportPopup.importOption.seedPhrase)
root.walletStore.runKeypairImportPopup(model.keyPair.keyUid, Constants.keypairImportPopup.mode.importViaSeedPhrase)
}
onRunImportViaPrivateKeyFlow: {
root.walletStore.runKeypairImportPopup(model.keyPair.keyUid, Constants.keypairImportPopup.importOption.privateKey)
root.walletStore.runKeypairImportPopup(model.keyPair.keyUid, Constants.keypairImportPopup.mode.importViaPrivateKey)
}
onRunExportQrFlow: {
root.walletStore.runKeypairImportPopup(model.keyPair.keyUid, Constants.keypairImportPopup.mode.exportKeypairQr)
}
onRunImportViaQrFlow: {
root.walletStore.runKeypairImportPopup(model.keyPair.keyUid, Constants.keypairImportPopup.mode.importViaQr)
}
}
}

View File

@ -148,6 +148,29 @@ Item {
""
)
}
function onShowToastKeypairsImported(keypairName: string, keypairsCount: int, error: string) {
let notification = qsTr("Please re-generate QR code and try importing again")
if (error !== "") {
if (error.startsWith("one or more expected keystore files are not found among the sent files")) {
notification = qsTr("Make sure you're importing the exported keypair on paired device")
}
}
else {
notification = qsTr("%1 keypair successfully imported").arg(keypairName)
if (keypairsCount > 1) {
notification = qsTr("%n keypair(s) successfully imported", "", keypairsCount)
}
}
Global.displayToastMessage(
notification,
"",
error!==""? "info" : "checkmark-circle",
false,
error!==""? Constants.ephemeralNotificationType.normal : Constants.ephemeralNotificationType.success,
""
)
}
}
QtObject {

View File

@ -6,9 +6,13 @@ import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import shared.views 1.0
Column {
id: root
property int type: SyncingCodeInstructions.Type.AppSync
spacing: 4
QtObject {
@ -45,7 +49,6 @@ Column {
color: Theme.palette.directColor1
text: qsTr("Settings")
}
}
RowLayout {
@ -58,11 +61,21 @@ Column {
text: qsTr("3. Navigate to the ")
}
StatusRoundIcon {
asset.name: "rotate"
asset.name: {
if (root.type === SyncingCodeInstructions.Type.KeypairSync) {
return "wallet"
}
return "rotate"
}
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Syncing tab")
text: {
if (root.type === SyncingCodeInstructions.Type.KeypairSync) {
return qsTr("Wallet tab")
}
return qsTr("Syncing tab")
}
font.pixelSize: 15
color: Theme.palette.directColor1
}
@ -79,7 +92,12 @@ Column {
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Setup Syncing")
text: {
if (root.type === SyncingCodeInstructions.Type.KeypairSync) {
return qsTr("Import missing keypairs")
}
return qsTr("Setup Syncing")
}
font.pixelSize: 15
color: Theme.palette.directColor1
}
@ -90,7 +108,35 @@ Column {
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("5. Scan or enter the code ")
text: qsTr("5.")
font.pixelSize: 15
color: Theme.palette.baseColor1
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Enable camera")
font.pixelSize: 15
color: Theme.palette.directColor1
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("on this device")
font.pixelSize: 15
color: Theme.palette.baseColor1
}
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: {
if (root.type === SyncingCodeInstructions.Type.KeypairSync) {
return qsTr("6. Scan or enter the encrypted key with this device")
}
return qsTr("6. Scan or enter the code")
}
font.pixelSize: 15
color: Theme.palette.baseColor1
}

View File

@ -6,9 +6,13 @@ import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import shared.views 1.0
Column {
id: root
property int type: SyncingCodeInstructions.Type.AppSync
spacing: 4
QtObject {
@ -34,7 +38,7 @@ Column {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("2. Open your")
text: qsTr("2. Open")
}
StatusRoundIcon {
asset.name: "profile"
@ -43,9 +47,8 @@ Column {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: 15
color: Theme.palette.directColor1
text: qsTr("Profile")
text: qsTr("Settings")
}
}
RowLayout {
@ -58,11 +61,21 @@ Column {
text: qsTr("3. Go to")
}
StatusRoundIcon {
asset.name: "rotate"
asset.name: {
if (root.type === SyncingCodeInstructions.Type.KeypairSync) {
return "wallet"
}
return "rotate"
}
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Syncing")
text: {
if (root.type === SyncingCodeInstructions.Type.KeypairSync) {
return qsTr("Wallet")
}
return qsTr("Syncing")
}
font.pixelSize: 15
color: Theme.palette.directColor1
}
@ -79,7 +92,12 @@ Column {
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Sync new device")
text: {
if (root.type === SyncingCodeInstructions.Type.KeypairSync) {
return qsTr("Import missing keypairs")
}
return qsTr("Sync new device")
}
font.pixelSize: 15
color: Theme.palette.directColor1
}
@ -90,7 +108,35 @@ Column {
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("5. Scan or enter the code ")
text: qsTr("5.")
font.pixelSize: 15
color: Theme.palette.baseColor1
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Enable camera")
font.pixelSize: 15
color: Theme.palette.directColor1
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("on this device")
font.pixelSize: 15
color: Theme.palette.baseColor1
}
}
RowLayout {
height: d.listItemHeight
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: {
if (root.type === SyncingCodeInstructions.Type.KeypairSync) {
return qsTr("6. Scan or enter the encrypted key with this device")
}
return qsTr("6. Scan or enter the code")
}
font.pixelSize: 15
color: Theme.palette.baseColor1
}

View File

@ -14,17 +14,10 @@ StatusInput {
ReadMode
}
property int mode: StatusSyncCodeInput.Mode.WriteMode
required property int mode
property bool readOnly: false
QtObject {
id: d
readonly property bool writeMode: root.mode === StatusSyncCodeInput.Mode.WriteMode
}
label: d.writeMode ? qsTr("Paste sync code") : qsTr("Sync code")
input.edit.readOnly: root.readOnly
input.placeholderText: d.writeMode ? qsTr("eg. %1").arg("0x2Ef19") : ""
input.font: Theme.palette.monoFont.name
input.placeholderFont: root.input.font

View File

@ -19,6 +19,7 @@ StatusListItem {
property bool tagDisplayRemoveAccountButton: false
property bool canBeSelected: true
property bool displayRadioButtonForSelection: true
property bool useTransparentItemBackgroundColor: false
property int keyPairType: Constants.keycard.keyPairType.unknown
property string keyPairKeyUid: ""
@ -33,7 +34,17 @@ StatusListItem {
signal removeAccount(int index, string name)
signal accountClicked(int index)
color: root.keyPairCardLocked? Theme.palette.dangerColor3 : Theme.palette.baseColor2
color: {
if (!root.useTransparentItemBackgroundColor) {
return root.keyPairCardLocked? Theme.palette.dangerColor3 : Theme.palette.baseColor2
}
if (sensor.containsMouse) {
return Theme.palette.baseColor2
}
return Theme.palette.transparent
}
title: root.keyPairName
statusListItemTitleAside.textFormat: Text.RichText
statusListItemTitleAside.visible: !!statusListItemTitleAside.text

View File

@ -12,6 +12,7 @@ Item {
property ButtonGroup buttonGroup
property bool disableSelectionForKeypairsWithNonDefaultDerivationPath: true
property bool displayRadioButtonForSelection: true
property bool useTransparentItemBackgroundColor: false
property string optionLabel: ""
property alias modelFilters: proxyModel.filters
@ -36,6 +37,7 @@ Item {
canBeSelected: !root.disableSelectionForKeypairsWithNonDefaultDerivationPath ||
!model.keyPair.containsPathOutOfTheDefaultStatusDerivationTree()
displayRadioButtonForSelection: root.displayRadioButtonForSelection
useTransparentItemBackgroundColor: root.useTransparentItemBackgroundColor
keyPairType: model.keyPair.pairType
keyPairKeyUid: model.keyPair.keyUid

View File

@ -26,8 +26,12 @@ StatusModal {
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:
case Constants.keypairImportPopup.state.exportKeypair:
return qsTr("Encrypted QR for keypairs on this device")
case Constants.keypairImportPopup.state.importQr:
return qsTr("Scan encrypted QR")
case Constants.keypairImportPopup.state.displayInstructions:
return qsTr("How to generate the encrypted QR")
}
return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name)
@ -60,12 +64,16 @@ StatusModal {
return selectKeypairComponent
case Constants.keypairImportPopup.state.selectImportMethod:
return selectImportMethodComponent
case Constants.keypairImportPopup.state.scanQr:
return scanQrComponent
case Constants.keypairImportPopup.state.exportKeypair:
return exportKeypairComponent
case Constants.keypairImportPopup.state.importQr:
return keypairImportQrComponent
case Constants.keypairImportPopup.state.importPrivateKey:
return keypairImportPrivateKeyComponent
case Constants.keypairImportPopup.state.importSeedPhrase:
return keypairImportSeedPhraseComponent
case Constants.keypairImportPopup.state.displayInstructions:
return displayInstructionsComponent
}
return undefined
@ -93,8 +101,18 @@ StatusModal {
}
Component {
id: scanQrComponent
Item {
id: exportKeypairComponent
ExportKeypair {
height: Constants.keypairImportPopup.contentHeight
store: root.store
}
}
Component {
id: keypairImportQrComponent
ScanOrEnterQrCode {
height: Constants.keypairImportPopup.contentHeight
store: root.store
}
}
@ -113,6 +131,13 @@ StatusModal {
store: root.store
}
}
Component {
id: displayInstructionsComponent
DisplayInstructions {
height: Constants.keypairImportPopup.contentHeight
}
}
}
}
@ -135,8 +160,9 @@ StatusModal {
text: {
switch (root.store.currentState.stateType) {
case Constants.keypairImportPopup.state.scanQr:
case Constants.keypairImportPopup.state.exportKeypair:
return qsTr("Done")
case Constants.keypairImportPopup.state.importQr:
case Constants.keypairImportPopup.state.importPrivateKey:
case Constants.keypairImportPopup.state.importSeedPhrase:
return qsTr("Import %1 keypair").arg(root.store.selectedKeypair.name)
@ -148,6 +174,10 @@ StatusModal {
enabled: root.store.primaryPopupButtonEnabled
icon.name: {
if (root.store.currentState.stateType === Constants.keypairImportPopup.state.exportKeypair) {
return ""
}
if (root.store.userProfileUsingBiometricLogin) {
return "touch-id"
}

View File

@ -0,0 +1,19 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import shared.views 1.0
Item {
id: root
implicitHeight: layout.implicitHeight
SyncingCodeInstructions {
id: layout
anchors.fill: parent
anchors.margins: 24
type: SyncingCodeInstructions.Type.KeypairSync
}
}

View File

@ -0,0 +1,49 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import shared.views 1.0
import "../stores"
Item {
id: root
property KeypairImportStore store
implicitHeight: layout.implicitHeight
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: 16
SyncingDisplayCode {
Layout.fillWidth: true
Layout.margins: 16
visible: !!root.store.keypairImportModule.connectionString
connectionStringLabel: qsTr("Encrypted keypairs code")
connectionString: root.store.keypairImportModule.connectionString
importCodeInstructions: qsTr("On your other device, navigate to the Wallet screen<br>and select Import missing keypairs. For security reasons,<br>do not save this code anywhere.")
codeExpiredMessage: qsTr("Your QR and encrypted keypairs code have expired.")
onConnectionStringChanged: {
if (!!connectionString) {
start()
}
}
onRequestConnectionString: {
root.store.generateConnectionStringForExporting()
}
}
SyncingErrorMessage {
Layout.fillWidth: true
visible: !!root.store.keypairImportModule.connectionStringError
primaryText: qsTr("Failed to generate sync code")
secondaryText: qsTr("Failed to start pairing server")
errorDetails: root.store.keypairImportModule.connectionStringError
}
}
}

View File

@ -0,0 +1,47 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import shared.views 1.0
import "../stores"
Item {
id: root
property KeypairImportStore store
implicitHeight: layout.implicitHeight
SyncingEnterCode {
id: layout
anchors.fill: parent
anchors.margins: 24
spacing: 32
firstTabName: qsTr("Scan encrypted QR code")
secondTabName: qsTr("Enter encrypted key")
syncQrErrorMessage: qsTr("This does not look like the correct keypair QR code")
syncCodeErrorMessage: qsTr("This does not look like an encrypted keypair code")
firstInstructionButtonName: qsTr("How to generate the QR")
secondInstructionButtonName: qsTr("How to generate the key")
syncCodeLabel: qsTr("Paste encrypted key")
validateConnectionString: function(connectionString) {
const result = root.store.validateConnectionString(connectionString)
return result === ""
}
onProceed: {
root.store.keypairImportModule.connectionString = connectionString
if (!syncViaQr) {
return
}
root.store.submitPopup()
}
onDisplayInstructions: {
root.store.currentState.doSecondaryAction()
}
}
}

View File

@ -3,8 +3,11 @@ import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import shared.popups.keycard.helpers 1.0
import shared.status 1.0
import utils 1.0
@ -24,6 +27,12 @@ Item {
readonly property string fullyOperableValue: Constants.keypair.operability.fullyOperable
readonly property string partiallyOperableValue: Constants.keypair.operability.partiallyOperable
readonly property string profileTypeValue: Constants.keypair.type.profile
readonly property int margin: 16
function importKeypairs() {
root.store.currentState.doSecondaryAction()
}
}
ColumnLayout {
@ -34,21 +43,66 @@ Item {
StatusBaseText {
Layout.fillWidth: true
Layout.leftMargin: d.margin
Layout.rightMargin: d.margin
text: qsTr("To use the associated accounts on this device, you need to import their keypairs.")
font.pixelSize: Constants.keypairImportPopup.labelFontSize1
wrapMode: Text.WordWrap
}
StatusSectionHeadline {
Layout.fillWidth: true
Layout.leftMargin: d.margin
Layout.rightMargin: d.margin
text: qsTr("Import keypairs from your other device")
}
StatusListItem {
title: qsTr("Import via scanning encrypted QR")
asset {
width: 24
height: 24
name: "qr"
}
onClicked: {
d.importKeypairs()
}
components: [
StatusIcon {
icon: "next"
color: Theme.palette.baseColor1
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
d.importKeypairs()
}
}
}
]
}
StatusSectionHeadline {
Layout.fillWidth: true
Layout.leftMargin: d.margin
Layout.rightMargin: d.margin
text: qsTr("Import individual keys")
}
KeyPairList {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignLeft
optionLabel: qsTr("import")
disableSelectionForKeypairsWithNonDefaultDerivationPath: false
displayRadioButtonForSelection: false
useTransparentItemBackgroundColor: true
modelFilters: ExpressionFilter {
expression: model.keyPair.migratedToKeycard ||
model.keyPair.pairType == d.profileTypeValue ||
model.keyPair.operability == d.fullyOperableValue ||
model.keyPair.operability == d.partiallyOperableValue
inverted: true

View File

@ -50,6 +50,11 @@ BasePopupStore {
}
readonly property bool primaryPopupButtonEnabled: {
if (root.currentState.stateType === Constants.keypairImportPopup.state.importQr) {
return !!root.keypairImportModule.connectionString &&
!root.keypairImportModule.connectionStringError
}
if (root.currentState.stateType === Constants.keypairImportPopup.state.importPrivateKey) {
return root.enteredPrivateKeyIsValid &&
root.enteredPrivateKeyMatchTheKeypair &&
@ -65,4 +70,12 @@ BasePopupStore {
return true
}
function generateConnectionStringForExporting() {
root.keypairImportModule.generateConnectionStringForExporting()
}
function validateConnectionString(connectionString) {
return root.keypairImportModule.validateConnectionString(connectionString)
}
}

View File

@ -8,11 +8,13 @@ import shared.controls 1.0
ColumnLayout {
id: root
enum Source {
Mobile,
Desktop
enum Type {
AppSync,
KeypairSync
}
property int type: SyncingCodeInstructions.Type.AppSync
spacing: 0
StatusSwitchTabBar {
@ -47,6 +49,8 @@ ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: false
Layout.alignment: Qt.AlignHCenter
type: root.type
}
GetSyncCodeDesktopInstructions {
@ -54,6 +58,8 @@ ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: false
Layout.alignment: Qt.AlignHCenter
type: root.type
}
}
}

View File

@ -14,7 +14,10 @@ ColumnLayout {
id: root
property int secondsTimeout: 5 * 60
property string connectionStringLabel: ""
property string connectionString: ""
property string importCodeInstructions : ""
property string codeExpiredMessage: ""
signal requestConnectionString()
@ -156,27 +159,27 @@ ColumnLayout {
Layout.fillWidth: true
Layout.topMargin: 12
Layout.bottomMargin: 7
text: qsTr("Sync code")
text: root.connectionStringLabel
font.weight: Font.Medium
font.pixelSize: 13
color: Theme.palette.directColor1
}
Input {
StatusPasswordInput {
id: syncCodeInput
property bool showPassword
readonly property bool effectiveShowPassword: showPassword && !d.codeExpired
Layout.fillWidth: true
Layout.preferredHeight: 88
Layout.bottomMargin: 24
readOnly: true
keepHeight: true
textField.echoMode: effectiveShowPassword ? TextInput.Normal : TextInput.Password
textField.rightPadding: syncCodeButtons.width + Style.current.padding / 2
textField.color: Style.current.textColor
textField.selectByMouse: !d.codeExpired
selectByMouse: !d.codeExpired
text: root.connectionString
rightPadding: syncCodeButtons.width + Style.current.padding / 2
wrapMode: TextEdit.Wrap
echoMode: effectiveShowPassword ? TextInput.Normal : TextInput.Password
Row {
id: syncCodeButtons
@ -206,9 +209,9 @@ ColumnLayout {
onClicked: {
const showPassword = syncCodeInput.showPassword
syncCodeInput.showPassword = true
syncCodeInput.textField.selectAll()
syncCodeInput.textField.copy()
syncCodeInput.textField.deselect()
syncCodeInput.selectAll()
syncCodeInput.copy()
syncCodeInput.deselect()
syncCodeInput.showPassword = showPassword
}
}
@ -222,7 +225,7 @@ ColumnLayout {
visible: !d.codeExpired
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("On your other device, navigate to the Syncing<br>screen and select Enter Sync Code.")
text: root.importCodeInstructions
}
StatusBaseText {
@ -232,8 +235,6 @@ ColumnLayout {
visible: d.codeExpired
font.pixelSize: 15
color: Theme.palette.baseColor1
text: qsTr("Your QR and Sync Code has expired.")
text: root.codeExpiredMessage
}
}

View File

@ -11,31 +11,25 @@ ColumnLayout {
property string firstTabName: qsTr("Scan QR code")
property string secondTabName: qsTr("Enter sync code")
property string firstInstructionButtonName: qsTr("How to get a sync code")
property string secondInstructionButtonName: qsTr("How to get a sync code")
property string syncQrErrorMessage: qsTr("This does not look like a sync QR code")
property string syncCodeErrorMessage: qsTr("This does not look like a sync code")
property string instructionButtonName: qsTr("How to get a sync code")
property string syncCodeLabel: qsTr("Paste sync code")
property var validateConnectionString: function(){}
readonly property bool syncViaQr: !switchTabBar.currentIndex
signal displayInstructions()
signal proceed(string connectionString)
Timer {
id: nextStateDelay
property string connectionString
interval: 1000
repeat: false
onTriggered: {
root.proceed(connectionString)
}
}
StatusSwitchTabBar {
id: switchTabBar
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
currentIndex: 0
StatusSwitchTabButton {
@ -49,21 +43,17 @@ ColumnLayout {
StackLayout {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(mobileSync.implicitHeight, desktopSync.implicitHeight)
Layout.preferredHeight: Math.max(syncQr.implicitHeight, syncCode.implicitHeight)
currentIndex: switchTabBar.currentIndex
// StackLayout doesn't support alignment, so we create an `Item` wrappers
Item {
Layout.fillWidth: true
Layout.fillHeight: true
StatusSyncCodeScan {
id: mobileSync
id: syncQr
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
validators: [
StatusValidator {
@ -79,17 +69,15 @@ ColumnLayout {
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
StatusSyncCodeInput {
id: desktopSync
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
readOnly: nextStateDelay.running
id: syncCode
anchors.horizontalCenter: parent.horizontalCenter
width: 424
mode: StatusSyncCodeInput.Mode.WriteMode
label:root.syncCodeLabel
input.placeholderText: qsTr("eg. %1").arg("0x2Ef19")
validators: [
StatusValidator {
name: "isSyncCode"
@ -100,8 +88,7 @@ ColumnLayout {
input.onValidChanged: {
if (!input.valid)
return
nextStateDelay.connectionString = desktopSync.text
nextStateDelay.start()
root.proceed(syncCode.text)
}
}
}
@ -109,10 +96,18 @@ ColumnLayout {
StatusFlatButton {
Layout.alignment: Qt.AlignHCenter
visible: !!root.instructionButtonName
text: root.instructionButtonName
visible: switchTabBar.currentIndex == 0 && !!root.firstInstructionButtonName ||
switchTabBar.currentIndex == 1 && !!root.secondInstructionButtonName
text: switchTabBar.currentIndex == 0?
root.firstInstructionButtonName :
root.secondInstructionButtonName
onClicked: {
root.displayInstructions()
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}

View File

@ -13,40 +13,24 @@ ColumnLayout {
property string primaryText
property string secondaryText
property string errorDetails
spacing: 12
StatusBaseText {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: parent.height / 2
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
text: root.primaryText
font.pixelSize: 17
color: Theme.palette.dangerColor1
}
Item {
ErrorDetails {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 60
Layout.rightMargin: 60
Layout.preferredWidth: 360
Layout.preferredHeight: parent.height / 2
Layout.minimumHeight: detailsView.implicitHeight
ErrorDetails {
id: detailsView
anchors {
top: parent.top
left: parent.left
right: parent.right
}
title: qsTr("Failed to start pairing server")
details: root.secondaryText
}
Layout.preferredHeight: implicitHeight
title: root.secondaryText
details: root.errorDetails
}
}

View File

@ -744,20 +744,23 @@ QtObject {
readonly property int footerButtonsHeight: 44
readonly property int labelFontSize1: 15
readonly property QtObject importOption: QtObject {
readonly property QtObject mode: QtObject {
readonly property int selectKeypair: 1
readonly property int seedPhrase: 2
readonly property int privateKey: 3
readonly property int qrCode: 4
readonly property int importViaSeedPhrase: 2
readonly property int importViaPrivateKey: 3
readonly property int importViaQr: 4
readonly property int exportKeypairQr: 5
}
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 exportKeypair: "ExportKeypair"
readonly property string importQr: "ImportQr"
readonly property string importSeedPhrase: "ImportSeedPhrase"
readonly property string importPrivateKey: "ImportPrivateKey"
readonly property string displayInstructions: "DisplayInstructions"
}
}

View File

@ -853,7 +853,7 @@ QtObject {
}
}
if (keypair.syncedFrom !== "") {
return qsTr("Synced from %1").arg(keypair.syncedFrom)
return qsTr("Requires import to use")
}
}