feat(@desktop/wallet): keypair rename (a profile keypair name should follow display name)

Fixes: #10769
This commit is contained in:
Sale Djenic 2023-07-13 15:58:48 +02:00 committed by saledjenic
parent bed7db5528
commit 6d25a888d3
17 changed files with 364 additions and 30 deletions

View File

@ -1,7 +1,6 @@
import io_interface
import ../../../../../../app_service/service/wallet_account/service as wallet_account_service
import ../../../../shared_modules/keycard_popup/io_interface as keycard_shared_module
import app_service/service/wallet_account/service as wallet_account_service
import app/modules/shared_modules/keycard_popup/io_interface as keycard_shared_module
type
Controller* = ref object of RootObj
@ -31,6 +30,9 @@ proc updateAccount*(self: Controller, address: string, accountName: string, colo
proc updateAccountPosition*(self: Controller, address: string, position: int) =
self.walletAccountService.updateWalletAccountPosition(address, position)
proc renameKeypair*(self: Controller, keyUid: string, name: string) =
self.walletAccountService.updateKeypairName(keyUid, name)
proc deleteAccount*(self: Controller, address: string) =
self.walletAccountService.deleteAccount(address)

View File

@ -28,6 +28,9 @@ method updateAccount*(self: AccessInterface, address: string, accountName: strin
method updateAccountPosition*(self: AccessInterface, address: string, position: int) {.base.} =
raise newException(ValueError, "No implementation available")
method renameKeypair*(self: AccessInterface, keyUid: string, name: string) {.base.} =
raise newException(ValueError, "No implementation available")
# View Delegate Interface
# Delegate for the view must be declared here due to use of QtObject and multi
# inheritance, which is not well supported in Nim.

View File

@ -1,6 +1,8 @@
import NimQml, Tables, strutils, strformat
import ./item
export item
type
ModelRole {.pure.} = enum
Name = UserRole + 1,

View File

@ -2,15 +2,15 @@ import NimQml, sequtils, sugar, chronicles
import ./io_interface, ./view, ./item, ./controller
import ../io_interface as delegate_interface
import ../../../../shared/wallet_utils
import ../../../../shared/keypairs
import ../../../../shared_models/keypair_item
import ../../../../../global/global_singleton
import ../../../../../core/eventemitter
import ../../../../../../app_service/service/keycard/service as keycard_service
import ../../../../../../app_service/service/wallet_account/service as wallet_account_service
import ../../../../../../app_service/service/network/service as network_service
import ../../../../../../app_service/service/settings/service
import app/modules/shared/wallet_utils
import app/modules/shared/keypairs
import app/modules/shared_models/keypair_model
import app/global/global_singleton
import app/core/eventemitter
import app_service/service/keycard/service as keycard_service
import app_service/service/wallet_account/service as wallet_account_service
import app_service/service/network/service as network_service
import app_service/service/settings/service
export io_interface
@ -39,6 +39,9 @@ proc newModule*(
result.controller = controller.newController(result, walletAccountService)
result.moduleLoaded = false
## Forward declarations
proc onKeypairRenamed(self: Module, keyUid: string, name: string)
method delete*(self: Module) =
self.view.delete
self.viewVariant.delete
@ -102,6 +105,14 @@ method load*(self: Module) =
return
self.refreshWalletAccounts()
self.events.on(SIGNAL_KEYPAIR_NAME_CHANGED) do(e: Args):
let args = KeypairArgs(e)
self.onKeypairRenamed(args.keypair.keyUid, args.keypair.name)
self.events.on(SIGNAL_DISPLAY_NAME_UPDATED) do(e:Args):
let args = SettingsTextValueArgs(e)
self.onKeypairRenamed(singletonInstance.userProfile.getKeyUid(), args.value)
self.events.on(SIGNAL_WALLET_ACCOUNT_POSITION_UPDATED) do(e:Args):
self.refreshWalletAccounts()
@ -131,3 +142,9 @@ method deleteAccount*(self: Module, address: string) =
method toggleIncludeWatchOnlyAccount*(self: Module) =
self.controller.toggleIncludeWatchOnlyAccount()
method renameKeypair*(self: Module, keyUid: string, name: string) =
self.controller.renameKeypair(keyUid, name)
proc onKeypairRenamed(self: Module, keyUid: string, name: string) =
self.view.keyPairModel.updateKeypairName(keyUid, name)

View File

@ -1,9 +1,8 @@
import NimQml, sequtils, strutils, sugar
import ./model
import ./item
import ./io_interface
import ../../../../shared_models/[keypair_model, keypair_item]
import ./model
import app/modules/shared_models/keypair_model
QtObject:
type
@ -52,10 +51,13 @@ QtObject:
proc onUpdatedAccount*(self: View, account: Item) =
self.accounts.onUpdatedAccount(account)
self.keyPairModel.onUpdatedAccount(account.keyUid, account.address, account.name, account.colorId, account.emoji)
proc deleteAccount*(self: View, address: string) {.slot.} =
self.delegate.deleteAccount(address)
proc keyPairModel*(self: View): KeyPairModel =
return self.keyPairModel
proc keyPairModelChanged*(self: View) {.signal.}
proc getKeyPairModel(self: View): QVariant {.slot.} =
return newQVariant(self.keyPairModel)
@ -80,3 +82,9 @@ QtObject:
proc setIncludeWatchOnlyAccount*(self: View, includeWatchOnlyAccount: bool) =
self.includeWatchOnlyAccount = includeWatchOnlyAccount
self.includeWatchOnlyAccountChanged()
proc keypairNameExists*(self: View, name: string): bool {.slot.} =
return self.keyPairModel.keypairNameExists(name)
proc renameKeypair*(self: View, keyUid: string, name: string) {.slot.} =
self.delegate.renameKeypair(keyUid, name)

View File

@ -1,4 +1,4 @@
import NimQml, Tables, strformat
import NimQml, Tables, strformat, sequtils, sugar
import keypair_item
export keypair_item
@ -80,3 +80,12 @@ QtObject:
if keyUid == item.getKeyUid():
item.getAccountsModel().updateDetailsForAddressIfTheyAreSet(address, name, colorId, emoji)
break
proc keypairNameExists*(self: KeyPairModel, name: string): bool =
return self.items.any(x => x.getName() == name)
proc updateKeypairName*(self: KeyPairModel, keyUid: string, name: string) =
let item = self.findItemByKeyUid(keyUid)
if item.isNil:
return
item.setName(name)

View File

@ -576,6 +576,24 @@ QtObject:
except Exception as e:
error "error: ", procName="updateAccountPosition", errName=e.name, errDesription=e.msg
proc updateKeypairName*(self: Service, keyUid: string, name: string) =
try:
let response = backend.updateKeypairName(keyUid, name)
if not response.error.isNil:
error "status-go error", procName="updateKeypairName", errCode=response.error.code, errDesription=response.error.message
return
# Once we start maintaining local store by keypairs we will need to update that store from here,
# till then we just emit signal from here.
self.events.emit(SIGNAL_KEYPAIR_NAME_CHANGED, KeypairArgs(
keypair: KeypairDto(
keyUid: keyUid,
name: name
)
)
)
except Exception as e:
error "error: ", procName="updateKeypairName", errName=e.name, errDesription=e.msg
proc fetchDerivedAddresses*(self: Service, password: string, derivedFrom: string, paths: seq[string], hashPassword: bool) =
let arg = FetchDerivedAddressesTaskArg(
password: if hashPassword: utils.hashPassword(password) else: password,
@ -945,10 +963,7 @@ QtObject:
proc handleKeypair(self: Service, keypair: KeypairDto) =
## In some point in future instead `self.walletAccounts` table we should switch to maintaining local state in the
## form of keypairs + another list just for watch only accounts. We will benefint from that in terms of maintaining.
## Keycards detaiils will be in that case tracked easier and stored locally as well. Also at that point we can check
## if the local keypair name is different than one received here and emit signal only in that case, till then,
## we emit it always.
self.events.emit(SIGNAL_KEYPAIR_NAME_CHANGED, KeypairArgs(keypair: KeypairDto(name: keypair.name)))
## Keycards details will be in that case tracked easier and stored locally as well.
# handle keypair related accounts
# - first remove removed accounts from the UI

View File

@ -263,6 +263,10 @@ rpc(updateAccountPosition, "accounts"):
address: string
position: int
rpc(updateKeypairName, "accounts"):
keyUid: string
name: string
rpc(getHourlyMarketValues, "wallet"):
symbol: string
currency: string

View File

@ -215,6 +215,7 @@
<file>assets/img/icons/key_pair_private_key.svg</file>
<file>assets/img/icons/key_pair_seed_phrase.svg</file>
<file>assets/img/icons/keyboard.svg</file>
<file>assets/img/icons/keycard-crossed.svg</file>
<file>assets/img/icons/keycard-logo.svg</file>
<file>assets/img/icons/keycard.svg</file>
<file>assets/img/icons/language.svg</file>

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Union" fill-rule="evenodd" clip-rule="evenodd" d="M3.46967 2.21967C3.76256 1.92678 4.23744 1.92678 4.53033 2.21967L6.91421 4.60355C7.00798 4.69732 7.13516 4.75 7.26777 4.75H17C19.2091 4.75 21 6.54086 21 8.75V16.75C21 17.2 20.9257 17.6326 20.7887 18.0362C20.7225 18.2313 20.7611 18.4504 20.9067 18.596L23.0303 20.7197C23.3232 21.0126 23.3232 21.4874 23.0303 21.7803C22.7374 22.0732 22.2626 22.0732 21.9697 21.7803L3.46967 3.28033C3.17678 2.98744 3.17678 2.51256 3.46967 2.21967ZM19.1785 16.8678C19.299 16.9884 19.5 16.9205 19.5 16.75V8.75C19.5 7.36929 18.3807 6.25 17 6.25H9.76777C9.32231 6.25 9.09923 6.78857 9.41421 7.10355L19.1785 16.8678ZM5 11.75C5 11.1977 5.44772 10.75 6 10.75H7C7.55228 10.75 8 11.1977 8 11.75V13.75C8 14.3023 7.55228 14.75 7 14.75H6C5.44772 14.75 5 14.3023 5 13.75V11.75ZM2.90954 5.33906C3.14341 5.19542 3.4411 5.25176 3.63517 5.44583C4.00257 5.81323 3.81119 6.4898 3.41036 6.82039C2.8544 7.27892 2.5 7.97307 2.5 8.75V16.75C2.5 18.1307 3.61929 19.25 5 19.25H17C17.2563 19.25 17.5139 19.3246 17.6951 19.5058L17.7172 19.5279C18.1439 19.9545 17.9959 20.6718 17.3954 20.7307C17.2653 20.7435 17.1334 20.75 17 20.75H5C2.79086 20.75 1 18.9591 1 16.75V8.75C1 7.30709 1.764 6.04263 2.90954 5.33906Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -5,6 +5,7 @@ import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
import utils 1.0
@ -17,6 +18,7 @@ Rectangle {
signal goToAccountView(var account)
signal toggleIncludeWatchOnlyAccount()
signal runRenameKeypairFlow()
QtObject {
id: d
@ -62,6 +64,69 @@ Rectangle {
icon.name: "more"
icon.color: Theme.palette.directColor1
visible: !d.isWatchOnly
highlighted: menuLoader.item && menuLoader.item.opened
onClicked: {
menuLoader.active = true
menuLoader.item.popup(0, height)
}
Loader {
id: menuLoader
active: false
sourceComponent: StatusMenu {
onClosed: {
menuLoader.active = false
}
StatusAction {
text: enabled? qsTr("Show encrypted QR of keys on device") : ""
enabled: !d.isProfileKeypair
icon.name: "qr"
icon.color: Theme.palette.primaryColor1
onTriggered: {
console.warn("TODO: show encrypted QR")
}
}
StatusAction {
text: model.keyPair.migratedToKeycard? qsTr("Stop using Keycard") : qsTr("Move keys to a Keycard")
icon.name: model.keyPair.migratedToKeycard? "keycard-crossed" : "keycard"
icon.color: Theme.palette.primaryColor1
onTriggered: {
if (model.keyPair.migratedToKeycard)
console.warn("TODO: stop using Keycard")
else
console.warn("TODO: move keys to a Keycard")
}
}
StatusAction {
text: enabled? qsTr("Rename keypair") : ""
enabled: !d.isProfileKeypair
icon.name: "edit"
icon.color: Theme.palette.primaryColor1
onTriggered: {
root.runRenameKeypairFlow()
}
}
StatusMenuSeparator {
visible: !d.isProfileKeypair
}
StatusAction {
text: enabled? qsTr("Remove master keys and associated accounts") : ""
enabled: !d.isProfileKeypair
type: StatusAction.Type.Danger
icon.name: "delete"
icon.color: Theme.palette.dangerColor1
onTriggered: {
console.warn("TODO: remove master keys and associated accounts")
}
}
}
}
},
StatusBaseText {
anchors.verticalCenter: parent.verticalCenter

View File

@ -32,7 +32,7 @@ Item {
label: qsTr("Display name")
placeholderText: qsTr("Display Name")
charLimit: 24
charLimit: Constants.keypair.nameLengthMax
validators: Constants.validators.displayName
input.tabNavItem: bioInput.input.edit

View File

@ -0,0 +1,153 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
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 StatusQ.Popups 0.1
import utils 1.0
import shared.controls 1.0
import "../stores"
StatusModal {
id: root
required property var accountsModule
required property string keyUid
required property string name
required property var accounts
headerSettings.title: qsTr("Rename keypair")
focus: visible
padding: Style.current.padding
QtObject {
id: d
property bool entryValid: false
function updateValidity() {
d.entryValid = nameInput.valid
if (!d.entryValid) {
return
}
d.entryValid = d.entryValid && nameInput.text !== root.name
if (!d.entryValid) {
nameInput.errorMessageCmp.text = qsTr("Same name")
nameInput.valid = false
return
}
d.entryValid = d.entryValid && !root.accountsModule.keypairNameExists(nameInput.text)
if (!d.entryValid) {
nameInput.errorMessageCmp.text = qsTr("Key name already in use")
nameInput.valid = false
}
}
function confirm() {
if (d.entryValid) {
root.accountsModule.renameKeypair(root.keyUid, nameInput.text)
root.close()
}
}
}
contentItem: ColumnLayout {
spacing: Style.current.halfPadding
StatusInput {
id: nameInput
Layout.preferredWidth: parent.width
Layout.preferredHeight: 120
topPadding: 8
bottomPadding: 8
label: qsTr("Key name")
charLimit: Constants.keypair.nameLengthMax
validators: Constants.validators.keypairName
input.clearable: true
input.rightPadding: 16
text: root.name
onTextChanged: {
d.updateValidity()
}
}
StatusBaseText {
Layout.preferredWidth: parent.width
Layout.topMargin: Style.current.padding
text: qsTr("Accounts derived from this key")
font.pixelSize: Style.current.primaryTextFontSize
}
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 60
color: "transparent"
radius: 8
border.width: 1
border.color: Theme.palette.baseColor2
StatusScrollView {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: contentHeight
padding: 0
leftPadding: 16
rightPadding: 16
Row {
spacing: 10
Repeater {
model: root.accounts
delegate: StatusListItemTag {
bgColor: Utils.getColorForId(model.account.colorId)
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
}
}
}
}
}
}
rightButtons: [
StatusFlatButton {
text: qsTr("Cancel")
type: StatusBaseButton.Type.Normal
onClicked: {
root.close()
}
},
StatusButton {
text: qsTr("Save changes")
enabled: d.entryValid
focus: true
Keys.onReturnPressed: function(event) {
d.confirm()
}
onClicked: {
d.confirm()
}
}
]
}

View File

@ -2,3 +2,4 @@ BackupSeedModal 1.0 BackupSeedModal.qml
SetupSyncingPopup 1.0 SetupSyncingPopup.qml
AddSocialLinkModal 1.0 AddSocialLinkModal.qml
ModifySocialLinkModal 1.0 ModifySocialLinkModal.qml
RenameKeypairPopup 1.0 RenameKeypairPopup.qml

View File

@ -105,7 +105,37 @@ Column {
includeWatchOnlyAccount: walletStore.includeWatchOnlyAccount
onGoToAccountView: root.goToAccountView(account)
onToggleIncludeWatchOnlyAccount: walletStore.toggleIncludeWatchOnlyAccount()
onRunRenameKeypairFlow: {
renameKeypairPopup.keyUid = model.keyPair.keyUid
renameKeypairPopup.name = model.keyPair.name
renameKeypairPopup.accounts = model.keyPair.accounts
renameKeypairPopup.active = true
}
}
}
}
Loader {
id: renameKeypairPopup
active: false
property string keyUid
property string name
property var accounts
sourceComponent: RenameKeypairPopup {
accountsModule: root.walletStore.accountsModule
keyUid: renameKeypairPopup.keyUid
name: renameKeypairPopup.name
accounts: renameKeypairPopup.accounts
onClosed: {
renameKeypairPopup.active = false
}
}
onLoaded: {
renameKeypairPopup.item.open()
}
}
}

View File

@ -1,4 +1,4 @@
NetworkSelectPopup 1.0 NetworkSelectPopup.qml
ActivityFilterMenu 1.0 ActivityFilterMenu.qml
ActivityPeriodFilterSubMenu 1.0 filterSubMenus/ActivityPeriodFilterSubMenu.qml
ActivityTypeFilterSubMenu 1.0 filterSubMenus/ActivityTypeFilterSubMenu.qml
ActivityTypeFilterSubMenu 1.0 filterSubMenus/ActivityTypeFilterSubMenu.qml

View File

@ -466,7 +466,28 @@ QtObject {
readonly property int blockedContacts: 6
}
readonly property QtObject keypair: QtObject {
readonly property int nameLengthMax: 20
readonly property int nameLengthMin: 5
}
readonly property QtObject validators: QtObject {
readonly property list<StatusValidator> keypairName: [
StatusValidator {
name: "startsWithSpaceValidator"
validate: function (t) { return !t.startsWith(" ") }
errorMessage: qsTr("Keypair starting with whitespace are not allowed")
},
StatusRegularExpressionValidator {
regularExpression: /^[a-zA-Z0-9\-_ ]+$/
errorMessage: errorMessages.alphanumericalExpandedRegExp
},
StatusMinLengthValidator {
minLength: keypair.nameLengthMin
errorMessage: qsTr("Keypair must be at least %n character(s)", "", keypair.nameLengthMin)
}
]
readonly property list<StatusValidator> displayName: [
StatusValidator {
name: "startsWithSpaceValidator"
@ -475,11 +496,11 @@ QtObject {
},
StatusRegularExpressionValidator {
regularExpression: /^[a-zA-Z0-9\-_ ]+$/
errorMessage: qsTr("Only letters, numbers, underscores, whitespaces and hyphens allowed")
errorMessage: errorMessages.alphanumericalExpandedRegExp
},
StatusMinLengthValidator {
minLength: 5
errorMessage: qsTr("Username must be at least 5 characters")
minLength: keypair.nameLengthMin
errorMessage: qsTr("Username must be at least %1 characters").arg(keypair.nameLengthMin)
},
StatusValidator {
name: "endsWithSpaceValidator"
@ -489,8 +510,8 @@ QtObject {
// TODO: Create `StatusMaxLengthValidator` in StatusQ
StatusValidator {
name: "maxLengthValidator"
validate: function (t) { return t.length <= 24 }
errorMessage: qsTr("24 character username limit")
validate: function (t) { return t.length <= keypair.nameLengthMax }
errorMessage: qsTr("%n character(s) username limit", "", keypair.nameLengthMax)
},
StatusValidator {
name: "endsWith-ethValidator"
@ -575,7 +596,7 @@ QtObject {
readonly property int enterSeedPhraseWordsHeight: 60
readonly property int keycardPinLength: 6
readonly property int keycardPukLength: 12
readonly property int keycardNameLength: 20
readonly property int keycardNameLength: keypair.nameLengthMax
readonly property int keycardNameInputWidth: 448
readonly property int keycardPairingCodeInputWidth: 512
readonly property int keycardPukAdditionalSpacingOnEvery4Items: 4