feat(TransactionModal): introduce async validation for ENS names

This commit extends the AddressInput to perform ENS lookups when valid
ENS values are entered. The lookup happens asynchronously, so we show a loading
indicator as the request is happening.

Closes #790
This commit is contained in:
Pascal Precht 2020-09-09 13:04:01 +02:00 committed by Iuri Matias
parent d64446f868
commit 729a2781f0
5 changed files with 83 additions and 18 deletions

View File

@ -5,6 +5,7 @@ import ../../status/libstatus/wallet as status_wallet
import ../../status/libstatus/tokens import ../../status/libstatus/tokens
import ../../status/libstatus/types import ../../status/libstatus/types
import ../../status/libstatus/utils as status_utils import ../../status/libstatus/utils as status_utils
import ../../status/ens as status_ens
import views/[asset_list, account_list, account_item, token_list, transaction_list, collectibles_list] import views/[asset_list, account_list, account_item, token_list, transaction_list, collectibles_list]
QtObject: QtObject:
@ -527,3 +528,11 @@ QtObject:
proc wei2Token*(self: WalletView, wei: string, decimals: int): string {.slot.} = proc wei2Token*(self: WalletView, wei: string, decimals: int): string {.slot.} =
return status_utils.wei2Token(wei, decimals) return status_utils.wei2Token(wei, decimals)
proc resolveENS*(self: WalletView, ens: string) {.slot.} =
spawnAndSend(self, "ensResolved") do:
status_ens.owner(ens)
proc ensWasResolved*(self: WalletView, resolvedPubKey: string) {.signal.}
proc ensResolved(self: WalletView, pubKey: string) {.slot.} =
self.ensWasResolved(pubKey)

View File

@ -236,11 +236,10 @@ ModalPopup {
id: btnNext id: btnNext
anchors.right: parent.right anchors.right: parent.right
label: qsTr("Next") label: qsTr("Next")
disabled: !stack.currentGroup.isValid disabled: !stack.currentGroup.isValid || stack.currentGroup.isPending
onClicked: { onClicked: {
const isValid = stack.currentGroup.validate() const validity = stack.currentGroup.validate()
if (validity.isValid && !validity.isPending) {
if (stack.currentGroup.validate()) {
if (stack.isLastGroup) { if (stack.isLastGroup) {
return root.sendTransaction() return root.sendTransaction()
} }

View File

@ -70,6 +70,13 @@ QtObject {
return /^0x[a-fA-F0-9]{40}$/.test(inputValue) return /^0x[a-fA-F0-9]{40}$/.test(inputValue)
} }
function isValidEns(inputValue) {
const isEmail = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(inputValue)
const isDomain = /(?:(?:(?<thld>[\w\-]*)(?:\.))?(?<sld>[\w\-]*))\.(?<tld>[\w\-]*)/.test(inputValue)
return isEmail || isDomain || inputValue.startsWith("@")
}
/** /**
* Removes trailing zeros from a string-representation of a number. Throws * Removes trailing zeros from a string-representation of a number. Throws
* if parameter is not a string * if parameter is not a string

View File

@ -3,13 +3,16 @@ import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13 import QtQuick.Layouts 1.13
import QtGraphicalEffects 1.13 import QtGraphicalEffects 1.13
import "../imports" import "../imports"
import "../shared"
Item { Item {
id: root id: root
property string validationError: "Error" property string validationError: "Error"
property string ensAsyncValidationError: qsTr("ENS Username not found")
property alias label: inpAddress.label property alias label: inpAddress.label
property string selectedAddress property string selectedAddress
property var isValid: false property var isValid: false
property bool isPending: false
height: inpAddress.height height: inpAddress.height
@ -20,24 +23,37 @@ Item {
isValid = false isValid = false
} }
function isValidEns(inputValue) {
// TODO: Check if the entered value resolves to an address. Long operation.
// Issue tracked: https://github.com/status-im/nim-status-client/issues/718
const isEmail = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(inputValue)
const isDomain = /(?:(?:(?<thld>[\w\-]*)(?:\.))?(?<sld>[\w\-]*))\.(?<tld>[\w\-]*)/.test(inputValue)
return isEmail || isDomain
}
function validate(inputValue) { function validate(inputValue) {
if (!inputValue) inputValue = selectedAddress if (!inputValue) inputValue = selectedAddress
let isValid = let isValid =
(inputValue && inputValue.startsWith("0x") && Utils.isValidAddress(inputValue)) || (inputValue && inputValue.startsWith("0x") && Utils.isValidAddress(inputValue) || Utils.isValidEns(inputValue))
isValidEns(inputValue)
inpAddress.validationError = isValid ? "" : validationError inpAddress.validationError = isValid ? "" : validationError
root.isValid = isValid root.isValid = isValid
return isValid return isValid
} }
property var validateAsync: Backpressure.debounce(inpAddress, 300, function (inputValue) {
root.isPending = true
var name = inputValue.startsWith("@") ? inputValue.substring(1) : inputValue
walletModel.resolveENS(name)
});
Connections {
target: walletModel
onEnsWasResolved: {
root.isPending = false
if (resolvedPubKey === ""){
inpAddress.validationError = root.ensAsyncValidationError
root.isValid = false
} else {
root.isValid = true
root.selectedAddress = resolvedPubKey
inpAddress.validationError = ""
}
}
}
Input { Input {
id: inpAddress id: inpAddress
//% "eg. 0x1234 or ENS" //% "eg. 0x1234 or ENS"
@ -64,7 +80,11 @@ Item {
metrics.text = text metrics.text = text
const isValid = root.validate(inputValue) const isValid = root.validate(inputValue)
if (isValid) { if (isValid) {
if (Utils.isValidAddress(inputValue)) {
root.selectedAddress = inputValue root.selectedAddress = inputValue
} else {
Qt.callLater(root.validateAsync, inputValue)
}
} }
} }
TextMetrics { TextMetrics {
@ -86,6 +106,22 @@ Item {
} }
} }
} }
Loader {
sourceComponent: loadingIndicator
anchors.top: inpAddress.bottom
anchors.right: inpAddress.right
anchors.topMargin: Style.current.halfPadding
active: root.isPending
}
Component {
id: loadingIndicator
LoadingImage {
width: 12
height: 12
}
}
} }

View File

@ -5,6 +5,7 @@ import "../imports"
Rectangle { Rectangle {
id: root id: root
property var isValid: true property var isValid: true
property var isPending: false
property var validate: function() { property var validate: function() {
let isValid = true let isValid = true
for (let i=0; i<children.length; i++) { for (let i=0; i<children.length; i++) {
@ -12,9 +13,13 @@ Rectangle {
if (component.hasOwnProperty("validate") && typeof component.validate === "function") { if (component.hasOwnProperty("validate") && typeof component.validate === "function") {
isValid = component.validate() isValid = component.validate()
} }
if (component.hasOwnProperty("isPending")) {
isPending = component.isPending
}
} }
root.isValid = isValid root.isValid = isValid
return isValid root.isPending = isPending
return { isValid, isPending }
} }
color: Style.current.background color: Style.current.background
function reset() { function reset() {
@ -41,19 +46,28 @@ Rectangle {
for (let i=0; i<children.length; i++) { for (let i=0; i<children.length; i++) {
const component = children[i] const component = children[i]
if (component.hasOwnProperty("isValid")) { if (component.hasOwnProperty("isValid")) {
component.isValidChanged.connect(updateGroupValidity) component.isValidChanged.connect(updateGroupValidityAndPendingStatus)
root.isValid = root.isValid && component.isValid // set the initial state root.isValid = root.isValid && component.isValid // set the initial state
} }
if (component.hasOwnProperty("isPending")) {
component.isPendingChanged.connect(updateGroupValidityAndPendingStatus)
root.isPending = component.isPending
}
} }
} }
function updateGroupValidity() { function updateGroupValidityAndPendingStatus() {
let isValid = true let isValid = true
let isPending = false
for (let i=0; i<children.length; i++) { for (let i=0; i<children.length; i++) {
const component = children[i] const component = children[i]
if (component.hasOwnProperty("isValid")) { if (component.hasOwnProperty("isValid")) {
isValid = isValid && component.isValid isValid = isValid && component.isValid
} }
if (component.hasOwnProperty("isPending")) {
isPending = component.isPending
}
} }
root.isValid = isValid root.isValid = isValid
root.isPending = isPending
} }
} }