diff --git a/src/app/modules/main/wallet_section/add_account/controller.nim b/src/app/modules/main/wallet_section/add_account/controller.nim
new file mode 100644
index 0000000000..6af8d49bb9
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/controller.nim
@@ -0,0 +1,229 @@
+import times, os, chronicles
+import uuids
+import io_interface
+
+import ../../../../../app_service/service/accounts/service as accounts_service
+import ../../../../../app_service/service/wallet_account/service as wallet_account_service
+import ../../../../../app_service/service/keycard/service as keycard_service
+
+import ../../../shared_modules/keycard_popup/io_interface as keycard_shared_module
+
+import ../../../../core/eventemitter
+
+logScope:
+ topics = "wallet-add-account-controller"
+
+const UNIQUE_WALLET_SECTION_ADD_ACCOUNTS_MODULE_IDENTIFIER* = "WalletSection-AddAccountsModule"
+
+type
+ Controller* = ref object of RootObj
+ delegate: io_interface.AccessInterface
+ events: EventEmitter
+ accountsService: accounts_service.Service
+ walletAccountService: wallet_account_service.Service
+ keycardService: keycard_service.Service
+ connectionIds: seq[UUID]
+ connectionKeycardResponse: UUID
+ tmpAuthenticatedKeyUid: string
+ tmpPin: string
+ tmpPassword: string
+ tmpSeedPhrase: string
+ tmpPaths: seq[string]
+ tmpGeneratedAccount: GeneratedAccountDto
+ uniqueFetchingDetailsId: string
+
+## Forward declaration
+proc disconnectKeycardReponseSignal(self: Controller)
+
+
+proc newController*(delegate: io_interface.AccessInterface,
+ events: EventEmitter,
+ accountsService: accounts_service.Service,
+ walletAccountService: wallet_account_service.Service,
+ keycardService: keycard_service.Service):
+ Controller =
+ result = Controller()
+ result.delegate = delegate
+ result.events = events
+ result.accountsService = accountsService
+ result.walletAccountService = walletAccountService
+ result.keycardService = keycardService
+
+proc disconnectAll*(self: Controller) =
+ self.disconnectKeycardReponseSignal()
+ for id in self.connectionIds:
+ self.events.disconnect(id)
+
+proc delete*(self: Controller) =
+ self.disconnectAll()
+
+proc init*(self: Controller) =
+ var handlerId = self.events.onWithUUID(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args):
+ let args = SharedKeycarModuleArgs(e)
+ if args.uniqueIdentifier != UNIQUE_WALLET_SECTION_ADD_ACCOUNTS_MODULE_IDENTIFIER:
+ return
+ self.delegate.onUserAuthenticated(args.pin, args.password, args.keyUid)
+ self.connectionIds.add(handlerId)
+
+ handlerId = self.events.onWithUUID(SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESSES_FETCHED) do(e:Args):
+ var args = DerivedAddressesArgs(e)
+ self.delegate.onDerivedAddressesFetched(args.derivedAddresses, args.error)
+ self.connectionIds.add(handlerId)
+
+ handlerId = self.events.onWithUUID(SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESSES_FROM_MNEMONIC_FETCHED) do(e:Args):
+ var args = DerivedAddressesArgs(e)
+ self.delegate.onDerivedAddressesFromMnemonicFetched(args.derivedAddresses, args.error)
+ self.connectionIds.add(handlerId)
+
+ handlerId = self.events.onWithUUID(SIGNAL_DERIVED_ADDRESSES_FROM_NOT_IMPORTED_MNEMONIC_FETCHED) do(e:Args):
+ var args = DerivedAddressesFromNotImportedMnemonicArgs(e)
+ self.delegate.onAddressesFromNotImportedMnemonicFetched(args.derivations, args.error)
+ self.connectionIds.add(handlerId)
+
+ 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 setAuthenticatedKeyUid*(self: Controller, value: string) =
+ self.tmpAuthenticatedKeyUid = value
+
+proc getAuthenticatedKeyUid*(self: Controller): string =
+ return self.tmpAuthenticatedKeyUid
+
+proc setPin*(self: Controller, value: string) =
+ self.tmpPin = value
+
+proc getPin*(self: Controller): string =
+ return self.tmpPin
+
+proc setPassword*(self: Controller, value: string) =
+ self.tmpPassword = value
+
+proc getPassword*(self: Controller): string =
+ return self.tmpPassword
+
+proc setSeedPhrase*(self: Controller, value: string) =
+ self.tmpSeedPhrase = value
+
+proc getSeedPhrase*(self: Controller): string =
+ return self.tmpSeedPhrase
+
+proc closeAddAccountPopup*(self: Controller) =
+ self.delegate.closeAddAccountPopup()
+
+proc getWalletAccounts*(self: Controller): seq[WalletAccountDto] =
+ return self.walletAccountService.fetchAccounts()
+
+proc getAllMigratedKeyPairs*(self: Controller): seq[KeyPairDto] =
+ return self.walletAccountService.getAllMigratedKeyPairs()
+
+proc addAccount*(self: Controller) =
+ self.delegate.addAccount()
+
+proc authenticateOrigin*(self: Controller, keyUid = "") =
+ let data = SharedKeycarModuleAuthenticationArgs(uniqueIdentifier: UNIQUE_WALLET_SECTION_ADD_ACCOUNTS_MODULE_IDENTIFIER,
+ keyUid: keyUid)
+ self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data)
+
+method fetchDerivedAddresses*(self: Controller, derivedFrom: string, paths: seq[string])=
+ var hashPassword = true
+ if self.getPin().len > 0:
+ hashPassword = false
+ self.walletAccountService.fetchDerivedAddresses(self.getPassword(), derivedFrom, paths, hashPassword)
+
+proc getRandomMnemonic*(self: Controller): string =
+ return self.walletAccountService.getRandomMnemonic()
+
+proc fetchDetailsForAddresses*(self: Controller, addresses: seq[string]) =
+ self.uniqueFetchingDetailsId = $now().toTime().toUnix()
+ self.walletAccountService.fetchDetailsForAddresses(self.uniqueFetchingDetailsId, addresses)
+
+proc addWalletAccount*(self: Controller, createKeystoreFile, doPasswordHashing: bool, name, keyPairName, address, path: string,
+ lastUsedDerivationIndex: int, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji: string): bool =
+ var password: string
+ if createKeystoreFile:
+ password = self.getPassword()
+ if password.len == 0:
+ info "cannot create keystore file if provided password is empty", name=name, address=address
+ return false
+ let err = self.walletAccountService.addWalletAccount(password, doPasswordHashing, name, keyPairName, address, path,
+ lastUsedDerivationIndex, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji)
+ if err.len > 0:
+ info "adding wallet account failed", name=name, address=address
+ return false
+ return true
+
+proc addNewPrivateKeyAccount*(self: Controller, privateKey: string, doPasswordHashing: bool, name, keyPairName,
+ address, path: string, lastUsedDerivationIndex: int, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji: string): bool =
+ let password = self.getPassword()
+ if password.len == 0:
+ info "cannot create keystore file if provided password is empty", name=name, address=address
+ return false
+ let err = self.walletAccountService.addNewPrivateKeyAccount(privateKey, password, doPasswordHashing, name, keyPairName, address, path,
+ lastUsedDerivationIndex, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji)
+ if err.len > 0:
+ info "adding new wallet account from private key failed", name=name, address=address
+ return false
+ return true
+
+proc addNewSeedPhraseAccount*(self: Controller, seedPhrase: string, doPasswordHashing: bool, name, keyPairName,
+ address, path: string, lastUsedDerivationIndex: int, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji: string): bool =
+ let password = self.getPassword()
+ if password.len == 0:
+ info "cannot create keystore file if provided password is empty", name=name, address=address
+ return false
+ let err = self.walletAccountService.addNewSeedPhraseAccount(seedPhrase, password, doPasswordHashing, name, keyPairName, address, path,
+ lastUsedDerivationIndex, rootWalletMasterKey, publicKey, keyUid, accountType, color, emoji)
+ if err.len > 0:
+ info "adding new wallet account from seed phrase failed", name=name, address=address
+ return false
+ return true
+
+proc getKeyUidForSeedPhrase*(self: Controller, seedPhrase: string): string =
+ let acc = self.accountsService.createAccountFromMnemonic(seedPhrase)
+ return acc.keyUid
+
+proc createAccountFromSeedPhrase*(self: Controller, seedPhrase: string): GeneratedAccountDto =
+ self.setSeedPhrase(seedPhrase)
+ self.tmpGeneratedAccount = self.accountsService.createAccountFromMnemonic(seedPhrase)
+ return self.tmpGeneratedAccount
+
+proc fetchAddressesFromNotImportedSeedPhrase*(self: Controller, seedPhrase: string, paths: seq[string] = @[]) =
+ self.accountsService.fetchAddressesFromNotImportedMnemonic(seedPhrase, paths)
+
+proc createAccountFromPrivateKey*(self: Controller, privateKey: string): GeneratedAccountDto =
+ self.tmpGeneratedAccount = self.accountsService.createAccountFromPrivateKey(privateKey)
+ return self.tmpGeneratedAccount
+
+proc getGeneratedAccount*(self: Controller): GeneratedAccountDto =
+ return self.tmpGeneratedAccount
+
+proc buildNewPrivateKeyKeypairAndAddItToOrigin*(self: Controller) =
+ self.delegate.buildNewPrivateKeyKeypairAndAddItToOrigin()
+
+proc buildNewSeedPhraseKeypairAndAddItToOrigin*(self: Controller) =
+ self.delegate.buildNewSeedPhraseKeypairAndAddItToOrigin()
+
+proc disconnectKeycardReponseSignal(self: Controller) =
+ self.events.disconnect(self.connectionKeycardResponse)
+
+proc connectKeycardReponseSignal(self: Controller) =
+ self.connectionKeycardResponse = self.events.onWithUUID(SIGNAL_KEYCARD_RESPONSE) do(e: Args):
+ let args = KeycardArgs(e)
+ self.disconnectKeycardReponseSignal()
+ self.delegate.onDerivedAddressesFromKeycardFetched(args.flowType, args.flowEvent, self.tmpPaths)
+
+proc cancelCurrentFlow*(self: Controller) =
+ self.keycardService.cancelCurrentFlow()
+ # in most cases we're running another flow after canceling the current one,
+ # this way we're giving to the keycard some time to cancel the current flow
+ sleep(200)
+
+proc fetchAddressesFromKeycard*(self: Controller, bip44Paths: seq[string]) =
+ self.cancelCurrentFlow()
+ self.connectKeycardReponseSignal()
+ self.tmpPaths = bip44Paths
+ self.keycardService.startExportPublicFlow(bip44Paths, exportMasterAddr=true, exportPrivateAddr=false, pin=self.getPin())
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/derived_address_item.nim b/src/app/modules/main/wallet_section/add_account/derived_address_item.nim
new file mode 100644
index 0000000000..37765a443c
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/derived_address_item.nim
@@ -0,0 +1,114 @@
+import NimQml, strformat
+
+QtObject:
+ type DerivedAddressItem* = ref object of QObject
+ order: int
+ address: string
+ path: string
+ hasActivity: bool
+ alreadyCreated: bool
+ loaded: bool
+
+ proc delete*(self: DerivedAddressItem) =
+ self.QObject.delete
+
+ proc newDerivedAddressItem*(
+ order: int = 0,
+ address: string = "",
+ path: string = "",
+ alreadyCreated: bool = false,
+ hasActivity: bool = false,
+ loaded: bool = false
+ ): DerivedAddressItem =
+ new(result, delete)
+ result.QObject.setup
+ result.order = order
+ result.address = address
+ result.path = path
+ result.alreadyCreated = alreadyCreated
+ result.hasActivity = hasActivity
+ result.loaded = loaded
+
+ proc `$`*(self: DerivedAddressItem): string =
+ result = fmt"""DerivedAddressItem(
+ order: {self.order},
+ address: {self.address},
+ path: {self.path},
+ alreadyCreated: {self.alreadyCreated},
+ hasActivity: {self.hasActivity},
+ loaded: {self.loaded}
+ ]"""
+
+ proc orderChanged*(self: DerivedAddressItem) {.signal.}
+ proc getOrder*(self: DerivedAddressItem): int {.slot.} =
+ return self.order
+ proc setOrder*(self: DerivedAddressItem, value: int) {.slot.} =
+ self.order = value
+ self.orderChanged()
+ QtProperty[int] order:
+ read = getOrder
+ write = setOrder
+ notify = orderChanged
+
+ proc addressChanged*(self: DerivedAddressItem) {.signal.}
+ proc getAddress*(self: DerivedAddressItem): string {.slot.} =
+ return self.address
+ proc setAddress*(self: DerivedAddressItem, value: string) {.slot.} =
+ self.address = value
+ self.addressChanged()
+ QtProperty[string] address:
+ read = getAddress
+ write = setAddress
+ notify = addressChanged
+
+ proc pathChanged*(self: DerivedAddressItem) {.signal.}
+ proc getPath*(self: DerivedAddressItem): string {.slot.} =
+ return self.path
+ proc setPath*(self: DerivedAddressItem, value: string) {.slot.} =
+ self.path = value
+ self.pathChanged()
+ QtProperty[string] path:
+ read = getPath
+ write = setPath
+ notify = pathChanged
+
+ proc alreadyCreatedChanged*(self: DerivedAddressItem) {.signal.}
+ proc getAlreadyCreated*(self: DerivedAddressItem): bool {.slot.} =
+ return self.alreadyCreated
+ proc setAlreadyCreated*(self: DerivedAddressItem, value: bool) {.slot.} =
+ self.alreadyCreated = value
+ self.alreadyCreatedChanged()
+ QtProperty[bool] alreadyCreated:
+ read = getAlreadyCreated
+ write = setAlreadyCreated
+ notify = alreadyCreatedChanged
+
+ proc hasActivityChanged*(self: DerivedAddressItem) {.signal.}
+ proc getHasActivity*(self: DerivedAddressItem): bool {.slot.} =
+ return self.hasActivity
+ proc setHasActivity*(self: DerivedAddressItem, value: bool) {.slot.} =
+ self.hasActivity = value
+ self.hasActivityChanged()
+ QtProperty[bool] hasActivity:
+ read = getHasActivity
+ write = setHasActivity
+ notify = hasActivityChanged
+
+ proc loadedChanged*(self: DerivedAddressItem) {.signal.}
+ proc getLoaded*(self: DerivedAddressItem): bool {.slot.} =
+ return self.loaded
+ proc setLoaded*(self: DerivedAddressItem, value: bool) {.slot.} =
+ self.loaded = value
+ self.loadedChanged()
+ QtProperty[bool] loaded:
+ read = getLoaded
+ write = setLoaded
+ notify = loadedChanged
+
+ proc setItem*(self: DerivedAddressItem, item: DerivedAddressItem) =
+ self.setOrder(item.getOrder())
+ self.setAddress(item.getAddress())
+ self.setPath(item.getPath())
+ self.setAlreadyCreated(item.getAlreadyCreated())
+ self.setHasActivity(item.getHasActivity())
+ self.setLoaded(item.getLoaded())
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/derived_address_model.nim b/src/app/modules/main/wallet_section/add_account/derived_address_model.nim
new file mode 100644
index 0000000000..d5eb6f64a4
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/derived_address_model.nim
@@ -0,0 +1,121 @@
+import NimQml, Tables, strutils, sequtils, sugar, strformat
+
+import ./derived_address_item
+
+export derived_address_item
+
+type
+ ModelRole {.pure.} = enum
+ AddressDetails = UserRole + 1
+
+QtObject:
+ type
+ DerivedAddressModel* = ref object of QAbstractListModel
+ items: seq[DerivedAddressItem]
+
+ proc delete(self: DerivedAddressModel) =
+ self.items = @[]
+ self.QAbstractListModel.delete
+
+ proc setup(self: DerivedAddressModel) =
+ self.QAbstractListModel.setup
+
+ proc newDerivedAddressModel*(): DerivedAddressModel =
+ new(result, delete)
+ result.setup
+
+ proc `$`*(self: DerivedAddressModel): string =
+ for i in 0 ..< self.items.len:
+ result &= fmt"""[{i}]:({$self.items[i]})"""
+
+ proc countChanged(self: DerivedAddressModel) {.signal.}
+ proc getCount(self: DerivedAddressModel): int {.slot.} =
+ self.items.len
+ QtProperty[int] count:
+ read = getCount
+ notify = countChanged
+
+ proc loadedCountChanged(self: DerivedAddressModel) {.signal.}
+ proc getLoadedCount(self: DerivedAddressModel): int {.slot.} =
+ return self.items.filter(x => x.getLoaded()).len
+ QtProperty[int] loadedCount:
+ read = getLoadedCount
+ notify = loadedCountChanged
+
+ method rowCount(self: DerivedAddressModel, index: QModelIndex = nil): int =
+ return self.items.len
+
+ method roleNames(self: DerivedAddressModel): Table[int, string] =
+ {
+ ModelRole.AddressDetails.int: "addressDetails"
+ }.toTable
+
+ method data(self: DerivedAddressModel, index: QModelIndex, role: int): QVariant =
+ if (not index.isValid):
+ return
+
+ if (index.row < 0 or index.row >= self.items.len):
+ return
+
+ let item = self.items[index.row]
+ let enumRole = role.ModelRole
+
+ case enumRole:
+ of ModelRole.AddressDetails:
+ result = newQVariant(item)
+
+ proc reset*(self: DerivedAddressModel) =
+ self.beginResetModel()
+ self.items = @[]
+ self.endResetModel()
+ self.countChanged()
+ self.loadedCountChanged()
+
+ proc setItems*(self: DerivedAddressModel, items: seq[DerivedAddressItem]) =
+ self.beginResetModel()
+ self.items = items
+ self.endResetModel()
+ self.countChanged()
+ self.loadedCountChanged()
+
+ proc getItemByAddress*(self: DerivedAddressModel, address: string): DerivedAddressItem =
+ for it in self.items:
+ if it.getAddress() == address:
+ return it
+ return nil
+
+ proc updateDetailsForAddressAndBubbleItToTop*(self: DerivedAddressModel, address: string, hasActivity: bool) =
+ var item: DerivedAddressItem
+ for i in 0 ..< self.items.len:
+ if cmpIgnoreCase(self.items[i].getAddress(), address) == 0:
+ item = self.items[i]
+
+ let parentModelIndex = newQModelIndex()
+ defer: parentModelIndex.delete
+ self.beginRemoveRows(parentModelIndex, i, i)
+ self.items.delete(i)
+ self.endRemoveRows()
+ break
+
+ if item.isNil:
+ return
+
+ var indexToInsertTo = 0
+ for i in 0 ..< self.items.len:
+ if not self.items[i].getLoaded():
+ break
+ indexToInsertTo.inc
+
+ let parentModelIndex = newQModelIndex()
+ defer: parentModelIndex.delete
+ self.beginInsertRows(parentModelIndex, indexToInsertTo, indexToInsertTo)
+ self.items.insert(
+ newDerivedAddressItem(item.getOrder(), item.getAddress(), item.getPath(), item.getAlreadyCreated(), hasActivity, loaded = true),
+ indexToInsertTo
+ )
+ self.endInsertRows()
+ self.countChanged() # we need this to trigger bindings on the qml side
+ self.loadedCountChanged()
+
+ proc getAllAddresses*(self: DerivedAddressModel): seq[string] =
+ return self.items.map(x => x.getAddress())
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/confirm_adding_new_master_key_state.nim b/src/app/modules/main/wallet_section/add_account/internal/confirm_adding_new_master_key_state.nim
new file mode 100644
index 0000000000..bc88e73297
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/confirm_adding_new_master_key_state.nim
@@ -0,0 +1,21 @@
+type
+ ConfirmAddingNewMasterKeyState* = ref object of State
+
+proc newConfirmAddingNewMasterKeyState*(backState: State): ConfirmAddingNewMasterKeyState =
+ result = ConfirmAddingNewMasterKeyState()
+ result.setup(StateType.ConfirmAddingNewMasterKey, backState)
+
+proc delete*(self: ConfirmAddingNewMasterKeyState) =
+ self.State.delete
+
+method executePrePrimaryStateCommand*(self: ConfirmAddingNewMasterKeyState, controller: Controller) =
+ let seedPhrase = controller.getRandomMnemonic()
+ let genAccDto = controller.createAccountFromSeedPhrase(seedPhrase)
+ if genAccDto.address.len == 0:
+ # should never be here
+ error "unable to create an account from the provided seed phrase"
+ controller.closeAddAccountPopup()
+ return
+
+method getNextPrimaryState*(self: ConfirmAddingNewMasterKeyState, controller: Controller): State =
+ return createState(StateType.DisplaySeedPhrase, self)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/confirm_seed_phrase_backup_state.nim b/src/app/modules/main/wallet_section/add_account/internal/confirm_seed_phrase_backup_state.nim
new file mode 100644
index 0000000000..1ff9bcf193
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/confirm_seed_phrase_backup_state.nim
@@ -0,0 +1,12 @@
+type
+ ConfirmSeedPhraseBackupState* = ref object of State
+
+proc newConfirmSeedPhraseBackupState*(backState: State): ConfirmSeedPhraseBackupState =
+ result = ConfirmSeedPhraseBackupState()
+ result.setup(StateType.ConfirmSeedPhraseBackup, backState)
+
+proc delete*(self: ConfirmSeedPhraseBackupState) =
+ self.State.delete
+
+method getNextPrimaryState*(self: ConfirmSeedPhraseBackupState, controller: Controller): State =
+ return createState(StateType.EnterKeypairName, self)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/display_seed_phrase_state.nim b/src/app/modules/main/wallet_section/add_account/internal/display_seed_phrase_state.nim
new file mode 100644
index 0000000000..cc9c4eaf7f
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/display_seed_phrase_state.nim
@@ -0,0 +1,12 @@
+type
+ DisplaySeedPhraseState* = ref object of State
+
+proc newDisplaySeedPhraseState*(backState: State): DisplaySeedPhraseState =
+ result = DisplaySeedPhraseState()
+ result.setup(StateType.DisplaySeedPhrase, backState)
+
+proc delete*(self: DisplaySeedPhraseState) =
+ self.State.delete
+
+method getNextPrimaryState*(self: DisplaySeedPhraseState, controller: Controller): State =
+ return createState(StateType.EnterSeedPhraseWord1, self)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/enter_keypair_name_state.nim b/src/app/modules/main/wallet_section/add_account/internal/enter_keypair_name_state.nim
new file mode 100644
index 0000000000..5e91dbff40
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/enter_keypair_name_state.nim
@@ -0,0 +1,15 @@
+type
+ EnterKeypairNameState* = ref object of State
+
+proc newEnterKeypairNameState*(backState: State): EnterKeypairNameState =
+ result = EnterKeypairNameState()
+ result.setup(StateType.EnterKeypairName, backState)
+
+proc delete*(self: EnterKeypairNameState) =
+ self.State.delete
+
+method executePrePrimaryStateCommand*(self: EnterKeypairNameState, controller: Controller) =
+ controller.buildNewSeedPhraseKeypairAndAddItToOrigin()
+
+method getNextPrimaryState*(self: EnterKeypairNameState, controller: Controller): State =
+ return createState(StateType.Main, nil)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/enter_private_key_state.nim b/src/app/modules/main/wallet_section/add_account/internal/enter_private_key_state.nim
new file mode 100644
index 0000000000..832de2dc31
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/enter_private_key_state.nim
@@ -0,0 +1,15 @@
+type
+ EnterPrivateKeyState* = ref object of State
+
+proc newEnterPrivateKeyState*(backState: State): EnterPrivateKeyState =
+ result = EnterPrivateKeyState()
+ result.setup(StateType.EnterPrivateKey, backState)
+
+proc delete*(self: EnterPrivateKeyState) =
+ self.State.delete
+
+method executePrePrimaryStateCommand*(self: EnterPrivateKeyState, controller: Controller) =
+ controller.buildNewPrivateKeyKeypairAndAddItToOrigin()
+
+method getNextPrimaryState*(self: EnterPrivateKeyState, controller: Controller): State =
+ return createState(StateType.Main, nil)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/enter_seed_phrase_state.nim b/src/app/modules/main/wallet_section/add_account/internal/enter_seed_phrase_state.nim
new file mode 100644
index 0000000000..32b89c12bb
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/enter_seed_phrase_state.nim
@@ -0,0 +1,15 @@
+type
+ EnterSeedPhraseState* = ref object of State
+
+proc newEnterSeedPhraseState*(backState: State): EnterSeedPhraseState =
+ result = EnterSeedPhraseState()
+ result.setup(StateType.EnterSeedPhrase, backState)
+
+proc delete*(self: EnterSeedPhraseState) =
+ self.State.delete
+
+method executePrePrimaryStateCommand*(self: EnterSeedPhraseState, controller: Controller) =
+ controller.buildNewSeedPhraseKeypairAndAddItToOrigin()
+
+method getNextPrimaryState*(self: EnterSeedPhraseState, controller: Controller): State =
+ return createState(StateType.Main, nil)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/enter_seed_phrase_word_1_state.nim b/src/app/modules/main/wallet_section/add_account/internal/enter_seed_phrase_word_1_state.nim
new file mode 100644
index 0000000000..95dbbbf0f9
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/enter_seed_phrase_word_1_state.nim
@@ -0,0 +1,12 @@
+type
+ EnterSeedPhraseWord1State* = ref object of State
+
+proc newEnterSeedPhraseWord1State*(backState: State): EnterSeedPhraseWord1State =
+ result = EnterSeedPhraseWord1State()
+ result.setup(StateType.EnterSeedPhraseWord1, backState)
+
+proc delete*(self: EnterSeedPhraseWord1State) =
+ self.State.delete
+
+method getNextPrimaryState*(self: EnterSeedPhraseWord1State, controller: Controller): State =
+ return createState(StateType.EnterSeedPhraseWord2, self)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/enter_seed_phrase_word_2_state.nim b/src/app/modules/main/wallet_section/add_account/internal/enter_seed_phrase_word_2_state.nim
new file mode 100644
index 0000000000..3bb6dca4fc
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/enter_seed_phrase_word_2_state.nim
@@ -0,0 +1,12 @@
+type
+ EnterSeedPhraseWord2State* = ref object of State
+
+proc newEnterSeedPhraseWord2State*(backState: State): EnterSeedPhraseWord2State =
+ result = EnterSeedPhraseWord2State()
+ result.setup(StateType.EnterSeedPhraseWord2, backState)
+
+proc delete*(self: EnterSeedPhraseWord2State) =
+ self.State.delete
+
+method getNextPrimaryState*(self: EnterSeedPhraseWord2State, controller: Controller): State =
+ return createState(StateType.ConfirmSeedPhraseBackup, self)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/main_state.nim b/src/app/modules/main/wallet_section/add_account/internal/main_state.nim
new file mode 100644
index 0000000000..21791169b0
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/main_state.nim
@@ -0,0 +1,15 @@
+type
+ MainState* = ref object of State
+
+proc newMainState*(backState: State): MainState =
+ result = MainState()
+ result.setup(StateType.Main, backState)
+
+proc delete*(self: MainState) =
+ self.State.delete
+
+method executePrePrimaryStateCommand*(self: MainState, controller: Controller) =
+ controller.addAccount()
+
+method getNextSecondaryState*(self: MainState, controller: Controller): State =
+ return createState(StateType.SelectMasterKey, self)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/select_master_key_sate.nim b/src/app/modules/main/wallet_section/add_account/internal/select_master_key_sate.nim
new file mode 100644
index 0000000000..649448b36f
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/select_master_key_sate.nim
@@ -0,0 +1,18 @@
+type
+ SelectMasterKeyState* = ref object of State
+
+proc newSelectMasterKeyState*(backState: State): SelectMasterKeyState =
+ result = SelectMasterKeyState()
+ result.setup(StateType.SelectMasterKey, backState)
+
+proc delete*(self: SelectMasterKeyState) =
+ self.State.delete
+
+method getNextPrimaryState*(self: SelectMasterKeyState, controller: Controller): State =
+ return createState(StateType.EnterSeedPhrase, self)
+
+method getNextSecondaryState*(self: SelectMasterKeyState, controller: Controller): State =
+ return createState(StateType.EnterPrivateKey, self)
+
+method getNextTertiaryState*(self: SelectMasterKeyState, controller: Controller): State =
+ return createState(StateType.ConfirmAddingNewMasterKey, self)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/state.nim b/src/app/modules/main/wallet_section/add_account/internal/state.nim
new file mode 100644
index 0000000000..d5fce56c12
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/state.nim
@@ -0,0 +1,88 @@
+import ../controller
+
+type StateType* {.pure.} = enum
+ NoState = "NoState"
+ Main = "Main"
+ SelectMasterKey = "SelectMasterKey"
+ EnterSeedPhrase = "EnterSeedPhrase"
+ EnterSeedPhraseWord1 = "EnterSeedPhraseWord1"
+ EnterSeedPhraseWord2 = "EnterSeedPhraseWord2"
+ EnterPrivateKey = "EnterPrivateKey"
+ EnterKeypairName = "EnterKeypairName"
+ DisplaySeedPhrase = "DisplaySeedPhrase"
+ ConfirmAddingNewMasterKey = "ConfirmAddingNewMasterKey"
+ ConfirmSeedPhraseBackup = "ConfirmSeedPhraseBackup"
+
+
+## This is the base class for all states
+## We should not instance of this class (in c++ this will be an abstract class).
+type
+ State* {.pure inheritable.} = ref object of RootObj
+ stateType: StateType
+ backState: State
+
+proc setup*(self: State, stateType: StateType, backState: State) =
+ self.stateType = stateType
+ self.backState = backState
+
+## `stateType` - detemines the state this instance describes
+## `backState` - the sate (instance) we're moving to if user clicks "back" button,
+## in case we should not display "back" button for this state, set it to `nil`
+proc newState*(self: State, stateType: StateType, backState: State): State =
+ result = State()
+ result.setup(stateType, backState)
+
+proc delete*(self: State) =
+ discard
+
+## Returns state type
+method stateType*(self: State): StateType {.inline base.} =
+ self.stateType
+
+## Returns back state instance
+method getBackState*(self: State): State {.inline base.} =
+ self.backState
+
+## Returns true if we should display "back" button, otherwise false
+method displayBackButton*(self: State): bool {.inline base.} =
+ return not self.backState.isNil
+
+## Returns next state instance if "primary" action is triggered
+method getNextPrimaryState*(self: State, controller: Controller): State {.inline base.} =
+ return nil
+
+## Returns next state instance if "secondary" action is triggered
+method getNextSecondaryState*(self: State, controller: Controller): State {.inline base.} =
+ return nil
+
+## Returns next state instance in case the "tertiary" action is triggered
+method getNextTertiaryState*(self: State, controller: Controller): State {.inline base.} =
+ return nil
+
+## Returns next state instance in case the "quaternary" action is triggered
+method getNextQuaternaryState*(self: State, controller: Controller): State {.inline base.} =
+ return nil
+
+## This method is executed if "cancel" action is triggered (invalidates current flow)
+method executeCancelCommand*(self: State, controller: Controller) {.inline base.} =
+ controller.closeAddAccountPopup()
+
+## This method is executed before back state is set, if "back" action is triggered
+method executePreBackStateCommand*(self: State, controller: Controller) {.inline base.} =
+ discard
+
+## This method is executed before primary state is set, if "primary" action is triggered
+method executePrePrimaryStateCommand*(self: State, controller: Controller) {.inline base.} =
+ discard
+
+## This method is executed before secondary state is set, if "secondary" action is triggered
+method executePreSecondaryStateCommand*(self: State, controller: Controller) {.inline base.} =
+ discard
+
+## This method is executed in case "tertiary" action is triggered
+method executePreTertiaryStateCommand*(self: State, controller: Controller) {.inline base.} =
+ discard
+
+## This method is executed in case "quaternary" action is triggered
+method executePreQuaternaryStateCommand*(self: State, controller: Controller) {.inline base.} =
+ discard
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/internal/state_factory.nim b/src/app/modules/main/wallet_section/add_account/internal/state_factory.nim
new file mode 100644
index 0000000000..40d70e6a3c
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/state_factory.nim
@@ -0,0 +1,45 @@
+import chronicles
+import ../controller
+
+import state
+
+logScope:
+ topics = "add-account-module-state-factory"
+
+# Forward declaration
+proc createState*(stateToBeCreated: StateType, backState: State): State
+
+include confirm_adding_new_master_key_state
+include confirm_seed_phrase_backup_state
+include display_seed_phrase_state
+include enter_keypair_name_state
+include enter_private_key_state
+include enter_seed_phrase_state
+include enter_seed_phrase_word_1_state
+include enter_seed_phrase_word_2_state
+include main_state
+include select_master_key_sate
+
+proc createState*(stateToBeCreated: StateType, backState: State): State =
+ if stateToBeCreated == StateType.ConfirmAddingNewMasterKey:
+ return newConfirmAddingNewMasterKeyState(backState)
+ if stateToBeCreated == StateType.ConfirmSeedPhraseBackup:
+ return newConfirmSeedPhraseBackupState(backState)
+ if stateToBeCreated == StateType.DisplaySeedPhrase:
+ return newDisplaySeedPhraseState(backState)
+ if stateToBeCreated == StateType.EnterKeypairName:
+ return newEnterKeypairNameState(backState)
+ if stateToBeCreated == StateType.EnterPrivateKey:
+ return newEnterPrivateKeyState(backState)
+ if stateToBeCreated == StateType.EnterSeedPhrase:
+ return newEnterSeedPhraseState(backState)
+ if stateToBeCreated == StateType.EnterSeedPhraseWord1:
+ return newEnterSeedPhraseWord1State(backState)
+ if stateToBeCreated == StateType.EnterSeedPhraseWord2:
+ return newEnterSeedPhraseWord2State(backState)
+ if stateToBeCreated == StateType.Main:
+ return newMainState(backState)
+ if stateToBeCreated == StateType.SelectMasterKey:
+ return newSelectMasterKeyState(backState)
+
+ error "Add account - no implementation available for state", state=stateToBeCreated
diff --git a/src/app/modules/main/wallet_section/add_account/internal/state_wrapper.nim b/src/app/modules/main/wallet_section/add_account/internal/state_wrapper.nim
new file mode 100644
index 0000000000..c526c3096f
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/internal/state_wrapper.nim
@@ -0,0 +1,62 @@
+import NimQml
+import state
+
+QtObject:
+ type StateWrapper* = ref object of QObject
+ stateObj: State
+
+ proc delete*(self: StateWrapper) =
+ self.QObject.delete
+
+ proc newStateWrapper*(): StateWrapper =
+ new(result, delete)
+ result.QObject.setup()
+
+ proc stateWrapperChanged*(self:StateWrapper) {.signal.}
+
+ proc setStateObj*(self: StateWrapper, stateObj: State) =
+ self.stateObj = stateObj
+ self.stateWrapperChanged()
+
+ proc getStateObj*(self: StateWrapper): State =
+ return self.stateObj
+
+ proc getStateType(self: StateWrapper): string {.slot.} =
+ if(self.stateObj.isNil):
+ return $StateType.NoState
+ return $self.stateObj.stateType()
+ QtProperty[string] stateType:
+ read = getStateType
+ notify = stateWrapperChanged
+
+ proc getDisplayBackButton(self: StateWrapper): bool {.slot.} =
+ if(self.stateObj.isNil):
+ return false
+ return self.stateObj.displayBackButton()
+ QtProperty[bool] displayBackButton:
+ read = getDisplayBackButton
+ notify = stateWrapperChanged
+
+ proc backActionClicked*(self: StateWrapper) {.signal.}
+ proc doBackAction*(self: StateWrapper) {.slot.} =
+ self.backActionClicked()
+
+ proc cancelActionClicked*(self: StateWrapper) {.signal.}
+ proc doCancelAction*(self: StateWrapper) {.slot.} =
+ self.cancelActionClicked()
+
+ proc primaryActionClicked*(self: StateWrapper) {.signal.}
+ proc doPrimaryAction*(self: StateWrapper) {.slot.} =
+ self.primaryActionClicked()
+
+ proc secondaryActionClicked*(self: StateWrapper) {.signal.}
+ proc doSecondaryAction*(self: StateWrapper) {.slot.} =
+ self.secondaryActionClicked()
+
+ proc tertiaryActionClicked*(self: StateWrapper) {.signal.}
+ proc doTertiaryAction*(self: StateWrapper) {.slot.} =
+ self.tertiaryActionClicked()
+
+ proc quaternaryActionClicked*(self: StateWrapper) {.signal.}
+ proc doQuaternaryAction*(self: StateWrapper) {.slot.} =
+ self.quaternaryActionClicked()
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/io_interface.nim b/src/app/modules/main/wallet_section/add_account/io_interface.nim
new file mode 100644
index 0000000000..56a5d7798c
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/io_interface.nim
@@ -0,0 +1,105 @@
+import Tables, NimQml
+
+import ../../../../../app_service/service/accounts/dto/generated_accounts
+import ../../../../../app_service/service/wallet_account/derived_address
+from ../../../../../app_service/service/keycard/service import KeycardEvent
+
+type
+ AccessInterface* {.pure inheritable.} = ref object of RootObj
+
+method delete*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method load*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method closeAddAccountPopup*(self: AccessInterface, switchToAccWithAddress: string = "") {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method getSeedPhrase*(self: AccessInterface): string {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onBackActionClicked*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onPrimaryActionClicked*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onSecondaryActionClicked*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onTertiaryActionClicked*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onQuaternaryActionClicked*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onCancelActionClicked*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method addAccount*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method authenticateForEditingDerivationPath*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onUserAuthenticated*(self: AccessInterface, pin: string, password: string, keyUid: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method changeSelectedOrigin*(self: AccessInterface, keyUid: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method changeDerivationPath*(self: AccessInterface, path: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method changeSelectedDerivedAddress*(self: AccessInterface, address: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method changeWatchOnlyAccountAddress*(self: AccessInterface, address: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method changePrivateKey*(self: AccessInterface, privateKey: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method changeSeedPhrase*(self: AccessInterface, seedPhrase: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method validSeedPhrase*(self: AccessInterface, seedPhrase: string): bool {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method resetDerivationPath*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onDerivedAddressesFetched*(self: AccessInterface, derivedAddresses: seq[DerivedAddressDto], error: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onDerivedAddressesFromMnemonicFetched*(self: AccessInterface, derivedAddresses: seq[DerivedAddressDto], error: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onAddressesFromNotImportedMnemonicFetched*(self: AccessInterface, derivations: Table[string, DerivedAccountDetails], error: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onAddressDetailsFetched*(self: AccessInterface, derivedAddresses: seq[DerivedAddressDto], error: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method startScanningForActivity*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onDerivedAddressesFromKeycardFetched*(self: AccessInterface, keycardFlowType: string, keycardEvent: KeycardEvent,
+ paths: seq[string]) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method buildNewPrivateKeyKeypairAndAddItToOrigin*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method buildNewSeedPhraseKeypairAndAddItToOrigin*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+
+type
+ DelegateInterface* = concept c
+ c.onAddAccountModuleLoaded()
+ c.destroyAddAccountPopup(string)
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/add_account/module.nim b/src/app/modules/main/wallet_section/add_account/module.nim
new file mode 100644
index 0000000000..cf8053c6be
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/module.nim
@@ -0,0 +1,636 @@
+import NimQml, Tables, strutils, sequtils, sugar, chronicles
+
+import io_interface
+import view, controller, derived_address_model
+import internal/[state, state_factory]
+
+import ../../../../core/eventemitter
+
+import ../../../../global/global_singleton
+
+import ../../../shared/keypairs
+import ../../../shared_models/[keypair_model]
+import ../../../shared_modules/keycard_popup/module as keycard_shared_module
+
+import ../../../../../app_service/common/account_constants
+import ../../../../../app_service/service/accounts/service as accounts_service
+import ../../../../../app_service/service/wallet_account/service as wallet_account_service
+import ../../../../../app_service/service/keycard/service as keycard_service
+
+export io_interface
+
+const Label_NewWatchOnlyAccount = "LABEL-NEW-WATCH-ONLY-ACCOUNT"
+const Label_Existing = "LABEL-EXISTING"
+const Label_ImportNew = "LABEL-IMPORT-NEW"
+const Label_OptionAddNewMasterKey = "LABEL-OPTION-ADD-NEW-MASTER-KEY"
+const Label_OptionAddWatchOnlyAcc = "LABEL-OPTION-ADD-WATCH-ONLY-ACC"
+
+const MaxNumOfGeneratedAddresses = 100
+const NumOfGeneratedAddressesRegular = MaxNumOfGeneratedAddresses
+const NumOfGeneratedAddressesKeycard = 10
+
+
+logScope:
+ topics = "wallet-add-account-module"
+
+type
+ AuthenticationReason {.pure.} = enum
+ AddingAccount = 0
+ EditingDerivationPath
+
+type
+ Module*[T: io_interface.DelegateInterface] = ref object of io_interface.AccessInterface
+ delegate: T
+ events: EventEmitter
+ view: View
+ viewVariant: QVariant
+ controller: Controller
+ accountsService: accounts_service.Service
+ walletAccountService: wallet_account_service.Service
+ authenticationReason: AuthenticationReason
+
+## Forward declaration
+proc doAddingAccount[T](self: Module[T])
+
+proc newModule*[T](delegate: T,
+ events: EventEmitter,
+ keycardService: keycard_service.Service,
+ accountsService: accounts_service.Service,
+ walletAccountService: wallet_account_service.Service):
+ Module[T] =
+ result = Module[T]()
+ result.delegate = delegate
+ result.events = events
+ result.walletAccountService = walletAccountService
+ result.view = newView(result)
+ result.viewVariant = newQVariant(result.view)
+ result.controller = controller.newController(result, events, accountsService, walletAccountService, keycardService)
+ result.authenticationReason = AuthenticationReason.AddingAccount
+
+method delete*[T](self: Module[T]) =
+ self.view.delete
+ self.viewVariant.delete
+ self.controller.delete
+
+method load*[T](self: Module[T]) =
+ self.controller.init()
+ self.view.setCurrentState(newMainState(nil))
+
+ var items = keypairs.buildKeyPairsList(self.controller.getWalletAccounts(), self.controller.getAllMigratedKeyPairs(),
+ excludeAlreadyMigratedPairs = false)
+ if items.len == 0:
+ error "list of identified keypairs is empty, but it must have at least a profile keypair"
+ return
+ if items.len > 1:
+ var item = newKeyPairItem(keyUid = Label_Existing)
+ items.insert(item, 1)
+ var item = newKeyPairItem(keyUid = Label_ImportNew)
+ items.add(item)
+ item = newKeyPairItem(keyUid = Label_OptionAddNewMasterKey)
+ item.setIcon("objects")
+ items.add(item)
+ item = newKeyPairItem(keyUid = Label_OptionAddWatchOnlyAcc)
+ item.setName(Label_NewWatchOnlyAccount)
+ item.setIcon("objects")
+ items.add(item)
+
+ self.view.setDisablePopup(false)
+ self.view.setOriginModelItems(items)
+ self.changeSelectedOrigin(items[0].getKeyUid())
+ self.delegate.onAddAccountModuleLoaded()
+
+proc tryKeycardSync[T](self: Module[T]) =
+ if self.controller.getPin().len == 0:
+ return
+ let dataForKeycardToSync = SharedKeycarModuleArgs(
+ pin: self.controller.getPin(),
+ keyUid: self.controller.getAuthenticatedKeyUid()
+ )
+ self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_TRY_KEYCARD_SYNC, dataForKeycardToSync)
+
+method closeAddAccountPopup*[T](self: Module[T], switchToAccWithAddress: string = "") =
+ self.tryKeycardSync()
+ self.delegate.destroyAddAccountPopup(switchToAccWithAddress)
+
+method getModuleAsVariant*[T](self: Module[T]): QVariant =
+ return self.viewVariant
+
+method getSeedPhrase*[T](self: Module[T]): string =
+ return self.controller.getSeedPhrase()
+
+method onBackActionClicked*[T](self: Module[T]) =
+ let currStateObj = self.view.currentStateObj()
+ if currStateObj.isNil:
+ error "waa_cannot resolve current state"
+ return
+ debug "waa_back_action", currState=currStateObj.stateType()
+ currStateObj.executePreBackStateCommand(self.controller)
+ let backState = currStateObj.getBackState()
+ if backState.isNil:
+ return
+ self.view.setCurrentState(backState)
+ debug "waa_back_action - set state", newCurrState=backState.stateType()
+
+method onCancelActionClicked*[T](self: Module[T]) =
+ let currStateObj = self.view.currentStateObj()
+ if currStateObj.isNil:
+ error "waa_cannot resolve current state"
+ return
+ debug "waa_cancel_action", currState=currStateObj.stateType()
+ currStateObj.executeCancelCommand(self.controller)
+
+method onPrimaryActionClicked*[T](self: Module[T]) =
+ let currStateObj = self.view.currentStateObj()
+ if currStateObj.isNil:
+ error "waa_cannot resolve current state"
+ return
+ debug "waa_primary_action", currState=currStateObj.stateType()
+ currStateObj.executePrePrimaryStateCommand(self.controller)
+ let nextState = currStateObj.getNextPrimaryState(self.controller)
+ if nextState.isNil:
+ return
+ self.view.setCurrentState(nextState)
+ debug "waa_primary_action - set state", setCurrState=nextState.stateType()
+
+method onSecondaryActionClicked*[T](self: Module[T]) =
+ let currStateObj = self.view.currentStateObj()
+ if currStateObj.isNil:
+ error "waa_cannot resolve current state"
+ return
+ debug "waa_secondary_action", currState=currStateObj.stateType()
+ currStateObj.executePreSecondaryStateCommand(self.controller)
+ let nextState = currStateObj.getNextSecondaryState(self.controller)
+ if nextState.isNil:
+ return
+ self.view.setCurrentState(nextState)
+ debug "waa_secondary_action - set state", setCurrState=nextState.stateType()
+
+method onTertiaryActionClicked*[T](self: Module[T]) =
+ let currStateObj = self.view.currentStateObj()
+ if currStateObj.isNil:
+ error "waa_cannot resolve current state"
+ return
+ debug "waa_tertiary_action", currState=currStateObj.stateType()
+ currStateObj.executePreTertiaryStateCommand(self.controller)
+ let nextState = currStateObj.getNextTertiaryState(self.controller)
+ if nextState.isNil:
+ return
+ self.view.setCurrentState(nextState)
+ debug "waa_tertiarry_action - set state", setCurrState=nextState.stateType()
+
+method onQuaternaryActionClicked*[T](self: Module[T]) =
+ let currStateObj = self.view.currentStateObj()
+ if currStateObj.isNil:
+ error "waa_cannot resolve current state"
+ return
+ debug "waa_quaternary_action", currState=currStateObj.stateType()
+ currStateObj.executePreQuaternaryStateCommand(self.controller)
+ let nextState = currStateObj.getNextQuaternaryState(self.controller)
+ if nextState.isNil:
+ return
+ self.view.setCurrentState(nextState)
+ debug "waa_quaternary_action - set state", setCurrState=nextState.stateType()
+
+proc isKeyPairAlreadyAdded[T](self: Module[T], keyUid: string): bool =
+ return self.controller.getWalletAccounts().filter(x => x.keyUid == keyUid).len > 0
+
+proc getNumOfAddressesToGenerate[T](self: Module[T]): int =
+ let selectedOrigin = self.view.getSelectedOrigin()
+ if selectedOrigin.getMigratedToKeycard():
+ let walletAccounts = self.controller.getWalletAccounts()
+ let numOfAlreadyCreated = walletAccounts.filter(x => x.keyUid == selectedOrigin.getKeyUid()).len
+ let final = NumOfGeneratedAddressesKeycard + numOfAlreadyCreated # In case of a Keycard keypair always offer 10 available addresses
+ if final < MaxNumOfGeneratedAddresses:
+ return final
+ return MaxNumOfGeneratedAddresses
+ return NumOfGeneratedAddressesRegular
+
+proc fetchAddressForDerivationPath[T](self: Module[T]) =
+ let derivationPath = self.view.getDerivationPath()
+ let derivedAddress = self.view.getSelectedDerivedAddress()
+ if derivationPath == derivedAddress.getPath() and derivedAddress.getAddress().len > 0:
+ return
+ self.view.setScanningForActivityIsOngoing(false)
+ self.view.derivedAddressModel().reset()
+ self.view.setSelectedDerivedAddress(newDerivedAddressItem())
+
+ let selectedOrigin = self.view.getSelectedOrigin()
+ var paths: seq[string]
+ if derivationPath.endsWith("/"):
+ for i in 0 ..< self.getNumOfAddressesToGenerate():
+ let path = derivationPath & $i
+ # exclude paths for which user already has created accounts
+ if selectedOrigin.containsAccountPath(path):
+ continue
+ paths.add(path)
+ else:
+ paths.add(derivationPath)
+
+ if selectedOrigin.getPairType() == KeyPairType.Profile.int or
+ selectedOrigin.getPairType() == KeyPairType.SeedImport.int:
+ if not self.isKeyPairAlreadyAdded(selectedOrigin.getKeyUid()):
+ self.controller.fetchAddressesFromNotImportedSeedPhrase(self.controller.getSeedPhrase(), paths)
+ return
+ if selectedOrigin.getMigratedToKeycard():
+ self.controller.fetchAddressesFromKeycard(paths)
+ return
+ self.controller.fetchDerivedAddresses(selectedOrigin.getDerivedFrom(), paths)
+ return
+ error "derivation is not supported for other than profile and seed imported keypairs for origin"
+
+proc authenticateSelectedOrigin[T](self: Module[T], reason: AuthenticationReason) =
+ let selectedOrigin = self.view.getSelectedOrigin()
+ self.authenticationReason = reason
+ if selectedOrigin.getMigratedToKeycard():
+ self.controller.authenticateOrigin(selectedOrigin.getKeyUid())
+ return
+ self.controller.authenticateOrigin()
+
+method onUserAuthenticated*[T](self: Module[T], pin: string, password: string, keyUid: string) =
+ if password.len == 0:
+ info "unsuccessful authentication"
+ return
+ self.controller.setPin(pin)
+ self.controller.setPassword(password)
+ self.controller.setAuthenticatedKeyUid(keyUid)
+ if self.authenticationReason == AuthenticationReason.AddingAccount:
+ self.view.setDisablePopup(true)
+ let selectedOrigin = self.view.getSelectedOrigin()
+ if selectedOrigin.getPairType() == KeyPairType.PrivateKeyImport.int:
+ self.doAddingAccount() # we're sure that we need to add an account from priv key from here, cause derivation is not possible for imported priv key
+ return
+ self.fetchAddressForDerivationPath()
+ return
+ if self.authenticationReason == AuthenticationReason.EditingDerivationPath:
+ self.view.setActionAuthenticated(true)
+ self.fetchAddressForDerivationPath()
+
+proc isAuthenticationNeededForSelectedOrigin[T](self: Module[T]): bool =
+ let selectedOrigin = self.view.getSelectedOrigin()
+ if selectedOrigin.isNil:
+ error "selected origin is not set"
+ return true
+ if selectedOrigin.getPairType() == KeyPairType.Unknown.int and
+ selectedOrigin.getKeyUid() == Label_OptionAddWatchOnlyAcc:
+ return false
+ if selectedOrigin.getKeyUid() == self.controller.getAuthenticatedKeyUid():
+ return false
+ if not selectedOrigin.getMigratedToKeycard() and self.controller.getAuthenticatedKeyUid() == singletonInstance.userProfile.getKeyUid():
+ return false
+ return true
+
+method changeDerivationPath*[T](self: Module[T], derivationPath: string) =
+ self.view.setDerivationPath(derivationPath)
+ if self.isAuthenticationNeededForSelectedOrigin():
+ return
+ self.fetchAddressForDerivationPath()
+
+proc resolveSuggestedPathForSelectedOrigin[T](self: Module[T]): tuple[suggestedPath: string, usedIndex: int] =
+ let selectedOrigin = self.view.getSelectedOrigin()
+ var nextIndex = 0
+ if self.isKeyPairAlreadyAdded(selectedOrigin.getKeyUid()):
+ nextIndex = selectedOrigin.getLastUsedDerivationIndex() + 1
+ var suggestedPath = account_constants.PATH_WALLET_ROOT & "/" & $nextIndex
+ let walletAccounts = self.controller.getWalletAccounts()
+ if walletAccounts.filter(x => x.keyUid == selectedOrigin.getKeyUid() and x.path == suggestedPath).len == 0:
+ return (suggestedPath, nextIndex)
+
+ nextIndex.inc
+ for i in nextIndex ..< self.getNumOfAddressesToGenerate():
+ suggestedPath = account_constants.PATH_WALLET_ROOT & "/" & $i
+ if walletAccounts.filter(x => x.keyUid == selectedOrigin.getKeyUid() and x.path == suggestedPath).len == 0:
+ return (suggestedPath, i)
+ error "we couldn't find available path for new account"
+
+method resetDerivationPath*[T](self: Module[T]) =
+ let selectedOrigin = self.view.getSelectedOrigin()
+ let (suggestedPath, _) = self.resolveSuggestedPathForSelectedOrigin()
+ self.changeDerivationPath(suggestedPath)
+
+proc setItemForSelectedOrigin[T](self: Module[T], item: KeyPairItem) =
+ if item.isNil:
+ error "provided item cannot be set as selected origin", keyUid=item.getKeyUid()
+ return
+
+ self.view.setSelectedOrigin(item)
+
+ if item.getKeyUid() == Label_OptionAddWatchOnlyAcc:
+ self.view.setWatchOnlyAccAddress(newDerivedAddressItem())
+ return
+
+ let (suggestedPath, _) = self.resolveSuggestedPathForSelectedOrigin()
+ self.view.setSuggestedDerivationPath(suggestedPath)
+ self.view.setDerivationPath(suggestedPath)
+
+ if self.isAuthenticationNeededForSelectedOrigin():
+ self.controller.setAuthenticatedKeyUid("")
+ self.controller.setPin("")
+ self.controller.setPassword("")
+ self.view.setActionAuthenticated(false)
+ else:
+ self.fetchAddressForDerivationPath()
+
+method changeSelectedOrigin*[T](self: Module[T], keyUid: string) =
+ let item = self.view.originModel().findItemByKeyUid(keyUid)
+ self.setItemForSelectedOrigin(item)
+
+method changeSelectedDerivedAddress*[T](self: Module[T], address: string) =
+ let item = self.view.derivedAddressModel().getItemByAddress(address)
+ if item.isNil:
+ error "cannot resolve derived address item by provided address", address=address
+ return
+ self.view.setSelectedDerivedAddress(item)
+ self.view.setDerivationPath(item.getPath())
+ self.view.setScanningForActivityIsOngoing(true)
+ self.controller.fetchDetailsForAddresses(@[address])
+
+method changeWatchOnlyAccountAddress*[T](self: Module[T], address: string) =
+ self.view.setScanningForActivityIsOngoing(false)
+ self.view.setWatchOnlyAccAddress(newDerivedAddressItem(order = 0, address = address))
+ if address.len == 0:
+ return
+ self.view.setScanningForActivityIsOngoing(true)
+ self.view.setWatchOnlyAccAddress(newDerivedAddressItem(order = 0, address = address))
+ self.controller.fetchDetailsForAddresses(@[address])
+
+method changePrivateKey*[T](self: Module[T], privateKey: string) =
+ self.view.setScanningForActivityIsOngoing(false)
+ self.view.setPrivateKeyAccAddress(newDerivedAddressItem())
+ if privateKey.len == 0:
+ return
+ let genAccDto = self.controller.createAccountFromPrivateKey(privateKey)
+ if genAccDto.address.len == 0:
+ error "unable to resolve an address from the provided private key"
+ return
+ self.view.setScanningForActivityIsOngoing(true)
+ self.view.setPrivateKeyAccAddress(newDerivedAddressItem(order = 0, address = genAccDto.address))
+ self.controller.fetchDetailsForAddresses(@[genAccDto.address])
+
+method changeSeedPhrase*[T](self: Module[T], seedPhrase: string) =
+ let genAccDto = self.controller.createAccountFromSeedPhrase(seedPhrase)
+ self.view.setSelectedDerivedAddress(newDerivedAddressItem())
+ if genAccDto.address.len == 0:
+ error "unable to create an account from the provided seed phrase"
+ return
+
+method validSeedPhrase*[T](self: Module[T], seedPhrase: string): bool =
+ let keyUid = self.controller.getKeyUidForSeedPhrase(seedPhrase)
+ return not self.isKeyPairAlreadyAdded(keyUid)
+
+proc setDerivedAddresses[T](self: Module[T], derivedAddresses: seq[DerivedAddressDto]) =
+ var items: seq[DerivedAddressItem]
+ let derivationPath = self.view.getDerivationPath()
+ if derivationPath.endsWith("/"):
+ for i in 0 ..< self.getNumOfAddressesToGenerate():
+ let path = derivationPath & $i
+ for d in derivedAddresses:
+ if d.path == path:
+ items.add(newDerivedAddressItem(order = i, address = d.address, path = d.path, alreadyCreated = d.alreadyCreated))
+ break
+ self.view.derivedAddressModel().setItems(items)
+ else:
+ for d in derivedAddresses:
+ if d.path == derivationPath:
+ items.add(newDerivedAddressItem(order = 0, address = d.address, path = d.path, alreadyCreated = d.alreadyCreated))
+ break
+ self.view.derivedAddressModel().setItems(items)
+ self.changeSelectedDerivedAddress(items[0].getAddress())
+
+method onDerivedAddressesFetched*[T](self: Module[T], derivedAddresses: seq[DerivedAddressDto], error: string) =
+ if error.len > 0:
+ error "fetching derived addresses error", err=error
+ return
+ let selectedOrigin = self.view.getSelectedOrigin()
+ if selectedOrigin.getPairType() != KeyPairType.Profile.int and
+ selectedOrigin.getPairType() != KeyPairType.SeedImport.int:
+ error "received derived addresses reffer to profile or seed imported origin, but that's not the selected origin"
+ return
+ self.setDerivedAddresses(derivedAddresses)
+ if self.authenticationReason == AuthenticationReason.AddingAccount:
+ self.doAddingAccount()
+
+method onAddressesFromNotImportedMnemonicFetched*[T](self: Module[T], derivations: Table[string, DerivedAccountDetails], error: string) =
+ if error.len > 0:
+ error "fetching derived addresses from not imported mnemonic error", err=error
+ return
+ let selectedOrigin = self.view.getSelectedOrigin()
+ if selectedOrigin.getPairType() != KeyPairType.SeedImport.int:
+ error "received derived addresses from not imported mnemonic reffer to seed imported origin, but that's not the selected origin"
+ return
+ var derivedAddresses: seq[DerivedAddressDto]
+ for path, data in derivations.pairs:
+ derivedAddresses.add(DerivedAddressDto(
+ address: data.address,
+ path: path,
+ hasActivity: false,
+ alreadyCreated: false)
+ )
+ self.setDerivedAddresses(derivedAddresses)
+ if self.authenticationReason == AuthenticationReason.AddingAccount:
+ self.doAddingAccount()
+
+method onDerivedAddressesFromKeycardFetched*[T](self: Module[T], keycardFlowType: string, keycardEvent: KeycardEvent,
+ paths: seq[string]) =
+ let selectedOrigin = self.view.getSelectedOrigin()
+ if not selectedOrigin.getMigratedToKeycard():
+ error "receiving addresses from a keycard refers to a keycard origin, but selected origin is not a keycard origin"
+ return
+ if paths.len != keycardEvent.generatedWalletAccounts.len:
+ error "unexpected error, keycard didn't generate all addresses we need"
+ return
+ var derivedAddresses: seq[DerivedAddressDto]
+ for i in 0 ..< paths.len:
+ # we're safe to access `generatedWalletAccounts` by index (read comment in `startExportPublicFlow`)
+ derivedAddresses.add(DerivedAddressDto(address: keycardEvent.generatedWalletAccounts[i].address,
+ path: paths[i], hasActivity: false, alreadyCreated: false))
+
+ if selectedOrigin.getPairType() != KeyPairType.Profile.int and
+ selectedOrigin.getPairType() != KeyPairType.SeedImport.int:
+ error "received derived addresses reffer to profile or seed imported origin, but that's not the selected origin"
+ return
+ self.setDerivedAddresses(derivedAddresses)
+ if self.authenticationReason == AuthenticationReason.AddingAccount:
+ self.doAddingAccount()
+
+method onAddressDetailsFetched*[T](self: Module[T], derivedAddresses: seq[DerivedAddressDto], error: string) =
+ if not self.view.getScanningForActivityIsOngoing():
+ return
+ if error.len > 0:
+ error "fetching address details error", err=error
+ return
+ let currStateObj = self.view.currentStateObj()
+ if currStateObj.isNil:
+ error "waa_cannot resolve current state"
+ return
+
+ # we always receive responses one by one
+ if derivedAddresses.len == 1:
+ var addressDetailsItem = newDerivedAddressItem(order = 0,
+ address = derivedAddresses[0].address,
+ path = derivedAddresses[0].path,
+ alreadyCreated = derivedAddresses[0].alreadyCreated,
+ hasActivity = derivedAddresses[0].hasActivity,
+ loaded = true)
+ if currStateObj.stateType() == StateType.EnterPrivateKey:
+ if cmpIgnoreCase(self.view.getPrivateKeyAccAddress().getAddress(), addressDetailsItem.getAddress()) == 0:
+ self.view.setPrivateKeyAccAddress(addressDetailsItem)
+ return
+ elif currStateObj.stateType() == StateType.Main:
+ let selectedOrigin = self.view.getSelectedOrigin()
+ if selectedOrigin.getPairType() == KeyPairType.Unknown.int and
+ selectedOrigin.getKeyUid() == Label_OptionAddWatchOnlyAcc and
+ cmpIgnoreCase(self.view.getWatchOnlyAccAddress().getAddress(), addressDetailsItem.getAddress()) == 0:
+ self.view.setWatchOnlyAccAddress(addressDetailsItem)
+ return
+ let selectedAddress = self.view.getSelectedDerivedAddress()
+ if cmpIgnoreCase(selectedAddress.getAddress(), addressDetailsItem.getAddress()) == 0:
+ addressDetailsItem.setPath(selectedAddress.getPath())
+ self.view.setSelectedDerivedAddress(addressDetailsItem)
+ self.view.derivedAddressModel().updateDetailsForAddressAndBubbleItToTop(addressDetailsItem.getAddress(), addressDetailsItem.getHasActivity())
+ return
+ error "derived addresses received in the state in which the app doesn't expect them"
+ return
+ error "unknown error, since the length of the response is not expected", length=derivedAddresses.len
+
+method startScanningForActivity*[T](self: Module[T]) =
+ self.view.setScanningForActivityIsOngoing(true)
+ let allAddresses = self.view.derivedAddressModel().getAllAddresses()
+ self.controller.fetchDetailsForAddresses(allAddresses)
+
+method authenticateForEditingDerivationPath*[T](self: Module[T]) =
+ self.authenticateSelectedOrigin(AuthenticationReason.EditingDerivationPath)
+
+proc doAddingAccount[T](self: Module[T]) =
+ self.view.setDisablePopup(true)
+ let
+ selectedOrigin = self.view.getSelectedOrigin()
+ selectedAddrItem = self.view.getSelectedDerivedAddress()
+ (suggestedPath, usedIndex) = self.resolveSuggestedPathForSelectedOrigin()
+ var
+ addingNewKeyPair = false
+ accountType: string
+ keyPairName = selectedOrigin.getName()
+ address = selectedAddrItem.getAddress()
+ path = selectedAddrItem.getPath()
+ lastUsedDerivationIndex = if selectedAddrItem.getPath() == suggestedPath: usedIndex else: selectedOrigin.getLastUsedDerivationIndex()
+ rootWalletMasterKey = selectedOrigin.getDerivedFrom()
+ publicKey = selectedOrigin.getPubKey()
+ keyUid = selectedOrigin.getKeyUid()
+ createKeystoreFile = not selectedOrigin.getMigratedToKeycard()
+ doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser()
+
+ if selectedOrigin.getPairType() == KeyPairType.Profile.int:
+ accountType = account_constants.GENERATED
+ elif selectedOrigin.getPairType() == KeyPairType.SeedImport.int:
+ accountType = account_constants.SEED
+ addingNewKeyPair = not self.isKeyPairAlreadyAdded(keyUid)
+ elif selectedOrigin.getPairType() == KeyPairType.PrivateKeyImport.int:
+ let genAcc = self.controller.getGeneratedAccount()
+ accountType = account_constants.KEY
+ address = genAcc.address
+ path = "m" # from private key an address for path `m` is generate (corresponds to the master key address)
+ lastUsedDerivationIndex = 0
+ rootWalletMasterKey = ""
+ publicKey = genAcc.publicKey
+ keyUid = genAcc.keyUid
+ addingNewKeyPair = not self.isKeyPairAlreadyAdded(keyUid)
+ else:
+ accountType = account_constants.WATCH
+ createKeystoreFile = false
+ doPasswordHashing = false
+ keyPairName = ""
+ address = self.view.getWatchOnlyAccAddress().getAddress()
+ path = ""
+ lastUsedDerivationIndex = 0
+ rootWalletMasterKey = ""
+ publicKey = ""
+ keyUid = ""
+
+ var success = false
+ if addingNewKeyPair:
+ if selectedOrigin.getPairType() == KeyPairType.PrivateKeyImport.int:
+ success = self.controller.addNewPrivateKeyAccount(
+ privateKey = self.controller.getGeneratedAccount().privateKey,
+ doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser(),
+ name = self.view.getAccountName(),
+ keyPairName = keyPairName,
+ address = address,
+ path = path,
+ lastUsedDerivationIndex = lastUsedDerivationIndex,
+ rootWalletMasterKey = rootWalletMasterKey,
+ publicKey = publicKey,
+ keyUid = keyUid,
+ accountType = accountType,
+ color = self.view.getSelectedColor(),
+ emoji = self.view.getSelectedEmoji())
+ if not success:
+ error "failed to store new private key account", address=selectedAddrItem.getAddress()
+ elif selectedOrigin.getPairType() == KeyPairType.SeedImport.int:
+ success = self.controller.addNewSeedPhraseAccount(
+ seedPhrase = self.controller.getSeedPhrase(),
+ doPasswordHashing = not singletonInstance.userProfile.getIsKeycardUser(),
+ name = self.view.getAccountName(),
+ keyPairName = keyPairName,
+ address = address,
+ path = path,
+ lastUsedDerivationIndex = lastUsedDerivationIndex,
+ rootWalletMasterKey = rootWalletMasterKey,
+ publicKey = publicKey,
+ keyUid = keyUid,
+ accountType = accountType,
+ color = self.view.getSelectedColor(),
+ emoji = self.view.getSelectedEmoji())
+ if not success:
+ error "failed to store new seed phrase account", address=selectedAddrItem.getAddress()
+ else:
+ success = self.controller.addWalletAccount(
+ createKeystoreFile = createKeystoreFile,
+ doPasswordHashing = doPasswordHashing,
+ name = self.view.getAccountName(),
+ keyPairName = keyPairName,
+ address = address,
+ path = path,
+ lastUsedDerivationIndex = lastUsedDerivationIndex,
+ rootWalletMasterKey = rootWalletMasterKey,
+ publicKey = publicKey,
+ keyUid = keyUid,
+ accountType = accountType,
+ color = self.view.getSelectedColor(),
+ emoji = self.view.getSelectedEmoji())
+ if not success:
+ error "failed to store account", address=selectedAddrItem.getAddress()
+
+ if success:
+ self.closeAddAccountPopup(address)
+ else:
+ self.closeAddAccountPopup()
+
+method addAccount*[T](self: Module[T]) =
+ if self.isAuthenticationNeededForSelectedOrigin():
+ self.authenticateSelectedOrigin(AuthenticationReason.AddingAccount)
+ return
+ self.doAddingAccount()
+
+method buildNewPrivateKeyKeypairAndAddItToOrigin*[T](self: Module[T]) =
+ let genAcc = self.controller.getGeneratedAccount()
+ var item = newKeyPairItem(keyUid = genAcc.keyUid,
+ pubKey = genAcc.publicKey,
+ locked = false,
+ name = self.view.getNewKeyPairName(),
+ image = "",
+ icon = "key_pair_private_key",
+ pairType = KeyPairType.PrivateKeyImport)
+ self.setItemForSelectedOrigin(item)
+
+method buildNewSeedPhraseKeypairAndAddItToOrigin*[T](self: Module[T]) =
+ let genAcc = self.controller.getGeneratedAccount()
+
+ var item = newKeyPairItem(keyUid = genAcc.keyUid,
+ pubKey = genAcc.publicKey,
+ locked = false,
+ name = self.view.getNewKeyPairName(),
+ image = "",
+ icon = "key_pair_seed_phrase",
+ pairType = KeyPairType.SeedImport,
+ derivedFrom = genAcc.address)
+ self.setItemForSelectedOrigin(item)
diff --git a/src/app/modules/main/wallet_section/add_account/view.nim b/src/app/modules/main/wallet_section/add_account/view.nim
new file mode 100644
index 0000000000..813f7b6775
--- /dev/null
+++ b/src/app/modules/main/wallet_section/add_account/view.nim
@@ -0,0 +1,327 @@
+import NimQml
+import io_interface
+import derived_address_model
+import internal/[state, state_wrapper]
+import ../../../shared_models/[keypair_model, keypair_item]
+
+QtObject:
+ type
+ View* = ref object of QObject
+ delegate: io_interface.AccessInterface
+ currentState: StateWrapper
+ currentStateVariant: QVariant
+ originModel: KeyPairModel
+ originModelVariant: QVariant
+ selectedOrigin: KeyPairItem
+ selectedOriginVariant: QVariant
+ derivedAddressModel: DerivedAddressModel
+ derivedAddressModelVariant: QVariant
+ selectedDerivedAddress: DerivedAddressItem
+ selectedDerivedAddressVariant: QVariant
+ watchOnlyAccAddress: DerivedAddressItem
+ watchOnlyAccAddressVariant: QVariant
+ privateKeyAccAddress: DerivedAddressItem
+ privateKeyAccAddressVariant: QVariant
+ accountName: string
+ newKeyPairName: string
+ selectedEmoji: string
+ selectedColor: string
+ derivationPath: string
+ suggestedDerivationPath: string
+ actionAuthenticated: bool
+ scanningForActivityIsOngoing: bool
+ disablePopup: bool # unables user to interact with the popup (action buttons are disabled as well as close popup button)
+
+ proc delete*(self: View) =
+ self.currentStateVariant.delete
+ self.currentState.delete
+ self.originModel.delete
+ self.originModelVariant.delete
+ self.selectedOrigin.delete
+ self.selectedOriginVariant.delete
+ self.derivedAddressModel.delete
+ self.derivedAddressModelVariant.delete
+ self.selectedDerivedAddress.delete
+ self.selectedDerivedAddressVariant.delete
+ self.watchOnlyAccAddress.delete
+ self.watchOnlyAccAddressVariant.delete
+ self.privateKeyAccAddress.delete
+ self.privateKeyAccAddressVariant.delete
+ self.QObject.delete
+
+ proc newView*(delegate: io_interface.AccessInterface): View =
+ new(result, delete)
+ result.QObject.setup
+ result.delegate = delegate
+ result.currentState = newStateWrapper()
+ result.currentStateVariant = newQVariant(result.currentState)
+ result.originModel = newKeyPairModel()
+ result.originModelVariant = newQVariant(result.originModel)
+ result.selectedOrigin = newKeyPairItem()
+ result.selectedOriginVariant = newQVariant(result.selectedOrigin)
+ result.derivedAddressModel = newDerivedAddressModel()
+ result.derivedAddressModelVariant = newQVariant(result.derivedAddressModel)
+ result.selectedDerivedAddress = newDerivedAddressItem()
+ result.selectedDerivedAddressVariant = newQVariant(result.selectedDerivedAddress)
+ result.watchOnlyAccAddress = newDerivedAddressItem()
+ result.watchOnlyAccAddressVariant = newQVariant(result.watchOnlyAccAddress)
+ result.privateKeyAccAddress = newDerivedAddressItem()
+ result.privateKeyAccAddressVariant = newQVariant(result.privateKeyAccAddress)
+ result.actionAuthenticated = false
+ result.scanningForActivityIsOngoing = false
+ result.disablePopup = false
+
+ signalConnect(result.currentState, "backActionClicked()", result, "onBackActionClicked()", 2)
+ signalConnect(result.currentState, "cancelActionClicked()", result, "onCancelActionClicked()", 2)
+ signalConnect(result.currentState, "primaryActionClicked()", result, "onPrimaryActionClicked()", 2)
+ signalConnect(result.currentState, "secondaryActionClicked()", result, "onSecondaryActionClicked()", 2)
+ signalConnect(result.currentState, "tertiaryActionClicked()", result, "onTertiaryActionClicked()", 2)
+ signalConnect(result.currentState, "quaternaryActionClicked()", result, "onQuaternaryActionClicked()", 2)
+
+ proc currentStateObj*(self: View): State =
+ return self.currentState.getStateObj()
+
+ proc setCurrentState*(self: View, state: State) =
+ self.currentState.setStateObj(state)
+ proc getCurrentState(self: View): QVariant {.slot.} =
+ return self.currentStateVariant
+ QtProperty[QVariant] currentState:
+ read = getCurrentState
+
+ proc disablePopupChanged*(self: View) {.signal.}
+ proc getDisablePopup*(self: View): bool {.slot.} =
+ return self.disablePopup
+ QtProperty[bool] disablePopup:
+ read = getDisablePopup
+ notify = disablePopupChanged
+ proc setDisablePopup*(self: View, value: bool) =
+ self.disablePopup = value
+ self.disablePopupChanged()
+
+ proc getSeedPhrase*(self: View): string {.slot.} =
+ return self.delegate.getSeedPhrase()
+
+ proc onBackActionClicked*(self: View) {.slot.} =
+ self.delegate.onBackActionClicked()
+
+ proc onCancelActionClicked*(self: View) {.slot.} =
+ self.delegate.onCancelActionClicked()
+
+ proc onPrimaryActionClicked*(self: View) {.slot.} =
+ self.delegate.onPrimaryActionClicked()
+
+ proc onSecondaryActionClicked*(self: View) {.slot.} =
+ self.delegate.onSecondaryActionClicked()
+
+ proc onTertiaryActionClicked*(self: View) {.slot.} =
+ self.delegate.onTertiaryActionClicked()
+
+ proc onQuaternaryActionClicked*(self: View) {.slot.} =
+ self.delegate.onQuaternaryActionClicked()
+
+ proc originModelChanged*(self: View) {.signal.}
+ proc getOriginModel*(self: View): QVariant {.slot.} =
+ if self.originModelVariant.isNil:
+ return newQVariant()
+ return self.originModelVariant
+ QtProperty[QVariant] originModel:
+ read = getOriginModel
+ notify = originModelChanged
+
+ proc originModel*(self: View): KeyPairModel =
+ return self.originModel
+
+ proc setOriginModelItems*(self: View, items: seq[KeyPairItem]) =
+ self.originModel.setItems(items)
+ self.originModelChanged()
+
+ proc getSelectedOrigin*(self: View): KeyPairItem =
+ return self.selectedOrigin
+ proc getSelectedOriginAsVariant*(self: View): QVariant {.slot.} =
+ return self.selectedOriginVariant
+ QtProperty[QVariant] selectedOrigin:
+ read = getSelectedOriginAsVariant
+
+ proc setSelectedOrigin*(self: View, item: KeyPairItem) =
+ self.selectedOrigin.setItem(item)
+
+ proc getDerivedAddressModel(self: View): QVariant {.slot.} =
+ if self.derivedAddressModelVariant.isNil:
+ return newQVariant()
+ return self.derivedAddressModelVariant
+ QtProperty[QVariant] derivedAddressModel:
+ read = getDerivedAddressModel
+
+ proc derivedAddressModel*(self: View): DerivedAddressModel =
+ return self.derivedAddressModel
+
+ proc getSelectedDerivedAddress*(self: View): DerivedAddressItem =
+ return self.selectedDerivedAddress
+
+ proc selectedDerivedAddressChanged*(self: View) {.signal.}
+ proc getSelectedDerivedAddressVariant*(self: View): QVariant {.slot.} =
+ return self.selectedDerivedAddressVariant
+ QtProperty[QVariant] selectedDerivedAddress:
+ read = getSelectedDerivedAddressVariant
+ notify = selectedDerivedAddressChanged
+
+ proc setSelectedDerivedAddress*(self: View, item: DerivedAddressItem) =
+ self.selectedDerivedAddress.setItem(item)
+ self.selectedDerivedAddressChanged()
+
+ proc getWatchOnlyAccAddress*(self: View): DerivedAddressItem =
+ return self.watchOnlyAccAddress
+
+ proc watchOnlyAccAddressChanged*(self: View) {.signal.}
+ proc getWatchOnlyAccAddressVariant*(self: View): QVariant {.slot.} =
+ return self.watchOnlyAccAddressVariant
+ QtProperty[QVariant] watchOnlyAccAddress:
+ read = getWatchOnlyAccAddressVariant
+ notify = watchOnlyAccAddressChanged
+
+ proc setWatchOnlyAccAddress*(self: View, item: DerivedAddressItem) =
+ self.watchOnlyAccAddress.setItem(item)
+ self.watchOnlyAccAddressChanged()
+
+ proc getPrivateKeyAccAddress*(self: View): DerivedAddressItem =
+ return self.privateKeyAccAddress
+
+ proc privateKeyAccAddressChanged*(self: View) {.signal.}
+ proc getPrivateKeyAccAddressVariant*(self: View): QVariant {.slot.} =
+ return self.privateKeyAccAddressVariant
+ QtProperty[QVariant] privateKeyAccAddress:
+ read = getPrivateKeyAccAddressVariant
+ notify = privateKeyAccAddressChanged
+
+ proc setPrivateKeyAccAddress*(self: View, item: DerivedAddressItem) =
+ self.privateKeyAccAddress.setItem(item)
+ self.privateKeyAccAddressChanged()
+
+ proc actionAuthenticatedChanged*(self: View) {.signal.}
+ proc getActionAuthenticated*(self: View): bool {.slot.} =
+ return self.actionAuthenticated
+ proc setActionAuthenticated*(self: View, value: bool) {.slot.} =
+ self.actionAuthenticated = value
+ self.actionAuthenticatedChanged()
+ QtProperty[bool] actionAuthenticated:
+ read = getActionAuthenticated
+ write = setActionAuthenticated
+ notify = actionAuthenticatedChanged
+
+ proc scanningForActivityChanged*(self: View) {.signal.}
+ proc getScanningForActivityIsOngoing*(self: View): bool {.slot.} =
+ return self.scanningForActivityIsOngoing
+ proc setScanningForActivityIsOngoing*(self: View, value: bool) {.slot.} =
+ self.scanningForActivityIsOngoing = value
+ self.scanningForActivityChanged()
+ QtProperty[bool] scanningForActivityIsOngoing:
+ read = getScanningForActivityIsOngoing
+ write = setScanningForActivityIsOngoing
+ notify = scanningForActivityChanged
+
+ proc accountNameChanged*(self: View) {.signal.}
+ proc setAccountName*(self: View, value: string) {.slot.} =
+ if self.accountName == value:
+ return
+ self.accountName = value
+ self.accountNameChanged()
+ proc getAccountName*(self: View): string {.slot.} =
+ return self.accountName
+ QtProperty[string] accountName:
+ read = getAccountName
+ write = setAccountName
+ notify = accountNameChanged
+
+ proc newKeyPairNameChanged*(self: View) {.signal.}
+ proc setNewKeyPairName*(self: View, value: string) {.slot.} =
+ if self.newKeyPairName == value:
+ return
+ self.newKeyPairName = value
+ self.newKeyPairNameChanged()
+ proc getNewKeyPairName*(self: View): string {.slot.} =
+ return self.newKeyPairName
+ QtProperty[string] newKeyPairName:
+ read = getNewKeyPairName
+ write = setNewKeyPairName
+ notify = newKeyPairNameChanged
+
+ proc selectedEmojiChanged*(self: View) {.signal.}
+ proc setSelectedEmoji*(self: View, value: string) {.slot.} =
+ if self.selectedEmoji == value:
+ return
+ self.selectedEmoji = value
+ self.selectedEmojiChanged()
+ proc getSelectedEmoji*(self: View): string {.slot.} =
+ return self.selectedEmoji
+ QtProperty[string] selectedEmoji:
+ read = getSelectedEmoji
+ write = setSelectedEmoji
+ notify = selectedEmojiChanged
+
+ proc selectedColorChanged*(self: View) {.signal.}
+ proc setSelectedColor*(self: View, value: string) {.slot.} =
+ if self.selectedColor == value:
+ return
+ self.selectedColor = value
+ self.selectedColorChanged()
+ proc getSelectedColor*(self: View): string {.slot.} =
+ return self.selectedColor
+ QtProperty[string] selectedColor:
+ read = getSelectedColor
+ write = setSelectedColor
+ notify = selectedColorChanged
+
+ proc derivationPathChanged*(self: View) {.signal.}
+ proc getDerivationPath*(self: View): string {.slot.} =
+ return self.derivationPath
+ proc setDerivationPath*(self: View, value: string) {.slot.} =
+ if self.derivationPath == value:
+ return
+ self.derivationPath = value
+ self.derivationPathChanged()
+ QtProperty[string] derivationPath:
+ read = getDerivationPath
+ write = setDerivationPath
+ notify = derivationPathChanged
+
+ proc suggestedDerivationPathChanged*(self: View) {.signal.}
+ proc getSuggestedDerivationPath*(self: View): string {.slot.} =
+ return self.suggestedDerivationPath
+ QtProperty[string] suggestedDerivationPath:
+ read = getSuggestedDerivationPath
+ notify = suggestedDerivationPathChanged
+
+ proc setSuggestedDerivationPath*(self: View, value: string) =
+ self.suggestedDerivationPath = value
+ self.suggestedDerivationPathChanged()
+
+ proc changeSelectedOrigin*(self: View, keyUid: string) {.slot.} =
+ self.delegate.changeSelectedOrigin(keyUid)
+
+ proc changeDerivationPath*(self: View, path: string) {.slot.} =
+ self.delegate.changeDerivationPath(path)
+
+ proc changeSelectedDerivedAddress*(self: View, address: string) {.slot.} =
+ self.delegate.changeSelectedDerivedAddress(address)
+
+ proc changeWatchOnlyAccountAddress*(self: View, address: string) {.slot.} =
+ self.delegate.changeWatchOnlyAccountAddress(address)
+
+ proc changePrivateKey*(self: View, privateKey: string) {.slot.} =
+ self.delegate.changePrivateKey(privateKey)
+
+ proc changeSeedPhrase*(self: View, seedPhrase: string) {.slot.} =
+ self.delegate.changeSeedPhrase(seedPhrase)
+
+ proc validSeedPhrase*(self: View, seedPhrase: string): bool {.slot.} =
+ return self.delegate.validSeedPhrase(seedPhrase)
+
+ proc resetDerivationPath*(self: View) {.slot.} =
+ self.delegate.resetDerivationPath()
+
+ proc authenticateForEditingDerivationPath*(self: View) {.slot.} =
+ self.delegate.authenticateForEditingDerivationPath()
+
+ proc startScanningForActivity*(self: View) {.slot.} =
+ self.delegate.startScanningForActivity()
+
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/io_interface.nim b/src/app/modules/main/wallet_section/io_interface.nim
index b8d2e304ef..7b71ee7845 100644
--- a/src/app/modules/main/wallet_section/io_interface.nim
+++ b/src/app/modules/main/wallet_section/io_interface.nim
@@ -1,3 +1,4 @@
+import NimQml
import ../../shared_models/currency_amount
export CurrencyAmount
@@ -56,3 +57,15 @@ method savedAddressesModuleDidLoad*(self: AccessInterface) {.base.} =
method buySellCryptoModuleDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
+
+method runAddAccountPopup*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method getAddAccountModule*(self: AccessInterface): QVariant {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onAddAccountModuleLoaded*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method destroyAddAccountPopup*(self: AccessInterface, switchToAccWithAddress: string = "") {.base.} =
+ raise newException(ValueError, "No implementation available")
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/module.nim b/src/app/modules/main/wallet_section/module.nim
index fd69c8e452..68fea79ecf 100644
--- a/src/app/modules/main/wallet_section/module.nim
+++ b/src/app/modules/main/wallet_section/module.nim
@@ -1,4 +1,4 @@
-import NimQml
+import NimQml, chronicles
import ./controller, ./view
import ./io_interface as io_interface
@@ -11,6 +11,7 @@ import ./current_account/module as current_account_module
import ./transactions/module as transactions_module
import ./saved_addresses/module as saved_addresses_module
import ./buy_sell_crypto/module as buy_sell_crypto_module
+import ./add_account/module as add_account_module
import ../../../global/global_singleton
import ../../../core/eventemitter
@@ -27,6 +28,9 @@ import ../../../../app_service/service/accounts/service as accounts_service
import ../../../../app_service/service/node/service as node_service
import ../../../../app_service/service/network_connection/service as network_connection_service
+logScope:
+ topics = "wallet-section-module"
+
import io_interface
export io_interface
@@ -45,7 +49,10 @@ type
transactionsModule: transactions_module.AccessInterface
savedAddressesModule: saved_addresses_module.AccessInterface
buySellCryptoModule: buy_sell_crypto_module.AccessInterface
+ addAccountModule: add_account_module.AccessInterface
+ keycardService: keycard_service.Service
accountsService: accounts_service.Service
+ walletAccountService: wallet_account_service.Service
proc newModule*(
delegate: delegate_interface.AccessInterface,
@@ -66,6 +73,9 @@ proc newModule*(
result = Module()
result.delegate = delegate
result.events = events
+ result.keycardService = keycardService
+ result.accountsService = accountsService
+ result.walletAccountService = walletAccountService
result.moduleLoaded = false
result.controller = newController(result, settingsService, walletAccountService, currencyService)
result.view = newView(result)
@@ -88,6 +98,8 @@ method delete*(self: Module) =
self.buySellCryptoModule.delete
self.controller.delete
self.view.delete
+ if not self.addAccountModule.isNil:
+ self.addAccountModule.delete
method updateCurrency*(self: Module, currency: string) =
self.controller.updateCurrency(currency)
@@ -193,3 +205,25 @@ method savedAddressesModuleDidLoad*(self: Module) =
method buySellCryptoModuleDidLoad*(self: Module) =
self.checkIfModuleDidLoad()
+
+method destroyAddAccountPopup*(self: Module, switchToAccWithAddress: string = "") =
+ if not self.addAccountModule.isNil:
+ if switchToAccWithAddress.len > 0:
+ self.switchAccountByAddress(switchToAccWithAddress)
+ self.view.emitDestroyAddAccountPopup()
+ self.addAccountModule.delete
+ self.addAccountModule = nil
+
+method runAddAccountPopup*(self: Module) =
+ self.destroyAddAccountPopup()
+ self.addAccountModule = add_account_module.newModule(self, self.events, self.keycardService, self.accountsService,
+ self.walletAccountService)
+ self.addAccountModule.load()
+
+method getAddAccountModule*(self: Module): QVariant =
+ if self.addAccountModule.isNil:
+ return newQVariant()
+ return self.addAccountModule.getModuleAsVariant()
+
+method onAddAccountModuleLoaded*(self: Module) =
+ self.view.emitDisplayAddAccountPopup()
\ No newline at end of file
diff --git a/src/app/modules/main/wallet_section/view.nim b/src/app/modules/main/wallet_section/view.nim
index 1d13ef5397..cb3ed139a2 100644
--- a/src/app/modules/main/wallet_section/view.nim
+++ b/src/app/modules/main/wallet_section/view.nim
@@ -97,3 +97,19 @@ QtObject:
self.signingPhrase = signingPhrase
self.isMnemonicBackedUp = mnemonicBackedUp
self.currentCurrencyChanged()
+
+ proc runAddAccountPopup*(self: View) {.slot.} =
+ self.delegate.runAddAccountPopup()
+
+ proc getAddAccountModule(self: View): QVariant {.slot.} =
+ return self.delegate.getAddAccountModule()
+ QtProperty[QVariant] addAccountModule:
+ read = getAddAccountModule
+
+ proc displayAddAccountPopup*(self: View) {.signal.}
+ proc emitDisplayAddAccountPopup*(self: View) =
+ self.displayAddAccountPopup()
+
+ proc destroyAddAccountPopup*(self: View) {.signal.}
+ proc emitDestroyAddAccountPopup*(self: View) =
+ self.destroyAddAccountPopup()
\ No newline at end of file
diff --git a/src/app/modules/shared_modules/keycard_popup/module.nim b/src/app/modules/shared_modules/keycard_popup/module.nim
index 3261ea6f8c..fb965e97e6 100644
--- a/src/app/modules/shared_modules/keycard_popup/module.nim
+++ b/src/app/modules/shared_modules/keycard_popup/module.nim
@@ -453,18 +453,24 @@ method runFlow*[T](self: Module[T], flowToRun: FlowType, keyUid = "", bip44Path
return
if flowToRun == FlowType.Authentication:
self.controller.connectKeychainSignals()
- if keyUid.len > 0:
+ if keyUid.len == 0 or keyUid == singletonInstance.userProfile.getKeyUid():
+ if singletonInstance.userProfile.getUsingBiometricLogin():
+ self.controller.tryToObtainDataFromKeychain()
+ return
+ if singletonInstance.userProfile.getIsKeycardUser():
+ self.prepareKeyPairItemForAuthentication(singletonInstance.userProfile.getKeyUid())
+ self.tmpLocalState = newReadingKeycardState(flowToRun, nil)
+ self.controller.runAuthenticationFlow(singletonInstance.userProfile.getKeyUid())
+ return
+ self.view.setCurrentState(newEnterPasswordState(flowToRun, nil))
+ self.authenticationPopupIsAlreadyRunning = true
+ self.controller.readyToDisplayPopup()
+ return
+ else:
self.prepareKeyPairItemForAuthentication(keyUid)
self.tmpLocalState = newReadingKeycardState(flowToRun, nil)
self.controller.runAuthenticationFlow(keyUid)
- return
- if singletonInstance.userProfile.getUsingBiometricLogin():
- self.controller.tryToObtainDataFromKeychain()
- return
- self.view.setCurrentState(newEnterPasswordState(flowToRun, nil))
- self.authenticationPopupIsAlreadyRunning = true
- self.controller.readyToDisplayPopup()
- return
+ return
if flowToRun == FlowType.UnlockKeycard:
## since we can run unlock keycard flow from an already running flow, in order to avoid changing displayed keypair
## (locked keypair) we have to set keycard uid of a keycard used in the flow we're jumping from to `UnlockKeycard` flow.
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml
index 313bf0a8f3..c830de70c3 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml
@@ -423,8 +423,8 @@ Item {
anchors.fill: parent
verticalAlignment: parent.verticalAlignment
font.pixelSize: 15
- wrapMode: root.multiline ? Text.WordWrap : Text.NoWrap
- elide: StatusBaseText.ElideRight
+ wrapMode: root.multiline ? Text.WrapAnywhere : Text.NoWrap
+ elide: root.multiline? Text.ElideNone : Text.ElideRight
color: root.enabled ? Theme.palette.baseColor1 : Theme.palette.directColor6
}
}
diff --git a/ui/StatusQ/src/StatusQ/Core/Theme/StatusDarkTheme.qml b/ui/StatusQ/src/StatusQ/Core/Theme/StatusDarkTheme.qml
index 711f72e968..d0679df617 100644
--- a/ui/StatusQ/src/StatusQ/Core/Theme/StatusDarkTheme.qml
+++ b/ui/StatusQ/src/StatusQ/Core/Theme/StatusDarkTheme.qml
@@ -138,7 +138,7 @@ ThemePalette {
}
statusMenu: QtObject {
- property color backgroundColor: baseColor2
+ property color backgroundColor: baseColor3
property color hoverBackgroundColor: directColor7
property color separatorColor: directColor7
}
diff --git a/ui/app/AppLayouts/Onboarding/views/LoginView.qml b/ui/app/AppLayouts/Onboarding/views/LoginView.qml
index 30652c9683..eebb2c01af 100644
--- a/ui/app/AppLayouts/Onboarding/views/LoginView.qml
+++ b/ui/app/AppLayouts/Onboarding/views/LoginView.qml
@@ -321,7 +321,7 @@ Item {
if (model.username === Constants.appTranslatableConstants.loginAccountsListAddNewUser ||
model.username === Constants.appTranslatableConstants.loginAccountsListAddExistingUser ||
model.username === Constants.appTranslatableConstants.loginAccountsListLostKeycard) {
- return Constants.appTranslationMap[model.username]
+ return Utils.appTranslation(model.username)
}
return model.username
}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/AddAccountPopup.qml b/ui/app/AppLayouts/Wallet/addaccount/AddAccountPopup.qml
new file mode 100644
index 0000000000..9086153c2e
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/AddAccountPopup.qml
@@ -0,0 +1,260 @@
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+
+import StatusQ.Core 0.1
+import StatusQ.Popups 0.1
+import StatusQ.Controls 0.1
+
+import utils 1.0
+
+import "./stores"
+import "./states"
+
+StatusModal {
+ id: root
+
+ property AddAccountStore store
+
+ width: Constants.addAccountPopup.popupWidth
+ height: {
+ let availableSpace = Global.applicationWindow.height - root.margins * 2
+ return Math.min(implicitHeight, availableSpace)
+ }
+
+ closePolicy: root.store.disablePopup? Popup.NoAutoClose : Popup.CloseOnEscape | Popup.CloseOnPressOutside
+ hasCloseButton: !root.store.disablePopup
+
+ header.title: qsTr("Add a new account")
+
+ onOpened: {
+ root.store.resetStoreValues()
+ }
+
+ onClosed: {
+ root.store.currentState.doCancelAction()
+ }
+
+ contentItem: StatusScrollView {
+ id: scrollView
+
+ implicitWidth: contentWidth + leftPadding + rightPadding
+ implicitHeight: contentHeight + topPadding + bottomPadding
+ padding: 0
+
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+ width: parent.width
+ height: {
+ let availableSpace = Global.applicationWindow.height - (root.margins * 2 + root.topPadding + root.bottomPadding)
+ return Math.min(content.height, availableSpace)
+ }
+
+ Item {
+ id: content
+ objectName: "AddAccountPopup-Content"
+
+ width: scrollView.availableWidth
+
+ Loader {
+ id: loader
+ width: parent.width
+ sourceComponent: {
+ switch (root.store.currentState.stateType) {
+ case Constants.addAccountPopup.state.main:
+ return mainComponent
+ case Constants.addAccountPopup.state.confirmAddingNewMasterKey:
+ return confirmAddingNewMasterKeyComponent
+ case Constants.addAccountPopup.state.confirmSeedPhraseBackup:
+ return confirmSeedPhraseBackupComponent
+ case Constants.addAccountPopup.state.displaySeedPhrase:
+ return displaySeedPhraseComponent
+ case Constants.addAccountPopup.state.enterKeypairName:
+ return enterKeypairNameComponent
+ case Constants.addAccountPopup.state.enterPrivateKey:
+ return enterPrivateKeyComponent
+ case Constants.addAccountPopup.state.enterSeedPhraseWord1:
+ case Constants.addAccountPopup.state.enterSeedPhraseWord2:
+ return enterSeedPhraseWordComponent
+ case Constants.addAccountPopup.state.enterSeedPhrase:
+ return enterSeedPhraseComponent
+ case Constants.addAccountPopup.state.selectMasterKey:
+ return selectMasterKeyComponent
+ }
+
+ return undefined
+ }
+
+ onLoaded: {
+ content.height = Qt.binding(function(){return item.height})
+ }
+ }
+
+ Component {
+ id: mainComponent
+ Main {
+ store: root.store
+ }
+ }
+
+ Component {
+ id: confirmAddingNewMasterKeyComponent
+ ConfirmAddingNewMasterKey {
+ height: Constants.addAccountPopup.contentHeight1
+ store: root.store
+ }
+ }
+
+ Component {
+ id: confirmSeedPhraseBackupComponent
+ ConfirmSeedPhraseBackup {
+ height: Constants.addAccountPopup.contentHeight1
+ store: root.store
+ }
+ }
+
+ Component {
+ id: displaySeedPhraseComponent
+ DisplaySeedPhrase {
+ height: Constants.addAccountPopup.contentHeight1
+ store: root.store
+ }
+ }
+
+ Component {
+ id: enterKeypairNameComponent
+ EnterKeypairName {
+ height: Constants.addAccountPopup.contentHeight1
+ store: root.store
+ }
+ }
+
+ Component {
+ id: enterPrivateKeyComponent
+ EnterPrivateKey {
+ height: Constants.addAccountPopup.contentHeight1
+ store: root.store
+ }
+ }
+
+ Component {
+ id: enterSeedPhraseComponent
+ EnterSeedPhrase {
+ height: Constants.addAccountPopup.contentHeight2
+ store: root.store
+ }
+ }
+
+ Component {
+ id: enterSeedPhraseWordComponent
+ EnterSeedPhraseWord {
+ height: Constants.addAccountPopup.contentHeight1
+ store: root.store
+ }
+ }
+
+ Component {
+ id: selectMasterKeyComponent
+ SelectMasterKey {
+ height: Constants.addAccountPopup.contentHeight1
+ store: root.store
+ }
+ }
+ }
+ }
+
+ leftButtons: [
+ StatusBackButton {
+ id: backButton
+ objectName: "AddAccountPopup-BackButton"
+ visible: root.store.currentState.displayBackButton
+ enabled: !root.store.disablePopup
+ height: Constants.addAccountPopup.footerButtonsHeight
+ width: height
+ onClicked: {
+ if (root.store.currentState.stateType === Constants.addAccountPopup.state.confirmAddingNewMasterKey) {
+ root.store.addingNewMasterKeyConfirmed = false
+ }
+ else if (root.store.currentState.stateType === Constants.addAccountPopup.state.displaySeedPhrase) {
+ root.store.seedPhraseRevealed = false
+ }
+ else if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1) {
+ root.store.seedPhraseWord1Valid = false
+ root.store.seedPhraseWord1WordNumber = -1
+ root.store.seedPhraseWord2Valid = false
+ root.store.seedPhraseWord2WordNumber = -1
+ }
+ else if (root.store.currentState.stateType === Constants.addAccountPopup.state.confirmSeedPhraseBackup) {
+ root.store.seedPhraseBackupConfirmed = false
+ }
+ else if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterKeypairName) {
+ root.store.addAccountModule.newKeyPairName = ""
+ }
+
+ root.store.currentState.doBackAction()
+ }
+ }
+ ]
+
+ rightButtons: [
+ StatusButton {
+ id: primaryButton
+ objectName: "AddAccountPopup-PrimaryButton"
+ type: root.store.currentState.stateType === Constants.addAccountPopup.state.main?
+ StatusBaseButton.Type.Primary :
+ StatusBaseButton.Type.Normal
+ height: Constants.addAccountPopup.footerButtonsHeight
+ text: {
+ switch (root.store.currentState.stateType) {
+
+ case Constants.addAccountPopup.state.main:
+ return qsTr("Add account")
+ case Constants.addAccountPopup.state.enterPrivateKey:
+ case Constants.addAccountPopup.state.enterSeedPhrase:
+ case Constants.addAccountPopup.state.enterSeedPhraseWord1:
+ case Constants.addAccountPopup.state.enterSeedPhraseWord2:
+ case Constants.addAccountPopup.state.confirmSeedPhraseBackup:
+ case Constants.addAccountPopup.state.enterKeypairName:
+ return qsTr("Continue")
+ case Constants.addAccountPopup.state.confirmAddingNewMasterKey:
+ return qsTr("Reveal seed phrase")
+ case Constants.addAccountPopup.state.displaySeedPhrase:
+ return qsTr("Confirm seed phrase")
+ }
+
+ return ""
+ }
+ visible: text !== ""
+ enabled: root.store.primaryPopupButtonEnabled
+
+ icon.name: {
+ if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterPrivateKey ||
+ root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhrase ||
+ root.store.currentState.stateType === Constants.addAccountPopup.state.confirmAddingNewMasterKey ||
+ root.store.currentState.stateType === Constants.addAccountPopup.state.displaySeedPhrase ||
+ root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1 ||
+ root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord2 ||
+ root.store.currentState.stateType === Constants.addAccountPopup.state.confirmSeedPhraseBackup ||
+ root.store.currentState.stateType === Constants.addAccountPopup.state.enterKeypairName ||
+ root.store.addAccountModule.actionAuthenticated ||
+ root.store.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.unknown &&
+ root.store.selectedOrigin.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc) {
+ return ""
+ }
+
+ if (root.store.selectedOrigin.keyUid === root.store.userProfileKeyUid &&
+ root.store.userProfileUsingBiometricLogin) {
+ return "touch-id"
+ }
+
+ if (root.store.selectedOrigin.migratedToKeycard || root.store.userProfileIsKeycardUser) {
+ return "keycard"
+ }
+
+ return "password"
+ }
+
+ onClicked: {
+ root.store.submitAddAccount(null)
+ }
+ }
+ ]
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/panels/AccountAddressSelection.qml b/ui/app/AppLayouts/Wallet/addaccount/panels/AccountAddressSelection.qml
new file mode 100644
index 0000000000..9862b1d1c2
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/panels/AccountAddressSelection.qml
@@ -0,0 +1,229 @@
+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.Core.Utils 0.1 as StatusQUtils
+import StatusQ.Controls 0.1
+import StatusQ.Components 0.1
+import StatusQ.Popups 0.1
+
+import utils 1.0
+
+import SortFilterProxyModel 0.2
+
+import "../stores"
+
+StatusMenu {
+ id: root
+
+ property AddAccountStore store
+
+ property int itemsPerPage: 5
+
+ signal selected(string address)
+
+ QtObject {
+ id: d
+
+ property int currentPage: 0
+ readonly property int totalPages: Math.ceil(root.store.derivedAddressModel.count / root.itemsPerPage)
+ }
+
+ SortFilterProxyModel {
+ id: proxyModel
+ sourceModel: root.store.derivedAddressModel
+ filters: ExpressionFilter {
+ expression: {
+ let lowerBound = root.itemsPerPage * d.currentPage
+ return model.index >= lowerBound && model.index < lowerBound + root.itemsPerPage
+ }
+ }
+ }
+
+ contentItem: Column {
+ width: root.width
+
+ Item {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: Constants.addAccountPopup.itemHeight - root.topPadding
+
+ Row{
+ anchors.centerIn: parent
+ spacing: Style.current.halfPadding
+
+ StatusIcon {
+ visible: !root.store.addAccountModule.scanningForActivityIsOngoing ||
+ root.store.derivedAddressModel.loadedCount > 0
+ width: 20
+ height: 20
+ icon: "flash"
+ color: root.store.derivedAddressModel.loadedCount === 0?
+ Theme.palette.primaryColor1 : Theme.palette.successColor1
+ }
+
+ StatusLinkText {
+ visible: !root.store.addAccountModule.scanningForActivityIsOngoing
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ text: qsTr("Scan addresses for activity")
+ color: Theme.palette.primaryColor1
+ onClicked: {
+ root.store.startScanningForActivity()
+ }
+ }
+
+ StatusLoadingIndicator {
+ visible: root.store.addAccountModule.scanningForActivityIsOngoing &&
+ root.store.derivedAddressModel.loadedCount === 0
+ }
+
+ StatusBaseText {
+ visible: root.store.addAccountModule.scanningForActivityIsOngoing
+ color: root.store.derivedAddressModel.loadedCount === 0?
+ Theme.palette.baseColor1 : Theme.palette.successColor1
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ text: root.store.derivedAddressModel.loadedCount === 0?
+ qsTr("Scanning for activity...")
+ : qsTr("Activity fetched for %1 / %2 addresses").arg(root.store.derivedAddressModel.loadedCount).arg(root.store.derivedAddressModel.count)
+ }
+ }
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: 1
+ color: Theme.palette.baseColor2
+ }
+ }
+
+ Repeater {
+ model: proxyModel
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: Constants.addAccountPopup.itemHeight
+ enabled: !model.addressDetails.alreadyCreated
+ radius: Style.current.halfPadding
+ color: {
+ if (sensor.containsMouse) {
+ return Theme.palette.baseColor2
+ }
+ return "transparent"
+ }
+
+ GridLayout {
+ anchors.fill: parent
+ anchors.leftMargin: Style.current.padding
+ anchors.rightMargin: Style.current.padding
+ columnSpacing: Style.current.padding
+ rowSpacing: 0
+
+ StatusBaseText {
+ Layout.preferredWidth: 208
+ color: model.addressDetails.alreadyCreated? Theme.palette.baseColor1 : Theme.palette.directColor1
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ text: StatusQUtils.Utils.elideText(model.addressDetails.address, 15, 4)
+ }
+
+ Row {
+ Layout.preferredWidth: 108
+ spacing: Style.current.halfPadding * 0.5
+
+ StatusIcon {
+ visible: model.addressDetails.loaded && model.addressDetails.hasActivity
+ width: 20
+ height: 20
+ icon: "flash"
+ color: Theme.palette.successColor1
+ }
+
+ StatusTextWithLoadingState {
+ width: 84
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ text: {
+ if (!root.store.addAccountModule.scanningForActivityIsOngoing) {
+ return ""
+ }
+ if (!model.addressDetails.loaded) {
+ return qsTr("loading...")
+ }
+ if (model.addressDetails.hasActivity) {
+ return qsTr("Has activity")
+ }
+ return qsTr("No activity")
+ }
+ color: {
+ if (!root.store.addAccountModule.scanningForActivityIsOngoing || !model.addressDetails.loaded) {
+ return "transparent"
+ }
+ if (model.addressDetails.hasActivity) {
+ return Theme.palette.successColor1
+ }
+ return Theme.palette.warningColor1
+ }
+ loading: root.store.addAccountModule.scanningForActivityIsOngoing && !model.addressDetails.loaded
+ }
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: 20
+ color: Theme.palette.baseColor1
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ text: model.addressDetails.order
+ }
+
+ StatusFlatRoundButton {
+ Layout.preferredWidth: 20
+ Layout.preferredHeight: 20
+ type: StatusFlatRoundButton.Type.Tertiary
+ icon.name: "external"
+ icon.width: 16
+ icon.height: 16
+ onClicked: {
+ Qt.openUrlExternally("https://etherscan.io/address/%1".arg(model.addressDetails.address))
+ }
+ }
+ }
+
+ MouseArea {
+ id: sensor
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.selected(model.addressDetails.address)
+ }
+ }
+ }
+ }
+
+ Item {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: Constants.addAccountPopup.itemHeight - root.bottomPadding
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ height: 1
+ color: Theme.palette.baseColor2
+ }
+
+ StatusPageIndicator {
+ anchors.top: parent.top
+ anchors.topMargin: (Constants.addAccountPopup.itemHeight - height) * 0.5
+ anchors.horizontalCenter: parent.horizontalCenter
+ totalPages: d.totalPages
+
+ onCurrentIndexChanged: {
+ d.currentPage = currentIndex
+ }
+ }
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/panels/AddressDetails.qml b/ui/app/AppLayouts/Wallet/addaccount/panels/AddressDetails.qml
new file mode 100644
index 0000000000..fd27670f8e
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/panels/AddressDetails.qml
@@ -0,0 +1,60 @@
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+
+import StatusQ.Core 0.1
+import StatusQ.Core.Theme 0.1
+
+import utils 1.0
+
+Row {
+ id: root
+
+ property var addressDetailsItem
+ property bool defaultMessageCondition: true
+ property string defaultMessage: ""
+
+ StatusIcon {
+ id: icon
+ visible: root.addressDetailsItem &&
+ root.addressDetailsItem.loaded &&
+ root.addressDetailsItem.address !== "" &&
+ root.addressDetailsItem.hasActivity
+ width: 20
+ height: 20
+ icon: "flash"
+ color: Theme.palette.successColor1
+ }
+
+ StatusBaseText {
+ width: icon.visible? parent.width - icon.width : parent.width
+ font.pixelSize: Constants.addAccountPopup.labelFontSize2
+ wrapMode: Text.WordWrap
+ text: {
+ if (root.defaultMessageCondition) {
+ return root.defaultMessage
+ }
+ if (!root.addressDetailsItem || !root.addressDetailsItem.loaded) {
+ return qsTr("Scanning for activity...")
+ }
+ if (root.addressDetailsItem.alreadyCreated) {
+ return qsTr("Already added")
+ }
+ if (root.addressDetailsItem.hasActivity) {
+ return qsTr("Has activity")
+ }
+ return qsTr("No activity")
+ }
+ color: {
+ if (root.defaultMessageCondition || !root.addressDetailsItem || !root.addressDetailsItem.loaded) {
+ return Theme.palette.baseColor1
+ }
+ if (root.addressDetailsItem.alreadyCreated) {
+ return Theme.palette.dangerColor1
+ }
+ if (root.addressDetailsItem.hasActivity) {
+ return Theme.palette.successColor1
+ }
+ return Theme.palette.warningColor1
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPath.qml b/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPath.qml
new file mode 100644
index 0000000000..ef5a53df30
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPath.qml
@@ -0,0 +1,174 @@
+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.Controls 0.1
+import StatusQ.Components 0.1
+
+import utils 1.0
+
+import "../stores"
+
+GridLayout {
+ id: root
+
+ property AddAccountStore store
+
+ columns: 3
+ columnSpacing: Style.current.padding
+ rowSpacing: Style.current.halfPadding
+
+ QtObject {
+ id: d
+
+ readonly property int oneHalfWidth: (root.width - root.columnSpacing) * 0.5
+ }
+
+ StatusBaseText {
+ Layout.fillWidth: true
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ text: qsTr("Derivation Path")
+ }
+
+ StatusLinkText {
+ enabled: root.store.addAccountModule.suggestedDerivationPath !== root.store.addAccountModule.derivationPath
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ text: qsTr("Reset")
+ color: enabled? Theme.palette.primaryColor1 : Theme.palette.baseColor1
+
+ onClicked: {
+ root.store.resetDerivationPath()
+ }
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: d.oneHalfWidth
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ text: qsTr("Account")
+ }
+
+ StatusInput {
+ id: derivationPath
+ Layout.preferredWidth: d.oneHalfWidth
+ Layout.columnSpan: 2
+
+ text: root.store.addAccountModule.derivationPath
+ onTextChanged: {
+ let t = text
+ if (t.endsWith("\n")) {
+ t = t.replace("\n", "")
+ }
+ if(root.store.derivationPathRegEx.test(t)) {
+ root.store.changeDerivationPathPostponed(t)
+ }
+ else {
+ root.store.addAccountModule.derivationPath = t
+ }
+ }
+
+ multiline: false
+ input.rightComponent: StatusIcon {
+ icon: "chevron-down"
+ color: Theme.palette.baseColor1
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ derivationPathSelection.popup(derivationPath.x, derivationPath.y + derivationPath.height + Style.current.halfPadding)
+ }
+ }
+ }
+
+ onKeyPressed: {
+ root.store.submitAddAccount(event)
+ }
+
+ DerivationPathSelection {
+ id: derivationPathSelection
+
+ roots: root.store.roots
+ translation: root.store.translation
+ selectedRootPath: root.store.selectedRootPath
+
+ onSelected: {
+ root.store.changeRootDerivationPath(rootPath)
+ }
+ }
+ }
+
+ StatusListItem {
+ id: generatedAddress
+ Layout.preferredWidth: d.oneHalfWidth
+ Layout.preferredHeight: derivationPath.height
+ color: "transparent"
+ border.width: 1
+ border.color: Theme.palette.baseColor2
+ enabled: root.store.derivedAddressModel.count > 1
+ statusListItemTitle.elide: Qt.ElideMiddle
+ loading: root.store.derivedAddressModel.count === 0
+
+ title: {
+ if (!!root.store.selectedDerivedAddress && root.store.selectedDerivedAddress.address !== "") {
+ return root.store.selectedDerivedAddress.address
+ }
+ else if (root.store.derivedAddressModel.count > 1) {
+ return qsTr("Select address")
+ }
+
+ return "0x0000000000000000000000000000000000000000"
+ }
+
+ components: [
+ StatusIcon {
+ visible: root.store.derivedAddressModel.count > 1
+ icon: "chevron-down"
+ color: Theme.palette.baseColor1
+ }
+ ]
+
+ onClicked: {
+ accountAddressSelection.popup(-generatedAddress.x, generatedAddress.y + generatedAddress.height + Style.current.halfPadding)
+ }
+
+ AccountAddressSelection {
+ id: accountAddressSelection
+ width: root.width
+ store: root.store
+
+ onSelected: {
+ accountAddressSelection.close()
+ root.store.changeSelectedDerivedAddress(address)
+ }
+ }
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: d.oneHalfWidth
+ Layout.columnSpan: 2
+ font.pixelSize: Constants.addAccountPopup.labelFontSize2
+ color: Theme.palette.baseColor1
+ text: root.store.translation(root.store.selectedRootPath, true)
+ }
+
+ AddressDetails {
+ Layout.preferredWidth: d.oneHalfWidth
+ addressDetailsItem: root.store.selectedDerivedAddress
+ defaultMessage: ""
+ defaultMessageCondition: !root.store.selectedDerivedAddress || root.store.selectedDerivedAddress.address === ""
+ }
+
+ StatusCheckBox {
+ visible: root.store.derivationPathOutOfTheDefaultStatusDerivationTree
+ Layout.fillWidth: true
+ Layout.columnSpan: 3
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ text: qsTr("I understand that this non-Ethereum derivation path is incompatible with Keycard")
+ onToggled: {
+ root.store.derivationPathOutOfTheDefaultStatusDerivationTreeConfirmed = checked
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPathSection.qml b/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPathSection.qml
new file mode 100644
index 0000000000..f48aee4760
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPathSection.qml
@@ -0,0 +1,123 @@
+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.Controls 0.1
+
+import utils 1.0
+
+import "../stores"
+
+Column {
+ id: root
+
+ property AddAccountStore store
+
+ padding: Style.current.padding
+ state: root.store.addAccountModule.actionAuthenticated? d.expandedState : d.collapsedState
+
+ QtObject {
+ id: d
+
+ readonly property string expandedState: "expanded"
+ readonly property string collapsedState: "collapsed"
+ }
+
+ RowLayout {
+ width: parent.width - 2 * root.padding
+ height: 64
+
+ StatusBaseText {
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ textFormat: Text.RichText
+ text: {
+ let t = qsTr("Derivation path")
+ let t1 = qsTr("(advanced)")
+ return `%1 %2`.arg(t).arg(t1)
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ }
+
+ StatusButton {
+ visible: !root.store.addAccountModule.actionAuthenticated
+ text: qsTr("Edit")
+
+ icon.name: {
+ if (root.store.selectedOrigin.keyUid === root.store.userProfileKeyUid &&
+ root.store.userProfileUsingBiometricLogin) {
+ return "touch-id"
+ }
+
+ if (root.store.selectedOrigin.migratedToKeycard || root.store.userProfileIsKeycardUser) {
+ return "keycard"
+ }
+
+ return "password"
+ }
+
+ onClicked: {
+ root.store.authenticateForEditingDerivationPath()
+ }
+ }
+
+ StatusIcon {
+ id: expandImage
+ visible: root.store.addAccountModule.actionAuthenticated
+ color: Theme.palette.baseColor1
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.state === d.expandedState) {
+ root.state = d.collapsedState
+ return
+ }
+ root.state = d.expandedState
+
+ root.state = Qt.binding(function(){ return root.store.addAccountModule.actionAuthenticated? d.expandedState : d.collapsedState })
+ }
+ }
+ }
+ }
+
+ DerivationPath {
+ id: derivationPathContent
+ width: parent.width - 2 * root.padding
+
+ store: root.store
+ }
+
+ states: [
+ State {
+ name: d.expandedState
+ PropertyChanges {target: expandImage; icon: "chevron-up"}
+ PropertyChanges {target: derivationPathContent; visible: true}
+ },
+ State {
+ name: d.collapsedState
+ PropertyChanges {target: expandImage; icon: "chevron-down"}
+ PropertyChanges {target: derivationPathContent; visible: false}
+ }
+ ]
+
+ transitions: [
+ Transition {
+ from: d.collapsedState
+ to: d.expandedState
+ NumberAnimation { properties: "height"; duration: 200;}
+ },
+ Transition {
+ from: d.expandedState
+ to: d.collapsedState
+ NumberAnimation { properties: "height"; duration: 200;}
+ }
+ ]
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPathSelection.qml b/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPathSelection.qml
new file mode 100644
index 0000000000..bcc8499663
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPathSelection.qml
@@ -0,0 +1,48 @@
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+
+import StatusQ.Core 0.1
+import StatusQ.Core.Theme 0.1
+import StatusQ.Components 0.1
+import StatusQ.Popups 0.1
+
+import utils 1.0
+
+StatusMenu {
+ id: root
+
+ implicitWidth: 285
+
+ property string selectedRootPath
+ property var roots: []
+ property var translation: function (key, isTitle) {}
+
+ signal selected(string rootPath)
+
+ contentItem: Column {
+ width: root.width
+
+ Repeater {
+ model: root.roots.length
+
+ StatusListItem {
+ width: parent.width
+ title: root.translation(root.roots[index], true)
+ subTitle: root.translation(root.roots[index], false)
+
+ components: [
+ StatusIcon {
+ visible: root.selectedRootPath === root.roots[index]
+ icon: "checkmark"
+ color: Theme.palette.primaryColor1
+ }
+ ]
+
+ onClicked: {
+ root.selected(root.roots[index])
+ root.close()
+ }
+ }
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/panels/SelectOrigin.qml b/ui/app/AppLayouts/Wallet/addaccount/panels/SelectOrigin.qml
new file mode 100644
index 0000000000..af62ffa9b6
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/panels/SelectOrigin.qml
@@ -0,0 +1,155 @@
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+
+import StatusQ.Core 0.1
+import StatusQ.Core.Theme 0.1
+import StatusQ.Core.Utils 0.1
+import StatusQ.Controls 0.1
+import StatusQ.Components 0.1
+
+import utils 1.0
+
+StatusSelect {
+ id: root
+
+ property string userProfilePublicKey
+ property var originModel
+ property var selectedOrigin
+
+ signal originSelected(string keyUid)
+
+ label: qsTr("Origin")
+
+ model: root.originModel
+
+ selectedItemComponent: StatusListItem {
+ title: Utils.appTranslation(root.selectedOrigin.name)
+ border.width: 1
+ border.color: Theme.palette.baseColor2
+
+ asset {
+ width: root.selectedOrigin.icon? 24 : 40
+ height: root.selectedOrigin.icon? 24 : 40
+ name: root.selectedOrigin.image? root.selectedOrigin.image : root.selectedOrigin.icon
+ isImage: !!root.selectedOrigin.image
+ color: root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.profile?
+ Utils.colorForPubkey(root.userProfilePublicKey) : Theme.palette.primaryColor1
+ letterSize: Math.max(4, asset.width / 2.4)
+ charactersLen: 2
+ isLetterIdenticon: !root.selectedOrigin.icon && !asset.name.toString()
+ bgColor: "transparent"
+ }
+
+ ringSettings {
+ ringSpecModel: root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.profile?
+ Utils.getColorHashAsJson(root.userProfilePublicKey) : []
+ ringPxSize: Math.max(asset.width / 24.0)
+ }
+
+ tagsModel : root.selectedOrigin.accounts
+
+ tagsDelegate: StatusListItemTag {
+ bgColor: model.account.color
+ height: Style.current.bigPadding
+ bgRadius: 6
+ tagClickable: false
+ closeButtonVisible: false
+ asset {
+ emoji: model.account.emoji
+ emojiSize: Emoji.size.verySmall
+ isLetterIdenticon: !!model.account.emoji
+ name: model.account.icon
+ color: Theme.palette.indirectColor1
+ width: 16
+ height: 16
+ }
+ title: model.account.name
+ titleText.font.pixelSize: 12
+ titleText.color: Theme.palette.indirectColor1
+ }
+ }
+ menuDelegate: StatusListItem {
+ id: menu
+
+ property bool isProfileKeypair: model.keyPair.pairType === Constants.addAccountPopup.keyPairType.profile
+ property bool isOption: model.keyPair.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey ||
+ model.keyPair.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc
+ property bool isHeader: model.keyPair.pairType === Constants.addAccountPopup.keyPairType.unknown && !menu.isOption
+
+ title: model.keyPair.pairType === Constants.addAccountPopup.keyPairType.unknown?
+ Utils.appTranslation(model.keyPair.keyUid) :
+ model.keyPair.name
+ subTitle: {
+ if (menu.isOption) {
+ if (model.keyPair.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey)
+ return qsTr("From Keycard, private key or seed phrase")
+ if (model.keyPair.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc)
+ return qsTr("Any ETH address")
+ }
+ return ""
+ }
+ enabled: !menu.isHeader && model.keyPair.pairType !== Constants.addAccountPopup.keyPairType.privateKeyImport
+
+ asset {
+ width: model.keyPair.icon? 24 : 40
+ height: model.keyPair.icon? 24 : 40
+ name: model.keyPair.image? model.keyPair.image : model.keyPair.icon
+ isImage: !!model.keyPair.image
+ color: menu.isProfileKeypair? Utils.colorForPubkey(root.userProfilePublicKey) : Theme.palette.baseColor1
+ letterSize: Math.max(4, asset.width / 2.4)
+ charactersLen: 2
+ isLetterIdenticon: !menu.isHeader && !model.keyPair.icon && !asset.name.toString()
+ bgColor: "transparent"
+ }
+
+ ringSettings {
+ ringSpecModel: menu.isProfileKeypair? Utils.getColorHashAsJson(root.userProfilePublicKey) : []
+ ringPxSize: Math.max(asset.width / 24.0)
+ }
+
+ tagsModel: menu.isHeader || menu.isOption? [] : model.keyPair.accounts
+
+ tagsDelegate: StatusListItemTag {
+ bgColor: model.account.color
+ height: Style.current.bigPadding
+ bgRadius: 6
+ tagClickable: false
+ closeButtonVisible: false
+ asset {
+ emoji: model.account.emoji
+ emojiSize: Emoji.size.verySmall
+ isLetterIdenticon: !!model.account.emoji
+ name: model.account.icon
+ color: Theme.palette.indirectColor1
+ width: 16
+ height: 16
+ }
+ title: model.account.name
+ titleText.font.pixelSize: 12
+ titleText.color: Theme.palette.indirectColor1
+ }
+
+ components: [
+ StatusIcon {
+ visible: icon != ""
+ icon: {
+ if (menu.isOption) {
+ return "tiny/chevron-right"
+ }
+
+ if (!menu.isHeader && model.keyPair.name === root.selectedOrigin.name) {
+ return "checkmark"
+ }
+
+ return ""
+ }
+ color: menu.isOption? Theme.palette.baseColor1 : Theme.palette.primaryColor1
+ }
+ ]
+
+ onClicked: {
+ root.originSelected(model.keyPair.keyUid)
+ root.selectMenu.close()
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/panels/WatchOnlyAddressSection.qml b/ui/app/AppLayouts/Wallet/addaccount/panels/WatchOnlyAddressSection.qml
new file mode 100644
index 0000000000..9341c68782
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/panels/WatchOnlyAddressSection.qml
@@ -0,0 +1,65 @@
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+
+import StatusQ.Core 0.1
+import StatusQ.Core.Theme 0.1
+import StatusQ.Controls 0.1
+import StatusQ.Controls.Validators 0.1
+
+import utils 1.0
+
+import "../stores"
+
+Column {
+ id: root
+
+ property AddAccountStore store
+
+ function reset() {
+ addressInput.reset()
+ }
+
+ StatusInput {
+ id: addressInput
+ width: parent.width
+ maximumHeight: Constants.addAccountPopup.itemHeight
+ minimumHeight: Constants.addAccountPopup.itemHeight
+ label: qsTr("Ethereum address or ENS name")
+ placeholderText: "e.g.0x95222293DD7278Aa3Cdd389Cc1D1d165CCBAfe5"
+ input.multiline: true
+ input.rightComponent: StatusButton {
+ anchors.verticalCenter: parent.verticalCenter
+ borderColor: Theme.palette.primaryColor1
+ size: StatusBaseButton.Size.Tiny
+ text: qsTr("Paste")
+ onClicked: {
+ addressInput.text = ""
+ addressInput.input.edit.paste()
+ }
+ }
+ validators: [
+ StatusAddressOrEnsValidator {
+ errorMessage: qsTr("Please enter a valid Ethereum address or ENS name")
+ }
+ ]
+
+ onTextChanged: {
+ if (addressInput.valid) {
+ root.store.changeWatchOnlyAccountAddressPostponed(text.trim())
+ return
+ }
+ root.store.cleanWatchOnlyAccountAddress()
+ }
+
+ onKeyPressed: {
+ root.store.submitAddAccount(event)
+ }
+ }
+
+ AddressDetails {
+ width: parent.width
+ addressDetailsItem: root.store.watchOnlyAccAddress
+ defaultMessage: qsTr("You will need to import your seed phrase or use your Keycard to transact with this account")
+ defaultMessageCondition: addressInput.text === "" || !addressInput.valid
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/states/ConfirmAddingNewMasterKey.qml b/ui/app/AppLayouts/Wallet/addaccount/states/ConfirmAddingNewMasterKey.qml
new file mode 100644
index 0000000000..fc2b9a7541
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/states/ConfirmAddingNewMasterKey.qml
@@ -0,0 +1,129 @@
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+import QtQuick.Layouts 1.14
+
+import StatusQ.Controls 0.1
+import StatusQ.Core.Theme 0.1
+import StatusQ.Core 0.1
+
+import utils 1.0
+
+import "../stores"
+
+Item {
+ id: root
+
+ property AddAccountStore store
+
+ implicitHeight: layout.implicitHeight
+
+ Component.onCompleted: {
+ if (root.store.addingNewMasterKeyConfirmed) {
+ havePen.checked = true
+ writeDown.checked = true
+ storeIt.checked = true
+ }
+ }
+
+ QtObject {
+ id: d
+ readonly property int width1: layout.width - 2 * Style.current.padding
+ readonly property int width2: d.width1 - 2 * Style.current.padding
+ readonly property int checkboxHeight: 24
+ readonly property real lineHeight: 1.2
+
+ readonly property bool allAccepted: havePen.checked && writeDown.checked && storeIt.checked
+ onAllAcceptedChanged: {
+ root.store.addingNewMasterKeyConfirmed = allAccepted
+ }
+ }
+
+ ColumnLayout {
+ id: layout
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width - 2 * Style.current.padding
+ spacing: Style.current.padding
+
+ Image {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: Style.current.padding
+ Layout.preferredWidth: 120
+ Layout.preferredHeight: 120
+ fillMode: Image.PreserveAspectFit
+ source: Style.png("onboarding/keys")
+ mipmap: true
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: d.width1
+ Layout.alignment: Qt.AlignHCenter
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ font.bold: true
+ font.pixelSize: 22
+ text: qsTr("Secure Your Assets and Funds")
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: d.width1
+ Layout.alignment: Qt.AlignHCenter
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ textFormat: Text.RichText
+ font.pixelSize: Style.current.primaryTextFontSize
+ lineHeight: d.lineHeight
+ text: qsTr("Your seed phrase is a 12-word passcode to your funds.
Your seed phrase cannot be recovered if lost. Therefore, you must back it up. The simplest way is to write it down offline and store it somewhere secure.")
+ }
+
+ StatusCheckBox {
+ id: havePen
+ Layout.preferredWidth: d.width2
+ Layout.preferredHeight: d.checkboxHeight
+ Layout.topMargin: Style.current.padding
+ Layout.alignment: Qt.AlignHCenter
+ spacing: Style.current.padding
+ font.pixelSize: Style.current.primaryTextFontSize
+ text: qsTr("I have a pen and paper")
+ }
+
+ StatusCheckBox {
+ id: writeDown
+ Layout.preferredWidth: d.width2
+ Layout.preferredHeight: d.checkboxHeight
+ Layout.alignment: Qt.AlignHCenter
+ spacing: Style.current.padding
+ font.pixelSize: Style.current.primaryTextFontSize
+ text: qsTr("I am ready to write down my seed phrase")
+ }
+
+ StatusCheckBox {
+ id: storeIt
+ Layout.preferredWidth: d.width2
+ Layout.preferredHeight: d.checkboxHeight
+ Layout.alignment: Qt.AlignHCenter
+ spacing: Style.current.padding
+ font.pixelSize: Style.current.primaryTextFontSize
+ text: qsTr("I know where I’ll store it")
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 60
+ Layout.topMargin: Style.current.padding
+ radius: Style.current.radius
+ color: Theme.palette.dangerColor3
+
+ StatusBaseText {
+ anchors.fill: parent
+ anchors.margins: Style.current.halfPadding
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ font.pixelSize: Style.current.primaryTextFontSize
+ wrapMode: Text.WordWrap
+ color: Theme.palette.dangerColor1
+ lineHeight: d.lineHeight
+ text: qsTr("You can only complete this process once. Status will not store your seed phrase and can never help you recover it.")
+ }
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/states/ConfirmSeedPhraseBackup.qml b/ui/app/AppLayouts/Wallet/addaccount/states/ConfirmSeedPhraseBackup.qml
new file mode 100644
index 0000000000..64c6128df0
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/states/ConfirmSeedPhraseBackup.qml
@@ -0,0 +1,92 @@
+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.Controls 0.1
+import StatusQ.Components 0.1
+
+import utils 1.0
+
+import "../stores"
+
+Item {
+ id: root
+
+ property AddAccountStore store
+
+ implicitHeight: layout.implicitHeight
+
+ Component.onCompleted: {
+ if (root.store.seedPhraseBackupConfirmed) {
+ aknowledge.checked = true
+ }
+ }
+
+ ColumnLayout {
+ id: layout
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width - 2 * 3 * Style.current.padding
+ spacing: Style.current.halfPadding
+
+ StatusStepper {
+ Layout.preferredWidth: Constants.addAccountPopup.stepperWidth
+ Layout.preferredHeight: Constants.addAccountPopup.stepperHeight
+ Layout.topMargin: Style.current.padding
+ Layout.alignment: Qt.AlignCenter
+ title: qsTr("Step 4 of 4")
+ titleFontSize: Constants.addAccountPopup.labelFontSize1
+ totalSteps: 4
+ completedSteps: 4
+ leftPadding: 0
+ rightPadding: 0
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: parent.width
+ Layout.alignment: Qt.AlignCenter
+ horizontalAlignment: Text.AlignHCenter
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ color: Theme.palette.directColor1
+ text: qsTr("Complete back up")
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: parent.width
+ Layout.topMargin: 2 * Style.current.xlPadding
+ Layout.alignment: Qt.AlignHCenter
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ font.bold: true
+ font.pixelSize: 18
+ color: Theme.palette.directColor1
+ text: qsTr("Store Your Phrase Offline and Complete Your Back Up")
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: parent.width
+ Layout.topMargin: Style.current.halfPadding
+ Layout.alignment: Qt.AlignHCenter
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ font.pixelSize: Style.current.primaryTextFontSize
+ lineHeight: 1.2
+ color: Theme.palette.directColor1
+ text: qsTr("By completing this process, you will remove your seed phrase from this application’s storage. This makes your funds more secure.\n\nYou will remain logged in, and your seed phrase will be entirely in your hands.")
+ }
+
+ StatusCheckBox {
+ id: aknowledge
+ Layout.preferredWidth: parent.width
+ Layout.topMargin: 2 * Style.current.xlPadding
+ Layout.alignment: Qt.AlignHCenter
+ spacing: Style.current.padding
+ font.pixelSize: Style.current.primaryTextFontSize
+ text: qsTr("I aknowledge that Status will not be able to show me my seed phrase again.")
+ onToggled: {
+ root.store.seedPhraseBackupConfirmed = checked
+ }
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/states/DisplaySeedPhrase.qml b/ui/app/AppLayouts/Wallet/addaccount/states/DisplaySeedPhrase.qml
new file mode 100644
index 0000000000..789edbb613
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/states/DisplaySeedPhrase.qml
@@ -0,0 +1,75 @@
+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 shared.panels 1.0
+
+import "../stores"
+
+Item {
+ id: root
+
+ property AddAccountStore store
+
+ implicitHeight: layout.implicitHeight
+
+ ColumnLayout {
+ id: layout
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width - 2 * Style.current.padding
+ spacing: Style.current.halfPadding
+
+ StatusStepper {
+ Layout.preferredWidth: Constants.addAccountPopup.stepperWidth
+ Layout.preferredHeight: Constants.addAccountPopup.stepperHeight
+ Layout.topMargin: Style.current.padding
+ Layout.alignment: Qt.AlignCenter
+ title: qsTr("Step 1 of 4")
+ titleFontSize: Constants.addAccountPopup.labelFontSize1
+ totalSteps: 4
+ completedSteps: 1
+ leftPadding: 0
+ rightPadding: 0
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: parent.width
+ Layout.alignment: Qt.AlignCenter
+ horizontalAlignment: Text.AlignHCenter
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ color: Theme.palette.directColor1
+ text: qsTr("Write down your 12-word seed phrase to keep offline")
+ }
+
+ SeedPhrase {
+ Layout.preferredWidth: parent.width
+ Layout.preferredHeight: 304
+ Layout.topMargin: 3 * Style.current.padding
+ seedPhraseRevealed: root.store.seedPhraseRevealed
+
+ seedPhrase: root.store.getSeedPhrase().split(" ")
+
+ onSeedPhraseRevealedChanged: {
+ root.store.seedPhraseRevealed = seedPhraseRevealed
+ }
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: parent.width
+ Layout.topMargin: 2 * Style.current.padding
+ Layout.alignment: Qt.AlignCenter
+ horizontalAlignment: Text.AlignHCenter
+ visible: !root.store.seedPhraseRevealed
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ textFormat: Text.RichText
+ wrapMode: Text.WordWrap
+ color: Theme.palette.dangerColor1
+ text: qsTr("The next screen contains your seed phrase.
Anyone who sees it can use it to access to your funds.")
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/states/EnterKeypairName.qml b/ui/app/AppLayouts/Wallet/addaccount/states/EnterKeypairName.qml
new file mode 100644
index 0000000000..dcd410cb34
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/states/EnterKeypairName.qml
@@ -0,0 +1,53 @@
+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.Controls 0.1
+
+import utils 1.0
+
+import "../stores"
+
+Item {
+ id: root
+
+ property AddAccountStore store
+
+ implicitHeight: layout.implicitHeight
+
+ ColumnLayout {
+ id: layout
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width - 2 * Style.current.padding
+ spacing: Style.current.halfPadding
+
+ StatusInput {
+ Layout.preferredWidth: parent.width
+ Layout.topMargin: Style.current.padding
+ label: qsTr("Key name")
+ charLimit: Constants.addAccountPopup.keyPairNameMaxLength
+ placeholderText: qsTr("Enter a name")
+ text: root.store.addAccountModule.newKeyPairName
+
+ onTextChanged: {
+ if (text.trim() == "") {
+ root.store.addAccountModule.newKeyPairName = ""
+ return
+ }
+ root.store.addAccountModule.newKeyPairName = text
+ }
+
+ onKeyPressed: {
+ root.store.submitAddAccount(event)
+ }
+ }
+
+ StatusBaseText {
+ text: qsTr("For your future reference. This is only visible to you.")
+ font.pixelSize: Constants.addAccountPopup.labelFontSize2
+ color: Theme.palette.baseColor1
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/states/EnterPrivateKey.qml b/ui/app/AppLayouts/Wallet/addaccount/states/EnterPrivateKey.qml
new file mode 100644
index 0000000000..58b801b068
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/states/EnterPrivateKey.qml
@@ -0,0 +1,171 @@
+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.Controls 0.1
+import StatusQ.Controls.Validators 0.1
+import StatusQ.Components 0.1
+import StatusQ.Popups 0.1
+
+import utils 1.0
+
+import "../stores"
+import "../panels"
+
+Item {
+ id: root
+
+ property AddAccountStore store
+
+ QtObject {
+ id: d
+
+ property bool showPassword: false
+ property bool addressResolved: root.store.privateKeyAccAddress.address !== ""
+ }
+
+ Column {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Style.current.padding
+ spacing: Style.current.padding
+
+ Column {
+ width: parent.width
+ spacing: Style.current.halfPadding
+
+ StatusBaseText {
+ text: qsTr("Private key")
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ }
+
+ GridLayout {
+ width: parent.width
+ columns: 2
+ columnSpacing: Style.current.padding
+ rowSpacing: Style.current.halfPadding
+
+ StatusPasswordInput {
+ id: privKeyInput
+ Layout.preferredHeight: Constants.addAccountPopup.itemHeight
+ Layout.preferredWidth: parent.width - parent.columnSpacing - showHideButton.width
+ rightPadding: pasteButton.width + pasteButton.anchors.rightMargin + Style.current.halfPadding
+ placeholderText: qsTr("Type or paste your private key")
+ echoMode: d.showPassword ? TextInput.Normal : TextInput.Password
+
+ onTextChanged: {
+ root.store.enteredPrivateKeyIsValid = Utils.isPrivateKey(text)
+ if (root.store.enteredPrivateKeyIsValid) {
+ root.store.changePrivateKeyPostponed(text)
+ return
+ }
+ root.store.cleanPrivateKey()
+ }
+
+ onPressed: {
+ root.store.submitAddAccount(event)
+ }
+
+ StatusButton {
+ id: pasteButton
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+ anchors.rightMargin: Style.current.padding
+ borderColor: Theme.palette.primaryColor1
+ size: StatusBaseButton.Size.Tiny
+ text: qsTr("Paste")
+ onClicked: {
+ privKeyInput.text = root.store.getFromClipboard()
+ }
+ }
+ }
+
+ StatusFlatRoundButton {
+ id: showHideButton
+ Layout.alignment: Qt.AlignVCenter
+ Layout.preferredWidth: 24
+ Layout.preferredHeight: 24
+ icon.name: d.showPassword ? "hide" : "show"
+ icon.color: Theme.palette.baseColor1
+ onClicked: d.showPassword = !d.showPassword
+ }
+
+ StatusBaseText {
+ Layout.alignment: Qt.AlignRight
+ visible: privKeyInput.text !== "" && !root.store.enteredPrivateKeyIsValid
+ wrapMode: Text.WordWrap
+ font.pixelSize: 12
+ color: Theme.palette.dangerColor1
+ text: qsTr("Private key invalid")
+ }
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: Style.current.halfPadding
+ visible: d.addressResolved
+
+ StatusBaseText {
+ text: qsTr("Public address of private key")
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ }
+
+ StatusInput {
+ width: parent.width
+ input.edit.enabled: false
+ text: root.store.privateKeyAccAddress.address
+ input.enabled: false
+ input.background.color: "transparent"
+ input.background.border.color: Theme.palette.baseColor2
+ }
+
+ AddressDetails {
+ width: parent.width
+ addressDetailsItem: root.store.privateKeyAccAddress
+ defaultMessage: ""
+ defaultMessageCondition: !d.addressResolved
+ }
+ }
+
+ StatusModalDivider {
+ width: parent.width
+ visible: d.addressResolved
+ }
+
+ Column {
+ width: parent.width
+ spacing: Style.current.halfPadding
+ visible: d.addressResolved
+
+ StatusInput {
+ width: parent.width
+ label: qsTr("Key name")
+ charLimit: Constants.addAccountPopup.keyPairNameMaxLength
+ placeholderText: qsTr("Enter a name")
+ text: root.store.addAccountModule.newKeyPairName
+
+ onTextChanged: {
+ if (text.trim() == "") {
+ root.store.addAccountModule.newKeyPairName = ""
+ return
+ }
+ root.store.addAccountModule.newKeyPairName = text
+ }
+
+ onKeyPressed: {
+ root.store.submitAddAccount(event)
+ }
+ }
+
+ StatusBaseText {
+ text: qsTr("For your future reference. This is only visible to you.")
+ font.pixelSize: Constants.addAccountPopup.labelFontSize2
+ color: Theme.palette.baseColor1
+ }
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/states/EnterSeedPhrase.qml b/ui/app/AppLayouts/Wallet/addaccount/states/EnterSeedPhrase.qml
new file mode 100644
index 0000000000..ab3960900f
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/states/EnterSeedPhrase.qml
@@ -0,0 +1,96 @@
+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.Controls 0.1
+import StatusQ.Controls.Validators 0.1
+import StatusQ.Components 0.1
+import StatusQ.Popups 0.1
+
+import utils 1.0
+import shared.panels 1.0 as SharedPanels
+
+import "../stores"
+import "../panels"
+
+Item {
+ id: root
+
+ property AddAccountStore store
+
+ Column {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Style.current.padding
+ spacing: Style.current.padding
+
+
+ StatusBaseText {
+ text: qsTr("Enter seed phrase")
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ }
+
+ SharedPanels.EnterSeedPhrase {
+ id: enterSeedPhrase
+ width: parent.width
+
+ isSeedPhraseValid: function(mnemonic) {
+ return root.store.validSeedPhrase(mnemonic)
+ }
+
+ onSeedPhraseUpdated: {
+ if (valid) {
+ root.store.changeSeedPhrase(seedPhrase)
+ }
+ root.store.enteredSeedPhraseIsValid = valid
+ if (!enterSeedPhrase.isSeedPhraseValid(seedPhrase)) {
+ enterSeedPhrase.setWrongSeedPhraseMessage(qsTr("The entered seed phrase is already added"))
+ }
+ }
+
+ onSubmitSeedPhrase: {
+ root.store.submitAddAccount()
+ }
+ }
+
+ StatusModalDivider {
+ width: parent.width
+ visible: root.store.enteredSeedPhraseIsValid
+ }
+
+ Column {
+ width: parent.width
+ spacing: Style.current.halfPadding
+ visible: root.store.enteredSeedPhraseIsValid
+
+ StatusInput {
+ width: parent.width
+ label: qsTr("Key name")
+ charLimit: Constants.addAccountPopup.keyPairNameMaxLength
+ placeholderText: qsTr("Enter a name")
+ text: root.store.addAccountModule.newKeyPairName
+
+ onTextChanged: {
+ if (text.trim() == "") {
+ root.store.addAccountModule.newKeyPairName = ""
+ return
+ }
+ root.store.addAccountModule.newKeyPairName = text
+ }
+
+ onKeyPressed: {
+ root.store.submitAddAccount(event)
+ }
+ }
+
+ StatusBaseText {
+ text: qsTr("For your future reference. This is only visible to you.")
+ font.pixelSize: Constants.addAccountPopup.labelFontSize2
+ color: Theme.palette.baseColor1
+ }
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/states/EnterSeedPhraseWord.qml b/ui/app/AppLayouts/Wallet/addaccount/states/EnterSeedPhraseWord.qml
new file mode 100644
index 0000000000..2448472bf3
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/states/EnterSeedPhraseWord.qml
@@ -0,0 +1,157 @@
+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.Controls 0.1
+import StatusQ.Controls.Validators 0.1
+import StatusQ.Components 0.1
+
+import utils 1.0
+
+import "../stores"
+
+Item {
+ id: root
+
+ property AddAccountStore store
+
+ implicitHeight: layout.implicitHeight
+ state: root.store.currentState.stateType
+
+ Component.onCompleted: {
+ if (root.store.seedPhraseWord1WordNumber === -1) {
+ let randomNo1 = Math.floor(Math.random() * 12)
+ let randomNo2 = randomNo1
+ while (randomNo2 == randomNo1) {
+ randomNo2 = Math.floor(Math.random() * 12)
+ }
+ if (randomNo1 < randomNo2) {
+ root.store.seedPhraseWord1WordNumber = randomNo1
+ root.store.seedPhraseWord2WordNumber = randomNo2
+ }
+ else {
+ root.store.seedPhraseWord1WordNumber = randomNo2
+ root.store.seedPhraseWord2WordNumber = randomNo1
+ }
+ }
+ }
+
+ onStateChanged: {
+ d.updateEntry()
+ }
+
+ QtObject {
+ id: d
+
+ readonly property int step: root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1? 2 : 3
+ readonly property var seedPhrase: root.store.getSeedPhrase().split(" ")
+
+ function updateEntry() {
+ if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1) {
+ if (!root.store.seedPhraseWord1Valid) {
+ if (word.text.trim() !== "") {
+ word.reset()
+ }
+ }
+ else if (root.store.seedPhraseWord1WordNumber >= -1 && root.store.seedPhraseWord1WordNumber < d.seedPhrase.length) {
+ word.text = d.seedPhrase[root.store.seedPhraseWord1WordNumber]
+ }
+ }
+ else {
+ if (!root.store.seedPhraseWord2Valid) {
+ if (word.text.trim() !== "") {
+ word.reset()
+ }
+ }
+ else if (root.store.seedPhraseWord2WordNumber >= -1 && root.store.seedPhraseWord2WordNumber < d.seedPhrase.length) {
+ word.text = d.seedPhrase[root.store.seedPhraseWord2WordNumber]
+ }
+ }
+
+ word.validate()
+ }
+
+ function processText(text) {
+ if(text.length === 0)
+ return ""
+ if(/(^\s|^\r|^\n)|(\s$|^\r$|^\n$)/.test(text)) {
+ return text.trim()
+ }
+ else if(/\s|\r|\n/.test(text)) {
+ return ""
+ }
+ return text
+ }
+ }
+
+ ColumnLayout {
+ id: layout
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width - 2 * Style.current.padding
+ spacing: Style.current.halfPadding
+
+ StatusStepper {
+ Layout.preferredWidth: Constants.addAccountPopup.stepperWidth
+ Layout.preferredHeight: Constants.addAccountPopup.stepperHeight
+ Layout.topMargin: Style.current.padding
+ Layout.alignment: Qt.AlignCenter
+ title: qsTr("Step %1 of 4").arg(d.step)
+ titleFontSize: Constants.addAccountPopup.labelFontSize1
+ totalSteps: 4
+ completedSteps: d.step
+ leftPadding: 0
+ rightPadding: 0
+ }
+
+ StatusBaseText {
+ Layout.preferredWidth: parent.width
+ Layout.alignment: Qt.AlignCenter
+ horizontalAlignment: Text.AlignHCenter
+ font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ color: Theme.palette.directColor1
+ text: qsTr("Confirm word #%1 of your seed phrase").arg(root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1?
+ root.store.seedPhraseWord1WordNumber + 1 :
+ root.store.seedPhraseWord2WordNumber + 1)
+ }
+
+ StatusInput {
+ id: word
+ Layout.fillWidth: true
+ Layout.topMargin: Style.current.xlPadding
+ validationMode: StatusInput.ValidationMode.Always
+ label: qsTr("Word #%1").arg(root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1?
+ root.store.seedPhraseWord1WordNumber + 1 :
+ root.store.seedPhraseWord2WordNumber + 1)
+ placeholderText: qsTr("Enter word")
+ validators: [
+ StatusValidator {
+ validate: function (t) {
+ if (!d.seedPhrase || d.seedPhrase.length === 0 || word.text.length === 0)
+ return false
+ if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1) {
+ return (d.seedPhrase[root.store.seedPhraseWord1WordNumber] === word.text)
+ }
+ return (d.seedPhrase[root.store.seedPhraseWord2WordNumber] === word.text)
+ }
+ errorMessage: (word.text.length) > 0 ? qsTr("Incorrect word") : ""
+ }
+ ]
+ input.tabNavItem: word.input.edit
+
+ onTextChanged: {
+ text = d.processText(text)
+ if (root.store.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1) {
+ root.store.seedPhraseWord1Valid = word.valid
+ return
+ }
+ root.store.seedPhraseWord2Valid = word.valid
+ }
+
+ onKeyPressed: {
+ root.store.submitAddAccount(event)
+ }
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/states/Main.qml b/ui/app/AppLayouts/Wallet/addaccount/states/Main.qml
new file mode 100644
index 0000000000..4a2e2c485e
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/states/Main.qml
@@ -0,0 +1,181 @@
+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.Core.Utils 0.1 as StatusQUtils
+import StatusQ.Controls 0.1
+import StatusQ.Controls.Validators 0.1
+
+import utils 1.0
+
+import "../stores"
+import "../panels"
+
+Item {
+ id: root
+
+ property AddAccountStore store
+
+ implicitHeight: layout.implicitHeight
+
+ Component.onCompleted: {
+ if (root.store.addAccountModule.selectedColor === "") {
+ colorSelection.selectedColorIndex = Math.floor(Math.random() * colorSelection.model.length)
+ }
+ else {
+ let ind = d.evaluateColorIndex(root.store.addAccountModule.selectedColor)
+ colorSelection.selectedColorIndex = ind
+ }
+
+ if (root.store.addAccountModule.selectedEmoji === "") {
+ root.store.addAccountModule.selectedEmoji = StatusQUtils.Emoji.getRandomEmoji(StatusQUtils.Emoji.size.verySmall)
+ }
+
+ accountName.text = root.store.addAccountModule.accountName
+ accountName.input.edit.forceActiveFocus()
+ }
+
+ QtObject {
+ id: d
+
+ function evaluateColorIndex(color) {
+ for (let i = 0; i < Constants.preDefinedWalletAccountColors.length; i++) {
+ if(Constants.preDefinedWalletAccountColors[i] === color) {
+ return i
+ }
+ }
+ return 0
+ }
+ }
+
+ Connections {
+ target: root.store.emojiPopup
+
+ function onEmojiSelected (emojiText, atCursor) {
+ root.store.addAccountModule.selectedEmoji = emojiText
+ }
+ }
+
+ Component {
+ id: spacer
+ Rectangle {
+ color: Theme.palette.baseColor4
+ }
+ }
+
+ ColumnLayout {
+ id: layout
+ width: parent.width
+ spacing: 0
+
+ Loader {
+ Layout.preferredHeight: Style.current.padding
+ Layout.fillWidth: true
+ sourceComponent: spacer
+ }
+
+ Column {
+ Layout.fillWidth: true
+ spacing: Style.current.padding
+ topPadding: Style.current.padding
+ bottomPadding: Style.current.padding
+
+ StatusInput {
+ id: accountName
+ anchors.horizontalCenter: parent.horizontalCenter
+ placeholderText: qsTr("Enter an account name...")
+ label: qsTr("Name")
+ text: root.store.addAccountModule.accountName
+ input.isIconSelectable: true
+ input.leftPadding: Style.current.padding
+ input.asset.color: root.store.addAccountModule.selectedColor
+ input.asset.emoji: root.store.addAccountModule.selectedEmoji
+ onIconClicked: {
+ if (!root.store.emojiPopup) {
+ return
+ }
+ let inputCoords = accountName.mapToItem(appMain, 0, 0)
+ root.store.emojiPopup.open()
+ root.store.emojiPopup.emojiSize = StatusQUtils.Emoji.size.verySmall
+ root.store.emojiPopup.x = inputCoords.x
+ root.store.emojiPopup.y = inputCoords.y + accountName.height + Style.current.halfPadding
+ }
+
+ onTextChanged: {
+ root.store.addAccountModule.accountName = text
+ }
+
+ onKeyPressed: {
+ root.store.submitAddAccount(event)
+ }
+ }
+
+ StatusColorSelectorGrid {
+ id: colorSelection
+ anchors.horizontalCenter: parent.horizontalCenter
+ model: Constants.preDefinedWalletAccountColors
+ title.color: Theme.palette.directColor1
+ title.font.pixelSize: Constants.addAccountPopup.labelFontSize1
+ title.text: qsTr("Colour")
+ selectedColorIndex: -1
+
+ onSelectedColorChanged: {
+ root.store.addAccountModule.selectedColor = selectedColor
+ }
+ }
+
+ SelectOrigin {
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ userProfilePublicKey: root.store.userProfilePublicKey
+ originModel: root.store.originModel
+ selectedOrigin: root.store.selectedOrigin
+
+ onOriginSelected: {
+ if (keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey) {
+ root.store.currentState.doSecondaryAction()
+ return
+ }
+ root.store.changeSelectedOrigin(keyUid)
+ }
+ }
+
+ WatchOnlyAddressSection {
+ width: parent.width - 2 * Style.current.padding
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Style.current.padding
+ visible: root.store.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.unknown &&
+ root.store.selectedOrigin.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc
+
+ store: root.store
+
+ onVisibleChanged: {
+ reset()
+ }
+ }
+ }
+
+ Loader {
+ Layout.preferredHeight: Style.current.padding
+ Layout.fillWidth: true
+ sourceComponent: spacer
+ }
+
+ DerivationPathSection {
+ id: derivationPathSection
+ Layout.fillWidth: true
+ visible: root.store.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.profile ||
+ root.store.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.seedImport
+ store: root.store
+ }
+
+ Loader {
+ Layout.preferredHeight: Style.current.padding
+ Layout.fillWidth: true
+ visible: derivationPathSection.visible
+ sourceComponent: spacer
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/states/SelectMasterKey.qml b/ui/app/AppLayouts/Wallet/addaccount/states/SelectMasterKey.qml
new file mode 100644
index 0000000000..f6ae74c248
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/states/SelectMasterKey.qml
@@ -0,0 +1,114 @@
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+
+import StatusQ.Core 0.1
+import StatusQ.Core.Theme 0.1
+import StatusQ.Controls 0.1
+import StatusQ.Components 0.1
+import StatusQ.Popups 0.1
+
+import utils 1.0
+
+import "../stores"
+
+Item {
+ id: root
+
+ property AddAccountStore store
+
+ Column {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Style.current.padding
+
+ StatusListItem {
+ title: qsTr("Add new master key")
+ enabled: false
+ }
+
+ StatusListItem {
+ title: qsTr("Import using seed phrase")
+ asset {
+ name: "key_pair_seed_phrase"
+ color: Theme.palette.primaryColor1
+ bgColor: Theme.palette.primaryColor3
+ }
+ components: [
+ StatusIcon {
+ icon: "tiny/chevron-right"
+ color: Theme.palette.baseColor1
+ }
+ ]
+
+ onClicked: {
+ root.store.cleanSeedPhrase()
+ root.store.currentState.doPrimaryAction()
+ }
+ }
+
+ StatusListItem {
+ title: qsTr("Import private key")
+ asset {
+ name: "objects"
+ color: Theme.palette.primaryColor1
+ bgColor: Theme.palette.primaryColor3
+ }
+ components: [
+ StatusIcon {
+ icon: "tiny/chevron-right"
+ color: Theme.palette.baseColor1
+ }
+ ]
+
+ onClicked: {
+ root.store.cleanPrivateKey()
+ root.store.currentState.doSecondaryAction()
+ }
+ }
+
+ StatusListItem {
+ title: qsTr("Generate new master key")
+ asset {
+ name: "objects"
+ color: Theme.palette.primaryColor1
+ bgColor: Theme.palette.primaryColor3
+ }
+ components: [
+ StatusIcon {
+ icon: "tiny/chevron-right"
+ color: Theme.palette.baseColor1
+ }
+ ]
+
+ onClicked: {
+ root.store.resetStoreValues()
+ root.store.currentState.doTertiaryAction()
+ }
+ }
+
+ StatusModalDivider {
+ width: parent.width
+ }
+
+ StatusListItem {
+ title: qsTr("Use Keycard")
+ sensor.enabled: false
+ sensor.hoverEnabled: false
+ statusListItemIcon.enabled: false
+ statusListItemIcon.hoverEnabled: false
+ asset {
+ name: "keycard"
+ color: Theme.palette.primaryColor1
+ bgColor: Theme.palette.primaryColor3
+ }
+ components: [
+ StatusButton {
+ text: qsTr("Continue in Keycard settings")
+ onClicked: {
+ Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.keycard)
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/stores/AddAccountStore.qml b/ui/app/AppLayouts/Wallet/addaccount/stores/AddAccountStore.qml
new file mode 100644
index 0000000000..7fe59a8298
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/addaccount/stores/AddAccountStore.qml
@@ -0,0 +1,308 @@
+import QtQuick 2.13
+import utils 1.0
+
+QtObject {
+ id: root
+
+ property var addAccountModule
+
+ property var emojiPopup: null
+ property string userProfilePublicKey: userProfile.pubKey
+ property string userProfileKeyUid: userProfile.keyUid
+ property bool userProfileIsKeycardUser: userProfile.isKeycardUser
+ property bool userProfileUsingBiometricLogin: userProfile.usingBiometricLogin
+
+ // Module Properties
+ property var currentState: root.addAccountModule? root.addAccountModule.currentState : null
+ property var originModel: root.addAccountModule? root.addAccountModule.originModel : []
+ property var selectedOrigin: root.addAccountModule? root.addAccountModule.selectedOrigin : null
+ property var derivedAddressModel: root.addAccountModule? root.addAccountModule.derivedAddressModel : []
+ property var selectedDerivedAddress: root.addAccountModule? root.addAccountModule.selectedDerivedAddress : null
+ property var watchOnlyAccAddress: root.addAccountModule? root.addAccountModule.watchOnlyAccAddress : null
+ property var privateKeyAccAddress: root.addAccountModule? root.addAccountModule.privateKeyAccAddress : null
+ property bool disablePopup: root.addAccountModule? root.addAccountModule.disablePopup : false
+
+ property bool enteredSeedPhraseIsValid: false
+ property bool enteredPrivateKeyIsValid: false
+ property bool addingNewMasterKeyConfirmed: false
+ property bool seedPhraseRevealed: false
+ property bool seedPhraseWord1Valid: false
+ property int seedPhraseWord1WordNumber: -1
+ property bool seedPhraseWord2Valid: false
+ property int seedPhraseWord2WordNumber: -1
+ property bool seedPhraseBackupConfirmed: false
+ property bool derivationPathOutOfTheDefaultStatusDerivationTreeConfirmed: false
+ property bool derivationPathOutOfTheDefaultStatusDerivationTree: root.addAccountModule?
+ !root.addAccountModule.derivationPath.startsWith(Constants.addAccountPopup.predefinedPaths.ethereum) ||
+ (root.addAccountModule.derivationPath.match(/'/g) || []).length !== 3 ||
+ (root.addAccountModule.derivationPath.match(/\//g) || []).length !== 5
+ : false
+
+ readonly property var derivationPathRegEx: /^(m\/44'\/)([0-9|'|\/](?!\/'))*$/
+ property string selectedRootPath: Constants.addAccountPopup.predefinedPaths.ethereum
+ readonly property var roots: [Constants.addAccountPopup.predefinedPaths.custom,
+ Constants.addAccountPopup.predefinedPaths.ethereum,
+ Constants.addAccountPopup.predefinedPaths.ethereumRopsten,
+ Constants.addAccountPopup.predefinedPaths.ethereumLedger,
+ Constants.addAccountPopup.predefinedPaths.ethereumLedgerLive
+ ]
+
+ function resetStoreValues() {
+ root.enteredSeedPhraseIsValid = false
+ root.enteredPrivateKeyIsValid = false
+ root.addingNewMasterKeyConfirmed = false
+ root.seedPhraseRevealed = false
+ root.seedPhraseWord1Valid = false
+ root.seedPhraseWord1WordNumber = -1
+ root.seedPhraseWord2Valid = false
+ root.seedPhraseWord2WordNumber = -1
+ root.seedPhraseBackupConfirmed = false
+ root.derivationPathOutOfTheDefaultStatusDerivationTreeConfirmed = false
+ root.selectedRootPath = Constants.addAccountPopup.predefinedPaths.ethereum
+
+ root.cleanPrivateKey()
+ root.cleanSeedPhrase()
+ }
+
+ function moduleInitialized() {
+ if (!root.addAccountModule) {
+ console.warn("addAccountModule not initialized")
+ return false
+ }
+ return true
+ }
+
+ function submitAddAccount(event) {
+ if (!root.moduleInitialized() || !root.primaryPopupButtonEnabled) {
+ return
+ }
+
+ if(!event) {
+ root.currentState.doPrimaryAction()
+ }
+ else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ event.accepted = true
+ root.currentState.doPrimaryAction()
+ }
+ }
+
+ function getSeedPhrase() {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ return root.addAccountModule.getSeedPhrase()
+ }
+
+ function changeSelectedOrigin(keyUid) {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.addAccountModule.changeSelectedOrigin(keyUid)
+ }
+
+ readonly property var changeDerivationPathPostponed: Backpressure.debounce(root, 400, function (path) {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.changeDerivationPath(path)
+ })
+
+ readonly property var changeWatchOnlyAccountAddressPostponed: Backpressure.debounce(root, 400, function (address) {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.addAccountModule.changeWatchOnlyAccountAddress(address)
+ })
+
+ function cleanWatchOnlyAccountAddress() {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.addAccountModule.changeWatchOnlyAccountAddress("")
+ }
+
+ readonly property var changePrivateKeyPostponed: Backpressure.debounce(root, 400, function (privateKey) {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.addAccountModule.changePrivateKey(privateKey)
+ })
+
+ function cleanPrivateKey() {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.enteredPrivateKeyIsValid = false
+ root.addAccountModule.newKeyPairName = ""
+ root.addAccountModule.changePrivateKey("")
+ }
+
+ function changeDerivationPath(path) {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.addAccountModule.changeDerivationPath(path)
+ }
+
+ function changeRootDerivationPath(rootPath) {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.selectedRootPath = rootPath
+ root.addAccountModule.derivationPath = "%1/".arg(rootPath)
+ }
+
+ function changeSelectedDerivedAddress(address) {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.addAccountModule.changeSelectedDerivedAddress(address)
+ }
+
+ function resetDerivationPath() {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.selectedRootPath = Constants.addAccountPopup.predefinedPaths.ethereum
+ root.addAccountModule.resetDerivationPath()
+ }
+
+ function authenticateForEditingDerivationPath() {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.addAccountModule.authenticateForEditingDerivationPath()
+ }
+
+ function startScanningForActivity() {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.addAccountModule.startScanningForActivity()
+ }
+
+ function validSeedPhrase(seedPhrase) {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ return root.addAccountModule.validSeedPhrase(seedPhrase)
+ }
+
+ function changeSeedPhrase(seedPhrase) {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.addAccountModule.changeSeedPhrase(seedPhrase)
+ }
+
+ function cleanSeedPhrase() {
+ if (!root.moduleInitialized()) {
+ return
+ }
+ root.enteredSeedPhraseIsValid = false
+ root.addAccountModule.newKeyPairName = ""
+ root.changeSeedPhrase("")
+ }
+
+ function translation(key, isTitle) {
+ if (!isTitle) {
+ if (key === Constants.addAccountPopup.predefinedPaths.custom)
+ return qsTr("Type your own derivation path")
+ return key
+ }
+ switch(key) {
+ case Constants.addAccountPopup.predefinedPaths.custom:
+ return qsTr("Custom")
+ case Constants.addAccountPopup.predefinedPaths.ethereum:
+ return qsTr("Ethereum")
+ case Constants.addAccountPopup.predefinedPaths.ethereumRopsten:
+ return qsTr("Ethereum Testnet (Ropsten)")
+ case Constants.addAccountPopup.predefinedPaths.ethereumLedger:
+ return qsTr("Ethereum (Ledger)")
+ case Constants.addAccountPopup.predefinedPaths.ethereumLedgerLive:
+ return qsTr("Ethereum (Ledger Live/KeepKey)")
+ }
+ }
+
+ function getFromClipboard() {
+ return globalUtils.getFromClipboard()
+ }
+
+ readonly property bool primaryPopupButtonEnabled: {
+ if (!root.addAccountModule || !root.currentState || root.disablePopup) {
+ return false
+ }
+
+ let valid = root.addAccountModule.accountName !== "" &&
+ root.addAccountModule.selectedColor !== "" &&
+ root.addAccountModule.selectedEmoji !== ""
+
+ if (root.currentState.stateType === Constants.addAccountPopup.state.main) {
+ if (root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.profile ||
+ root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.seedImport) {
+ return valid &&
+ root.derivationPathRegEx.test(root.addAccountModule.derivationPath) &&
+ (!root.derivationPathOutOfTheDefaultStatusDerivationTree ||
+ root.derivationPathOutOfTheDefaultStatusDerivationTreeConfirmed)
+ }
+ if (root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.unknown &&
+ root.selectedOrigin.keyUid === Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc) {
+ return valid &&
+ !!root.watchOnlyAccAddress &&
+ root.watchOnlyAccAddress.loaded &&
+ !root.watchOnlyAccAddress.alreadyCreated &&
+ root.watchOnlyAccAddress.address !== ""
+ }
+ if (root.selectedOrigin.pairType === Constants.addAccountPopup.keyPairType.privateKeyImport) {
+ return valid &&
+ root.enteredPrivateKeyIsValid &&
+ !!root.privateKeyAccAddress &&
+ root.privateKeyAccAddress.loaded &&
+ !root.privateKeyAccAddress.alreadyCreated &&
+ root.privateKeyAccAddress.address !== "" &&
+ root.addAccountModule.newKeyPairName !== ""
+ }
+ }
+
+ if (root.currentState.stateType === Constants.addAccountPopup.state.enterPrivateKey) {
+ return root.enteredPrivateKeyIsValid &&
+ !!root.privateKeyAccAddress &&
+ root.privateKeyAccAddress.loaded &&
+ !root.privateKeyAccAddress.alreadyCreated &&
+ root.privateKeyAccAddress.address !== "" &&
+ root.addAccountModule.newKeyPairName !== ""
+ }
+
+ if (root.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhrase) {
+ return root.enteredSeedPhraseIsValid &&
+ root.addAccountModule.newKeyPairName !== ""
+
+ }
+
+ if (root.currentState.stateType === Constants.addAccountPopup.state.confirmAddingNewMasterKey) {
+ return root.addingNewMasterKeyConfirmed
+ }
+
+ if (root.currentState.stateType === Constants.addAccountPopup.state.displaySeedPhrase) {
+ return root.seedPhraseRevealed
+ }
+
+ if (root.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord1) {
+ return root.seedPhraseWord1Valid
+ }
+
+ if (root.currentState.stateType === Constants.addAccountPopup.state.enterSeedPhraseWord2) {
+ return root.seedPhraseWord2Valid
+ }
+
+ if (root.currentState.stateType === Constants.addAccountPopup.state.confirmSeedPhraseBackup) {
+ return root.seedPhraseBackupConfirmed
+ }
+
+ if (root.currentState.stateType === Constants.addAccountPopup.state.enterKeypairName) {
+ return root.addAccountModule.newKeyPairName !== ""
+ }
+
+ return true
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml
index 8b651504d6..3dcc86a42d 100644
--- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml
+++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml
@@ -6,6 +6,7 @@ import utils 1.0
import SortFilterProxyModel 0.2
import StatusQ.Core.Theme 0.1
+import "../addaccount/stores"
QtObject {
id: root
@@ -187,4 +188,11 @@ QtObject {
function copyToClipboard(text) {
globalUtils.copyToClipboard(text)
}
+
+ property AddAccountStore addAccountStore: AddAccountStore {
+ }
+
+ function runAddAccountPopup() {
+ walletSection.runAddAccountPopup()
+ }
}
diff --git a/ui/app/AppLayouts/Wallet/views/LeftTabView.qml b/ui/app/AppLayouts/Wallet/views/LeftTabView.qml
index 7294b8c205..a4c2715da2 100644
--- a/ui/app/AppLayouts/Wallet/views/LeftTabView.qml
+++ b/ui/app/AppLayouts/Wallet/views/LeftTabView.qml
@@ -19,6 +19,7 @@ import shared.stores 1.0
import "../controls"
import "../popups"
import "../stores"
+import "../addaccount"
Rectangle {
id: root
@@ -34,6 +35,34 @@ Rectangle {
color: Style.current.secondaryMenuBackground
+ Loader {
+ id: addAccount
+ active: false
+ asynchronous: true
+
+ sourceComponent: AddAccountPopup {
+ store: RootStore.addAccountStore
+ anchors.centerIn: parent
+ }
+
+ onLoaded: {
+ addAccount.item.open()
+ }
+ }
+
+ Connections {
+ target: walletSection
+
+ function onDisplayAddAccountPopup() {
+ RootStore.addAccountStore.emojiPopup = root.emojiPopup
+ RootStore.addAccountStore.addAccountModule = walletSection.addAccountModule
+ addAccount.active = true
+ }
+ function onDestroyAddAccountPopup() {
+ addAccount.active = false
+ }
+ }
+
ColumnLayout {
anchors.fill: parent
spacing: Style.current.padding
@@ -63,6 +92,7 @@ Rectangle {
height: parent.height * 2
color: hovered || highlighted ? Theme.palette.primaryColor3
: "transparent"
+ onClicked: RootStore.runAddAccountPopup()
}
}
diff --git a/ui/imports/shared/panels/EnterSeedPhrase.qml b/ui/imports/shared/panels/EnterSeedPhrase.qml
new file mode 100644
index 0000000000..5962eed4aa
--- /dev/null
+++ b/ui/imports/shared/panels/EnterSeedPhrase.qml
@@ -0,0 +1,294 @@
+import QtQuick 2.14
+import QtQuick.Layouts 1.14
+
+import StatusQ.Core 0.1
+import StatusQ.Core.Theme 0.1
+import StatusQ.Controls 0.1
+import StatusQ.Controls.Validators 0.1
+
+import utils 1.0
+import shared.stores 1.0
+import shared.controls 1.0
+
+ColumnLayout {
+ id: root
+
+ //**************************************************************************
+ //* This component is not refactored, just pulled out to a shared location *
+ //**************************************************************************
+
+ spacing: Style.current.padding
+ clip: true
+
+ property var isSeedPhraseValid: function (mnemonic) { return false }
+
+ signal submitSeedPhrase()
+ signal seedPhraseUpdated(bool valid, string seedPhrase)
+
+ function setWrongSeedPhraseMessage(message) {
+ invalidSeedTxt.text = message
+ }
+
+ QtObject {
+ id: d
+
+ property bool allEntriesValid: false
+ property var mnemonicInput: []
+ readonly property var tabs: [12, 18, 24]
+ readonly property ListModel seedPhrases_en: BIP39_en {}
+
+ onAllEntriesValidChanged: {
+ let mnemonicString = ""
+ if (d.allEntriesValid) {
+ const sortTable = mnemonicInput.sort((a, b) => a.pos - b.pos)
+ for (let i = 0; i < mnemonicInput.length; i++) {
+ d.checkWordExistence(sortTable[i].seed)
+ mnemonicString += sortTable[i].seed + ((i === (grid.count-1)) ? "" : " ")
+ }
+
+ if (!Utils.isMnemonic(mnemonicString) || !root.isSeedPhraseValid(mnemonicString)) {
+ root.setWrongSeedPhraseMessage(qsTr("Invalid seed phrase"))
+ d.allEntriesValid = false
+ }
+ }
+ root.seedPhraseUpdated(d.allEntriesValid, mnemonicString)
+ }
+
+ function checkMnemonicLength() {
+ d.allEntriesValid = d.mnemonicInput.length === d.tabs[switchTabBar.currentIndex]
+ }
+
+ function checkWordExistence(word) {
+ d.allEntriesValid = d.allEntriesValid && d.seedPhrases_en.words.includes(word)
+ if (d.allEntriesValid) {
+ root.setWrongSeedPhraseMessage("")
+ }
+ else {
+ root.setWrongSeedPhraseMessage(qsTr("The phrase you’ve entered is invalid"))
+ }
+ }
+
+ function pasteWords () {
+ const clipboardText = globalUtils.getFromClipboard()
+ // Split words separated by commas and or blank spaces (spaces, enters, tabs)
+ const words = clipboardText.split(/[, \s]+/)
+
+ let index = d.tabs.indexOf(words.length)
+ if (index === -1) {
+ return false
+ }
+
+ let timeout = 0
+ if (switchTabBar.currentIndex !== index) {
+ switchTabBar.currentIndex = index
+ // Set the teimeout to 100 so the grid has time to generate the new items
+ timeout = 100
+ }
+
+ d.mnemonicInput = []
+ timer.setTimeout(() => {
+ // Populate mnemonicInput
+ for (let i = 0; i < words.length; i++) {
+ grid.addWord(i + 1, words[i], true)
+ }
+ // Populate grid
+ for (let j = 0; j < grid.count; j++) {
+ const item = grid.itemAtIndex(j)
+ if (!item || !item.leftComponentText) {
+ // The grid has gaps in it and also sometimes doesn't return the item correctly when offscreen
+ // in those cases, we just add the word in the array but not in the grid.
+ // The button will still work and import correctly. The Grid itself will be partly empty, but offscreen
+ // With the re-design of the grid, this should be fixed
+ continue
+ }
+ const pos = item.mnemonicIndex
+ item.setWord(words[pos - 1])
+ }
+ d.checkMnemonicLength()
+ }, timeout)
+ return true
+ }
+ }
+
+
+
+ Timer {
+ id: timer
+ }
+
+ StatusSwitchTabBar {
+ id: switchTabBar
+ Layout.preferredWidth: parent.width
+ Repeater {
+ model: d.tabs
+ StatusSwitchTabButton {
+ text: qsTr("%n word(s)", "", modelData)
+ id: seedPhraseWords
+ objectName: `${modelData}SeedButton`
+ }
+ }
+ onCurrentIndexChanged: {
+ d.mnemonicInput = d.mnemonicInput.filter(function(value) {
+ return value.pos <= d.tabs[switchTabBar.currentIndex]
+ })
+ d.checkMnemonicLength()
+ }
+ }
+
+ StatusGridView {
+ id: grid
+ readonly property var wordIndex: [
+ ["1", "3", "5", "7", "9", "11", "2", "4", "6", "8", "10", "12"]
+ ,["1", "4", "7", "10", "13", "16", "2", "5", "8",
+ "11", "14", "17", "3", "6", "9", "12", "15", "18"]
+ ,["1", "5", "9", "13", "17", "21", "2", "6", "10", "14", "18", "22",
+ "3", "7", "11", "15", "19", "23", "4", "8", "12", "16", "20", "24"]
+ ]
+ Layout.preferredWidth: parent.width
+ Layout.preferredHeight: 312
+ clip: false
+ flow: GridView.FlowTopToBottom
+ cellWidth: (parent.width/(count/6))
+ cellHeight: 52
+ interactive: false
+ z: 100000
+ cacheBuffer: 9999
+ model: switchTabBar.currentItem.text.substring(0,2)
+
+ function addWord(pos, word, ignoreGoingNext = false) {
+ d.mnemonicInput.push({pos: pos, seed: word.replace(/\s/g, '')})
+
+ for (let j = 0; j < d.mnemonicInput.length; j++) {
+ if (d.mnemonicInput[j].pos === pos && d.mnemonicInput[j].seed !== word) {
+ d.mnemonicInput[j].seed = word
+ break
+ }
+ }
+ //remove duplicates
+ const valueArr = d.mnemonicInput.map(item => item.pos)
+ const isDuplicate = valueArr.some((item, idx) => {
+ if (valueArr.indexOf(item) !== idx) {
+ d.mnemonicInput.splice(idx, 1)
+ }
+ return valueArr.indexOf(item) !== idx
+ })
+ if (!ignoreGoingNext) {
+ for (let i = 0; i < grid.count; i++) {
+ if (grid.itemAtIndex(i).mnemonicIndex !== (pos + 1)) {
+ continue
+ }
+
+ grid.currentIndex = grid.itemAtIndex(i).itemIndex
+ grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus()
+
+ if (grid.currentIndex !== 12) {
+ continue
+ }
+
+ grid.positionViewAtEnd()
+
+ if (grid.count === 20) {
+ grid.contentX = 1500
+ }
+ }
+ }
+ d.checkMnemonicLength()
+ }
+
+ delegate: StatusSeedPhraseInput {
+ id: seedWordInput
+ textEdit.input.edit.objectName: `statusSeedPhraseInputField${seedWordInput.leftComponentText}`
+ width: (grid.cellWidth - 8)
+ height: (grid.cellHeight - 8)
+ Behavior on width { NumberAnimation { duration: 180 } }
+ textEdit.text: {
+ const pos = seedWordInput.mnemonicIndex
+ for (let i in d.mnemonicInput) {
+ const p = d.mnemonicInput[i]
+ if (p.pos === pos) {
+ return p.seed
+ }
+ }
+ return ""
+ }
+
+ readonly property int mnemonicIndex: grid.wordIndex[(grid.count / 6) - 2][index]
+
+ leftComponentText: mnemonicIndex
+ inputList: d.seedPhrases_en
+
+ property int itemIndex: index
+ z: (grid.currentIndex === index) ? 150000000 : 0
+ onTextChanged: {
+ d.checkWordExistence(text)
+ }
+ onDoneInsertingWord: {
+ grid.addWord(mnemonicIndex, word)
+ }
+ onEditClicked: {
+ grid.currentIndex = index
+ grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus()
+ }
+ onKeyPressed: {
+ grid.currentIndex = index
+
+ if (event.key === Qt.Key_Backtab) {
+ for (let i = 0; i < grid.count; i++) {
+ if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex - 1) >= 0 ? (mnemonicIndex - 1) : 0)) {
+ grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.BacktabFocusReason)
+ textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit
+ event.accepted = true
+ break
+ }
+ }
+ } else if (event.key === Qt.Key_Tab) {
+ for (let i = 0; i < grid.count; i++) {
+ if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex + 1) <= grid.count ? (mnemonicIndex + 1) : grid.count)) {
+ grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.TabFocusReason)
+ textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit
+ event.accepted = true
+ break
+ }
+ }
+ }
+
+ if (event.matches(StandardKey.Paste)) {
+ if (d.pasteWords()) {
+ // Paste was done by splitting the words
+ event.accepted = true
+ }
+ return
+ }
+
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ event.accepted = true
+ if (d.allEntriesValid) {
+ root.submitSeedPhrase()
+ return
+ }
+ }
+
+ if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) {
+ const wordIndex = d.mnemonicInput.findIndex(x => x.pos === mnemonicIndex)
+ if (wordIndex > -1) {
+ d.mnemonicInput.splice(wordIndex, 1)
+ d.checkMnemonicLength()
+ }
+ }
+ }
+ Component.onCompleted: {
+ const item = grid.itemAtIndex(0)
+ if (item) {
+ item.textEdit.input.edit.forceActiveFocus()
+ }
+ }
+ }
+ }
+
+ StatusBaseText {
+ id: invalidSeedTxt
+ visible: text !== ""
+ Layout.alignment: Qt.AlignHCenter
+ color: Theme.palette.dangerColor1
+ }
+}
diff --git a/ui/imports/shared/panels/SeedPhrase.qml b/ui/imports/shared/panels/SeedPhrase.qml
new file mode 100644
index 0000000000..18e368049c
--- /dev/null
+++ b/ui/imports/shared/panels/SeedPhrase.qml
@@ -0,0 +1,59 @@
+import QtQuick 2.14
+import QtQuick.Layouts 1.14
+import QtQuick.Controls 2.14
+import QtGraphicalEffects 1.14
+
+import StatusQ.Core 0.1
+import StatusQ.Controls 0.1
+
+Item {
+ id: root
+
+ property var seedPhrase: []
+ property bool seedPhraseRevealed: false
+
+ StatusGridView {
+ id: grid
+ anchors.fill: parent
+ visible: root.seedPhraseRevealed
+ cellWidth: parent.width * 0.5
+ cellHeight: 48
+ interactive: false
+ model: 12
+ readonly property var wordIndex: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
+ readonly property int spacing: 4
+ delegate: StatusSeedPhraseInput {
+ width: (grid.cellWidth - grid.spacing)
+ height: (grid.cellHeight - grid.spacing)
+ textEdit.input.edit.enabled: false
+ text: {
+ const idx = parseInt(leftComponentText) - 1;
+ if (!root.seedPhrase || idx < 0 || idx > root.seedPhrase.length - 1)
+ return "";
+ return root.seedPhrase[idx];
+ }
+ leftComponentText: grid.wordIndex[index]
+ }
+ }
+
+ GaussianBlur {
+ id: blur
+ anchors.fill: grid
+ visible: !root.seedPhraseRevealed
+ source: grid
+ radius: 16
+ samples: 16
+ transparentBorder: true
+ }
+
+ StatusButton {
+ anchors.centerIn: parent
+ visible: !root.seedPhraseRevealed
+ type: StatusBaseButton.Type.Primary
+ icon.name: "view"
+ text: qsTr("Reveal seed phrase")
+ onClicked: {
+ root.seedPhraseRevealed = true
+ }
+ }
+}
diff --git a/ui/imports/shared/panels/qmldir b/ui/imports/shared/panels/qmldir
index e34586d210..dcef3948ba 100644
--- a/ui/imports/shared/panels/qmldir
+++ b/ui/imports/shared/panels/qmldir
@@ -24,3 +24,5 @@ StatusAssetSelector 1.0 StatusAssetSelector.qml
AcceptRejectOptionsButtonsPanel 1.0 AcceptRejectOptionsButtonsPanel.qml
DidYouKnowSplashScreen 1.0 DidYouKnowSplashScreen.qml
ConnectionWarnings 1.0 ConnectionWarnings.qml
+EnterSeedPhrase 1.0 EnterSeedPhrase.qml
+SeedPhrase 1.0 SeedPhrase.qml
diff --git a/ui/imports/shared/popups/keycard/KeycardPopupContent.qml b/ui/imports/shared/popups/keycard/KeycardPopupContent.qml
index 2084264423..512119ca33 100644
--- a/ui/imports/shared/popups/keycard/KeycardPopupContent.qml
+++ b/ui/imports/shared/popups/keycard/KeycardPopupContent.qml
@@ -16,7 +16,6 @@ Item {
QtObject {
id: d
property bool primaryButtonEnabled: false
- property bool seedPhraseRevealed: false
}
Loader {
@@ -226,12 +225,11 @@ Item {
sharedKeycardModule: root.sharedKeycardModule
Component.onCompleted: {
- hideSeed = !d.seedPhraseRevealed
- d.primaryButtonEnabled = Qt.binding(function(){ return d.seedPhraseRevealed })
+ seedPhraseRevealed = false
}
- onSeedPhraseRevealed: {
- d.seedPhraseRevealed = true
+ onSeedPhraseRevealedChanged: {
+ d.primaryButtonEnabled = seedPhraseRevealed
}
}
}
diff --git a/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml b/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml
index 739d80cdef..3b0d819923 100644
--- a/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml
+++ b/ui/imports/shared/popups/keycard/states/EnterSeedPhrase.qml
@@ -1,14 +1,7 @@
import QtQuick 2.14
-import QtQuick.Layouts 1.14
-
-import StatusQ.Core 0.1
-import StatusQ.Core.Theme 0.1
-import StatusQ.Controls 0.1
-import StatusQ.Controls.Validators 0.1
import utils 1.0
-import shared.stores 1.0
-import shared.controls 1.0
+import shared.panels 1.0 as SharedPanels
Item {
id: root
@@ -20,292 +13,34 @@ Item {
QtObject {
id: d
- property bool allEntriesValid: false
- property var mnemonicInput: []
- readonly property var tabs: [12, 18, 24]
- readonly property ListModel seedPhrases_en: BIP39_en {}
property bool wrongSeedPhrase: root.sharedKeycardModule.keycardData & Constants.predefinedKeycardData.wrongSeedPhrase
-
onWrongSeedPhraseChanged: {
- if (wrongSeedPhrase) {
- invalidSeedTxt.text = qsTr("The phrase you’ve entered does not match this Keycard’s seed phrase")
- }
- else {
- invalidSeedTxt.text = ""
- }
+ seedPhrase.setWrongSeedPhraseMessage(wrongSeedPhrase? qsTr("The phrase you’ve entered does not match this Keycard’s seed phrase") : "")
}
- onAllEntriesValidChanged: {
- if (d.allEntriesValid) {
- let mnemonicString = ""
- const sortTable = mnemonicInput.sort((a, b) => a.pos - b.pos)
- for (let i = 0; i < mnemonicInput.length; i++) {
- d.checkWordExistence(sortTable[i].seed)
- mnemonicString += sortTable[i].seed + ((i === (grid.count-1)) ? "" : " ")
- }
-
- if (Utils.isMnemonic(mnemonicString) && root.sharedKeycardModule.validSeedPhrase(mnemonicString)) {
- root.sharedKeycardModule.setSeedPhrase(mnemonicString)
- } else {
- invalidSeedTxt.text = qsTr("Invalid seed phrase")
- d.allEntriesValid = false
- }
- }
- root.validation(d.allEntriesValid)
- }
-
- function checkMnemonicLength() {
- d.allEntriesValid = d.mnemonicInput.length === d.tabs[switchTabBar.currentIndex]
- }
-
- function checkWordExistence(word) {
- d.allEntriesValid = d.allEntriesValid && d.seedPhrases_en.words.includes(word)
- if (d.allEntriesValid) {
- invalidSeedTxt.text = ""
- }
- else {
- invalidSeedTxt.text = qsTr("The phrase you’ve entered is invalid")
- }
- }
-
- function pasteWords () {
- const clipboardText = globalUtils.getFromClipboard()
- // Split words separated by commas and or blank spaces (spaces, enters, tabs)
- const words = clipboardText.split(/[, \s]+/)
-
- let index = d.tabs.indexOf(words.length)
- if (index === -1) {
- return false
- }
-
- let timeout = 0
- if (switchTabBar.currentIndex !== index) {
- switchTabBar.currentIndex = index
- // Set the teimeout to 100 so the grid has time to generate the new items
- timeout = 100
- }
-
- d.mnemonicInput = []
- timer.setTimeout(() => {
- // Populate mnemonicInput
- for (let i = 0; i < words.length; i++) {
- grid.addWord(i + 1, words[i], true)
- }
- // Populate grid
- for (let j = 0; j < grid.count; j++) {
- const item = grid.itemAtIndex(j)
- if (!item || !item.leftComponentText) {
- // The grid has gaps in it and also sometimes doesn't return the item correctly when offscreen
- // in those cases, we just add the word in the array but not in the grid.
- // The button will still work and import correctly. The Grid itself will be partly empty, but offscreen
- // With the re-design of the grid, this should be fixed
- continue
- }
- const pos = item.mnemonicIndex
- item.setWord(words[pos - 1])
- }
- d.checkMnemonicLength()
- }, timeout)
- return true
- }
}
- ColumnLayout {
+ SharedPanels.EnterSeedPhrase {
+ id: seedPhrase
anchors.fill: parent
anchors.topMargin: Style.current.xlPadding
anchors.bottomMargin: Style.current.halfPadding
anchors.leftMargin: Style.current.xlPadding
anchors.rightMargin: Style.current.xlPadding
- spacing: Style.current.padding
- clip: true
- StatusBaseText {
- id: title
- Layout.preferredHeight: Constants.keycard.general.titleHeight
- Layout.alignment: Qt.AlignHCenter
- text: qsTr("Enter key pair seed phrase")
- font.pixelSize: Constants.keycard.general.fontSize1
- font.weight: Font.Bold
- color: Theme.palette.directColor1
+ isSeedPhraseValid: function(mnemonic) {
+ return root.sharedKeycardModule.validSeedPhrase(mnemonic)
}
- Timer {
- id: timer
+ onSeedPhraseUpdated: {
+ if (valid) {
+ root.sharedKeycardModule.setSeedPhrase(seedPhrase)
+ }
+ root.validation(valid)
}
- StatusSwitchTabBar {
- id: switchTabBar
- Layout.alignment: Qt.AlignHCenter
- Repeater {
- model: d.tabs
- StatusSwitchTabButton {
- text: qsTr("%n word(s)", "", modelData)
- id: seedPhraseWords
- objectName: `${modelData}SeedButton`
- }
- }
- onCurrentIndexChanged: {
- d.mnemonicInput = d.mnemonicInput.filter(function(value) {
- return value.pos <= d.tabs[switchTabBar.currentIndex]
- })
- d.checkMnemonicLength()
- }
- }
-
- StatusGridView {
- id: grid
- readonly property var wordIndex: [
- ["1", "3", "5", "7", "9", "11", "2", "4", "6", "8", "10", "12"]
- ,["1", "4", "7", "10", "13", "16", "2", "5", "8",
- "11", "14", "17", "3", "6", "9", "12", "15", "18"]
- ,["1", "5", "9", "13", "17", "21", "2", "6", "10", "14", "18", "22",
- "3", "7", "11", "15", "19", "23", "4", "8", "12", "16", "20", "24"]
- ]
- Layout.preferredWidth: parent.width
- Layout.preferredHeight: 312
- clip: false
- flow: GridView.FlowTopToBottom
- cellWidth: (parent.width/(count/6))
- cellHeight: 52
- interactive: false
- z: 100000
- cacheBuffer: 9999
- model: switchTabBar.currentItem.text.substring(0,2)
-
- function addWord(pos, word, ignoreGoingNext = false) {
- d.mnemonicInput.push({pos: pos, seed: word.replace(/\s/g, '')})
-
- for (let j = 0; j < d.mnemonicInput.length; j++) {
- if (d.mnemonicInput[j].pos === pos && d.mnemonicInput[j].seed !== word) {
- d.mnemonicInput[j].seed = word
- break
- }
- }
- //remove duplicates
- const valueArr = d.mnemonicInput.map(item => item.pos)
- const isDuplicate = valueArr.some((item, idx) => {
- if (valueArr.indexOf(item) !== idx) {
- d.mnemonicInput.splice(idx, 1)
- }
- return valueArr.indexOf(item) !== idx
- })
- if (!ignoreGoingNext) {
- for (let i = 0; i < grid.count; i++) {
- if (grid.itemAtIndex(i).mnemonicIndex !== (pos + 1)) {
- continue
- }
-
- grid.currentIndex = grid.itemAtIndex(i).itemIndex
- grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus()
-
- if (grid.currentIndex !== 12) {
- continue
- }
-
- grid.positionViewAtEnd()
-
- if (grid.count === 20) {
- grid.contentX = 1500
- }
- }
- }
- d.checkMnemonicLength()
- }
-
- delegate: StatusSeedPhraseInput {
- id: seedWordInput
- textEdit.input.edit.objectName: `statusSeedPhraseInputField${seedWordInput.leftComponentText}`
- width: (grid.cellWidth - 8)
- height: (grid.cellHeight - 8)
- Behavior on width { NumberAnimation { duration: 180 } }
- textEdit.text: {
- const pos = seedWordInput.mnemonicIndex
- for (let i in d.mnemonicInput) {
- const p = d.mnemonicInput[i]
- if (p.pos === pos) {
- return p.seed
- }
- }
- return ""
- }
-
- readonly property int mnemonicIndex: grid.wordIndex[(grid.count / 6) - 2][index]
-
- leftComponentText: mnemonicIndex
- inputList: d.seedPhrases_en
-
- property int itemIndex: index
- z: (grid.currentIndex === index) ? 150000000 : 0
- onTextChanged: {
- d.checkWordExistence(text)
- }
- onDoneInsertingWord: {
- grid.addWord(mnemonicIndex, word)
- }
- onEditClicked: {
- grid.currentIndex = index
- grid.itemAtIndex(index).textEdit.input.edit.forceActiveFocus()
- }
- onKeyPressed: {
- grid.currentIndex = index
-
- if (event.key === Qt.Key_Backtab) {
- for (let i = 0; i < grid.count; i++) {
- if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex - 1) >= 0 ? (mnemonicIndex - 1) : 0)) {
- grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.BacktabFocusReason)
- textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit
- event.accepted = true
- break
- }
- }
- } else if (event.key === Qt.Key_Tab) {
- for (let i = 0; i < grid.count; i++) {
- if (grid.itemAtIndex(i).mnemonicIndex === ((mnemonicIndex + 1) <= grid.count ? (mnemonicIndex + 1) : grid.count)) {
- grid.itemAtIndex(i).textEdit.input.edit.forceActiveFocus(Qt.TabFocusReason)
- textEdit.input.tabNavItem = grid.itemAtIndex(i).textEdit.input.edit
- event.accepted = true
- break
- }
- }
- }
-
- if (event.matches(StandardKey.Paste)) {
- if (d.pasteWords()) {
- // Paste was done by splitting the words
- event.accepted = true
- }
- return
- }
-
- if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
- event.accepted = true
- if (d.allEntriesValid) {
- root.sharedKeycardModule.currentState.doPrimaryAction()
- return
- }
- }
-
- if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) {
- const wordIndex = d.mnemonicInput.findIndex(x => x.pos === mnemonicIndex)
- if (wordIndex > -1) {
- d.mnemonicInput.splice(wordIndex, 1)
- d.checkMnemonicLength()
- }
- }
- }
- Component.onCompleted: {
- const item = grid.itemAtIndex(0)
- if (item) {
- item.textEdit.input.edit.forceActiveFocus()
- }
- }
- }
- }
-
- StatusBaseText {
- id: invalidSeedTxt
- Layout.alignment: Qt.AlignHCenter
- color: Theme.palette.dangerColor1
+ onSubmitSeedPhrase: {
+ root.sharedKeycardModule.currentState.doPrimaryAction()
}
}
diff --git a/ui/imports/shared/popups/keycard/states/SeedPhrase.qml b/ui/imports/shared/popups/keycard/states/SeedPhrase.qml
index 6ec355f3ac..b71299c488 100644
--- a/ui/imports/shared/popups/keycard/states/SeedPhrase.qml
+++ b/ui/imports/shared/popups/keycard/states/SeedPhrase.qml
@@ -9,20 +9,13 @@ import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import utils 1.0
+import shared.panels 1.0 as SharedPanels
Item {
id: root
property var sharedKeycardModule
- property bool hideSeed: true
-
- signal seedPhraseRevealed()
-
- QtObject {
- id: d
-
- readonly property var seedPhrase: root.sharedKeycardModule.getMnemonic().split(" ")
- }
+ property alias seedPhraseRevealed: displaySeed.seedPhraseRevealed
ColumnLayout {
anchors.fill: parent
@@ -48,55 +41,12 @@ Item {
wrapMode: Text.WordWrap
}
- Item {
+ SharedPanels.SeedPhrase {
+ id: displaySeed
Layout.preferredWidth: parent.width
Layout.fillHeight: true
- StatusGridView {
- id: grid
- anchors.fill: parent
- visible: !root.hideSeed
- cellWidth: parent.width * 0.5
- cellHeight: 48
- interactive: false
- model: 12
- readonly property var wordIndex: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
- readonly property int spacing: 4
- delegate: StatusSeedPhraseInput {
- width: (grid.cellWidth - grid.spacing)
- height: (grid.cellHeight - grid.spacing)
- textEdit.input.edit.enabled: false
- text: {
- const idx = parseInt(leftComponentText) - 1;
- if (!d.seedPhrase || idx < 0 || idx > d.seedPhrase.length - 1)
- return "";
- return d.seedPhrase[idx];
- }
- leftComponentText: grid.wordIndex[index]
- }
- }
-
- GaussianBlur {
- id: blur
- anchors.fill: grid
- visible: root.hideSeed
- source: grid
- radius: 16
- samples: 16
- transparentBorder: true
- }
-
- StatusButton {
- anchors.centerIn: parent
- visible: root.hideSeed
- type: StatusBaseButton.Type.Primary
- icon.name: "view"
- text: qsTr("Reveal seed phrase")
- onClicked: {
- root.hideSeed = false;
- root.seedPhraseRevealed()
- }
- }
+ property var seedPhrase: root.sharedKeycardModule.getMnemonic().split(" ")
}
}
diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml
index 43d031b81d..a691d80487 100644
--- a/ui/imports/utils/Constants.qml
+++ b/ui/imports/utils/Constants.qml
@@ -608,6 +608,48 @@ QtObject {
readonly property int eventProcessError: 6
}
+ readonly property QtObject addAccountPopup: QtObject {
+ readonly property int popupWidth: 480
+ readonly property int contentHeight1: 554
+ readonly property int contentHeight2: 642
+ readonly property int itemHeight: 64
+ readonly property int labelFontSize1: 15
+ readonly property int labelFontSize2: 13
+ readonly property int footerButtonsHeight: 44
+ readonly property int keyPairNameMaxLength: 20
+ readonly property int stepperWidth: 242
+ readonly property int stepperHeight: 30
+
+ readonly property QtObject keyPairType: QtObject {
+ readonly property int unknown: -1
+ readonly property int profile: 0
+ readonly property int seedImport: 1
+ readonly property int privateKeyImport: 2
+ }
+
+ readonly property QtObject predefinedPaths: QtObject {
+ readonly property string custom: "m/44'"
+ readonly property string ethereum: "m/44'/60'/0'/0"
+ readonly property string ethereumRopsten: "m/44'/1'/0'/0"
+ readonly property string ethereumLedger: "m/44'/60'/0'"
+ readonly property string ethereumLedgerLive: "m/44'/60'"
+ }
+
+ readonly property QtObject state: QtObject {
+ readonly property string noState: "NoState"
+ readonly property string main: "Main"
+ readonly property string selectMasterKey: "SelectMasterKey"
+ readonly property string enterSeedPhrase: "EnterSeedPhrase"
+ readonly property string enterSeedPhraseWord1: "EnterSeedPhraseWord1"
+ readonly property string enterSeedPhraseWord2: "EnterSeedPhraseWord2"
+ readonly property string enterPrivateKey: "EnterPrivateKey"
+ readonly property string enterKeypairName: "EnterKeypairName"
+ readonly property string displaySeedPhrase: "DisplaySeedPhrase"
+ readonly property string confirmAddingNewMasterKey: "ConfirmAddingNewMasterKey"
+ readonly property string confirmSeedPhraseBackup: "ConfirmSeedPhraseBackup"
+ }
+ }
+
readonly property QtObject localPairingAction: QtObject {
readonly property int actionUnknown: 0
readonly property int actionConnect: 1
@@ -842,12 +884,10 @@ QtObject {
readonly property string loginAccountsListAddNewUser: "LOGIN-ACCOUNTS-LIST-ADD-NEW-USER"
readonly property string loginAccountsListAddExistingUser: "LOGIN-ACCOUNTS-LIST-ADD-EXISTING-USER"
readonly property string loginAccountsListLostKeycard: "LOGIN-ACCOUNTS-LIST-LOST-KEYCARD"
- }
-
- readonly property var appTranslationMap: ({})
- Component.onCompleted: {
- appTranslationMap[appTranslatableConstants.loginAccountsListAddNewUser] = qsTr("Add new user")
- appTranslationMap[appTranslatableConstants.loginAccountsListAddExistingUser] = qsTr("Add existing Status user")
- appTranslationMap[appTranslatableConstants.loginAccountsListLostKeycard] = qsTr("Lost Keycard")
+ readonly property string addAccountLabelNewWatchOnlyAccount: "LABEL-NEW-WATCH-ONLY-ACCOUNT"
+ readonly property string addAccountLabelExisting: "LABEL-EXISTING"
+ readonly property string addAccountLabelImportNew: "LABEL-IMPORT-NEW"
+ readonly property string addAccountLabelOptionAddNewMasterKey: "LABEL-OPTION-ADD-NEW-MASTER-KEY"
+ readonly property string addAccountLabelOptionAddWatchOnlyAcc: "LABEL-OPTION-ADD-WATCH-ONLY-ACC"
}
}
diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml
index acde615177..d670b3563c 100644
--- a/ui/imports/utils/Utils.qml
+++ b/ui/imports/utils/Utils.qml
@@ -617,6 +617,29 @@ QtObject {
return result
}
+ function appTranslation(key) {
+ switch(key) {
+ case Constants.appTranslatableConstants.loginAccountsListAddNewUser:
+ return qsTr("Add new user")
+ case Constants.appTranslatableConstants.loginAccountsListAddExistingUser:
+ return qsTr("Add existing Status user")
+ case Constants.appTranslatableConstants.loginAccountsListLostKeycard:
+ return qsTr("Lost Keycard")
+ case Constants.appTranslatableConstants.addAccountLabelNewWatchOnlyAccount:
+ return qsTr("New watch-only account")
+ case Constants.appTranslatableConstants.addAccountLabelExisting:
+ return qsTr("Existing")
+ case Constants.appTranslatableConstants.addAccountLabelImportNew:
+ return qsTr("Import new")
+ case Constants.appTranslatableConstants.addAccountLabelOptionAddNewMasterKey:
+ return qsTr("Add new master key")
+ case Constants.appTranslatableConstants.addAccountLabelOptionAddWatchOnlyAcc:
+ return qsTr("Add watch-only account")
+ }
+
+ return key
+ }
+
// Leave this function at the bottom of the file as QT Creator messes up the code color after this
function isPunct(c) {
return /(!|\@|#|\$|%|\^|&|\*|\(|\)|\+|\||-|=|\\|{|}|[|]|"|;|'|<|>|\?|,|\.|\/)/.test(c)