feat(tx-comps): Send transaction modal
Fixes #669. Composes all tx components to create a send transaction modal for the wallet. 1. Add a reusable TransactionStackView component that wraps a StackView component to show the screens of the Send Tx modal and handles back/forward/reset functionality. 2. Add a reusable TransactionStackGroup which holds tx components and allows modal header and button text to be defined and handles validation for all child components. 3. Add an isValid property to all tx comps for pristine validation state. 4. Reset all components in modal once modal is closed. This consists of a `resetInternal` function that each component should implement to reinstate its original starting state, and a `reset` function that parent components can use to reinstate the overridden properties. 5. Tx error handling to display either a StatusGo error message in the dialog, or wrong password in the TransactionSigner. 6. Fix ReceiveModal to allow for pre-selected account based on current wallet account. 7. Add focused border colour to Input component. 8. Fix issue with last TransactionStackGroup input not being able to obtain focus. 9. Fix fiatBalance not appearing on initial load on AccountSelector. 10. Fix selected asset updated properly when assets changed in the AssetSelector component. 11. AccountSelector is pre-populated with selected wallet account. Supporting work on the components has been done to suppor this. 12. Changing accounts in the "from account" updates the asset balances in the AssetAndAmountInput component. 13. Move validation from ContactSelector to the Select component. 1. Test sending of tokens. This requires that tokens contracts are setup on testnet. Right now, they are set up for contract addresses on mainnet. 2. Loading state once transaction is sent. Button in modal needs to move to a loading state and the "toast" messages in the wallet need to appear informaing user of tx progress. 3. Need to clarify (and implement?) support of ENS names in the AddressInput. It appears that ENS names could be resolved. This would be a long operation and require some kind of UI loading indication. 4. Wallet balances need to be updated on every block, so for example, sending funds between accounts I should be able to see the balance updated in real time. 1. Sending to a contact currently doesn't work because the ContactSelector component selects the Contact's whipser key, instead of his/her wallet address. May need to figure out how this is done in status-react. As it stands, attempting to send to a contact will crash the app. 2. Sending *from* an imported account does not work, with an error from StatusGo `cannot locate account for address: 0x123...`
This commit is contained in:
parent
f6b1f31326
commit
1e020a203c
|
@ -3,6 +3,7 @@ import ../../status/[status, wallet, threads]
|
|||
import ../../status/wallet/collectibles as status_collectibles
|
||||
import ../../status/libstatus/wallet as status_wallet
|
||||
import ../../status/libstatus/tokens
|
||||
import ../../status/libstatus/types
|
||||
import ../../status/libstatus/utils
|
||||
import views/[asset_list, account_list, account_item, transaction_list, collectibles_list]
|
||||
|
||||
|
@ -48,7 +49,7 @@ QtObject:
|
|||
result.standardGasPrice = "0"
|
||||
result.fastGasPrice = "0"
|
||||
result.fastestGasPrice = "0"
|
||||
result.defaultGasLimit = "22000"
|
||||
result.defaultGasLimit = "21000"
|
||||
result.signingPhrase = ""
|
||||
result.setup
|
||||
|
||||
|
@ -231,8 +232,14 @@ QtObject:
|
|||
read = getAccountList
|
||||
notify = accountListChanged
|
||||
|
||||
proc onSendTransaction*(self: WalletView, from_value: string, to: string, assetAddress: string, value: string, password: string): string {.slot.} =
|
||||
return self.status.wallet.sendTransaction(from_value, to, assetAddress, value, password)
|
||||
proc sendTransaction*(self: WalletView, from_addr: string, to: string, assetAddress: string, value: string, gas: string, gasPrice: string, password: string): string {.slot.} =
|
||||
let resultJson = %*{}
|
||||
try:
|
||||
resultJson{"result"} = %self.status.wallet.sendTransaction(from_addr, to, assetAddress, value, gas, gasPrice, password)
|
||||
except StatusGoException as e:
|
||||
resultJson{"error"} = %e.msg
|
||||
finally:
|
||||
result = $resultJson
|
||||
|
||||
proc getDefaultAccount*(self: WalletView): string {.slot.} =
|
||||
self.currentAccount.address
|
||||
|
@ -257,7 +264,12 @@ QtObject:
|
|||
proc toggleAsset*(self: WalletView, symbol: string, checked: bool, address: string, name: string, decimals: int, color: string) {.slot.} =
|
||||
self.status.wallet.toggleAsset(symbol, checked, address, name, decimals, color)
|
||||
for account in self.status.wallet.accounts:
|
||||
self.accounts.updateAssetsInList(account.address, account.assetList)
|
||||
if account.address == self.currentAccount.address:
|
||||
self.currentAccount.setAccountItem(account)
|
||||
else:
|
||||
self.accounts.updateAssetsInList(account.address, account.assetList)
|
||||
self.accountListChanged()
|
||||
self.currentAccountChanged()
|
||||
|
||||
proc updateView*(self: WalletView) =
|
||||
self.totalFiatBalanceChanged()
|
||||
|
@ -362,7 +374,7 @@ QtObject:
|
|||
"collectibleType": status_collectibles.ETHERMON,
|
||||
"collectiblesOrError": status_collectibles.getEthermons(address)
|
||||
})
|
||||
of STICKER:
|
||||
of status_collectibles.STICKER:
|
||||
spawnAndSend(self, "setCollectiblesResult") do:
|
||||
$(%*{
|
||||
"address": address,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import NimQml, std/wrapnils
|
||||
import NimQml, std/wrapnils, strformat
|
||||
from ../../../status/wallet import WalletAccount
|
||||
import ./asset_list
|
||||
|
||||
|
@ -39,6 +39,10 @@ QtObject:
|
|||
proc balance*(self: AccountItemView): string {.slot.} = result = ?.self.account.balance
|
||||
QtProperty[string] balance:
|
||||
read = balance
|
||||
|
||||
proc fiatBalance*(self: AccountItemView): string {.slot.} = result = fmt"{?.self.account.realFiatBalance:>.2f}"
|
||||
QtProperty[string] fiatBalance:
|
||||
read = fiatBalance
|
||||
|
||||
proc path*(self: AccountItemView): string {.slot.} = result = ?.self.account.path
|
||||
QtProperty[string] path:
|
||||
|
|
|
@ -8,6 +8,13 @@ type SignalSubscriber* = ref object of RootObj
|
|||
type Signal* = ref object of RootObj
|
||||
signalType* {.serializedFieldName("type").}: SignalType
|
||||
|
||||
type StatusGoErrorDetail* = object
|
||||
message*: string
|
||||
code*: int
|
||||
|
||||
type StatusGoErrorExtended* = object
|
||||
error*: StatusGoErrorDetail
|
||||
|
||||
type StatusGoError* = object
|
||||
error*: string
|
||||
|
||||
|
|
|
@ -154,7 +154,7 @@ proc registerUsername*(username:string, address: EthAddress, pubKey: string, pas
|
|||
let
|
||||
register = Register(label: label, account: address, x: x, y: y)
|
||||
registerAbiEncoded = ensUsernamesContract.methods["register"].encodeAbi(register)
|
||||
approveAndCallObj = ApproveAndCall(to: ensUsernamesContract.address, value: price, data: DynamicBytes[132].fromHex(registerAbiEncoded))
|
||||
approveAndCallObj = ApproveAndCall(to: ensUsernamesContract.address, value: price, data: DynamicBytes[100].fromHex(registerAbiEncoded))
|
||||
approveAndCallAbiEncoded = sntContract.methods["approveAndCall"].encodeAbi(approveAndCallObj)
|
||||
|
||||
let payload = %* {
|
||||
|
|
|
@ -40,7 +40,7 @@ type
|
|||
ApproveAndCall* = object
|
||||
to*: EthAddress
|
||||
value*: Stuint[256]
|
||||
data*: DynamicBytes[132]
|
||||
data*: DynamicBytes[100]
|
||||
|
||||
Transfer* = object
|
||||
to*: EthAddress
|
||||
|
|
|
@ -134,7 +134,7 @@ proc buyPack*(packId: Stuint[256], address: EthAddress, price: Stuint[256], pass
|
|||
buyToken = BuyToken(packId: packId, address: address, price: price)
|
||||
buyTxAbiEncoded = stickerMktContract.methods["buyToken"].encodeAbi(buyToken)
|
||||
let
|
||||
approveAndCallObj = ApproveAndCall(to: stickerMktContract.address, value: price, data: DynamicBytes[132].fromHex(buyTxAbiEncoded))
|
||||
approveAndCallObj = ApproveAndCall(to: stickerMktContract.address, value: price, data: DynamicBytes[100].fromHex(buyTxAbiEncoded))
|
||||
approveAndCallAbiEncoded = sntContract.methods["approveAndCall"].encodeAbi(approveAndCallObj)
|
||||
let payload = %* {
|
||||
"from": $address,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import eventemitter, json
|
||||
import eth/common/eth_types, stew/byteutils, json_serialization, stint
|
||||
import eventemitter, json, options, typetraits, strutils
|
||||
import eth/common/eth_types, stew/byteutils, json_serialization, stint, faststreams/textio
|
||||
import accounts/constants
|
||||
|
||||
type SignalType* {.pure.} = enum
|
||||
|
@ -174,3 +174,56 @@ type
|
|||
name*: string
|
||||
etherscanLink* {.serializedFieldName("etherscan-link").}: string
|
||||
config*: NodeConfig
|
||||
|
||||
# TODO: Remove this when nim-web3 is added as a dependency
|
||||
Quantity* = distinct uint64
|
||||
|
||||
# TODO: Remove this when nim-web3 is added as a dependency
|
||||
EthSend* = object
|
||||
source*: EthAddress # the address the transaction is send from.
|
||||
to*: Option[EthAddress] # (optional when creating new contract) the address the transaction is directed to.
|
||||
gas*: Option[Quantity] # (optional, default: 90000) integer of the gas provided for the transaction execution. It will return unused gas.
|
||||
gasPrice*: Option[int] # (optional, default: To-Be-Determined) integer of the gasPrice used for each paid gas.
|
||||
value*: Option[Uint256] # (optional) integer of the value sent with this transaction.
|
||||
data*: string # the compiled code of a contract OR the hash of the invoked method signature and encoded parameters. For details see Ethereum Contract ABI.
|
||||
nonce*: Option[int] # (optional) integer of a nonce. This allows to overwrite your own pending transactions that use the same nonce
|
||||
|
||||
# TODO: Remove this when nim-web3 is added as a dependency
|
||||
template stripLeadingZeros(value: string): string =
|
||||
var cidx = 0
|
||||
# ignore the last character so we retain '0' on zero value
|
||||
while cidx < value.len - 1 and value[cidx] == '0':
|
||||
cidx.inc
|
||||
value[cidx .. ^1]
|
||||
|
||||
# TODO: Remove this when nim-web3 is added as a dependency
|
||||
proc encodeQuantity*(value: SomeUnsignedInt): string =
|
||||
var hValue = value.toHex.stripLeadingZeros
|
||||
result = "0x" & hValue
|
||||
|
||||
# TODO: Remove this when nim-web3 is added as a dependency
|
||||
proc `%`*(v: EthAddress): JsonNode =
|
||||
result = %("0x" & array[20, byte](v).toHex)
|
||||
|
||||
# TODO: Remove this when nim-web3 is added as a dependency
|
||||
proc `%`*(v: Quantity): JsonNode =
|
||||
result = %encodeQuantity(v.uint64)
|
||||
|
||||
# TODO: Remove this when nim-web3 is added as a dependency
|
||||
proc `%`*(n: Int256|UInt256): JsonNode = %("0x" & n.toHex)
|
||||
|
||||
# TODO: Remove this when nim-web3 is added as a dependency
|
||||
proc `%`*(x: EthSend): JsonNode =
|
||||
result = newJobject()
|
||||
result["from"] = %x.source
|
||||
if x.to.isSome:
|
||||
result["to"] = %x.to.unsafeGet
|
||||
if x.gas.isSome:
|
||||
result["gas"] = %x.gas.unsafeGet
|
||||
if x.gasPrice.isSome:
|
||||
result["gasPrice"] = %("0x" & x.gasPrice.unsafeGet.toHex.stripLeadingZeros)
|
||||
if x.value.isSome:
|
||||
result["value"] = %("0x" & x.value.unsafeGet.toHex)
|
||||
result["data"] = %x.data
|
||||
if x.nonce.isSome:
|
||||
result["nonce"] = %x.nonce.unsafeGet
|
|
@ -37,6 +37,24 @@ proc handleRPCErrors*(response: string) =
|
|||
if (parsedReponse.hasKey("error")):
|
||||
raise newException(ValueError, parsedReponse["error"]["message"].str)
|
||||
|
||||
proc toStUInt*[bits: static[int]](flt: float, T: typedesc[StUint[bits]]): T =
|
||||
var stringValue = fmt"{flt:<.0f}"
|
||||
stringValue.removeSuffix('.')
|
||||
result = parse($stringValue, StUint[bits])
|
||||
|
||||
proc toUInt256*(flt: float): UInt256 =
|
||||
toStUInt(flt, StUInt[256])
|
||||
|
||||
proc toUInt64*(flt: float): StUInt[64] =
|
||||
toStUInt(flt, StUInt[64])
|
||||
|
||||
proc eth2Wei*(eth: float, decimals: int = 18): UInt256 =
|
||||
let weiValue = eth * parseFloat(alignLeft("1", decimals + 1, '0'))
|
||||
weiValue.toUInt256
|
||||
|
||||
proc gwei2Wei*(gwei: float): UInt256 =
|
||||
eth2Wei(gwei, 9)
|
||||
|
||||
proc wei2Eth*(input: Stuint[256]): string =
|
||||
var one_eth = fromHex(Stuint[256], "DE0B6B3A7640000")
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import json, httpclient, json, strformat, stint, strutils, sequtils, chronicles, parseutils, tables
|
||||
import json, options
|
||||
import stint, chronicles, json_serialization
|
||||
import nim_status, core, types, utils
|
||||
import ../wallet/account
|
||||
from ./accounts/constants import ZERO_ADDRESS
|
||||
import ./contracts as contractMethods
|
||||
from eth/common/utils as eth_utils import parseAddress
|
||||
import eth/common/eth_types
|
||||
import ./types
|
||||
import ../../signals/types as signal_types
|
||||
|
||||
proc getWalletAccounts*(): seq[WalletAccount] =
|
||||
try:
|
||||
|
@ -59,46 +61,18 @@ proc getTransfersByAddress*(address: string): seq[types.Transaction] =
|
|||
except:
|
||||
let msg = getCurrentExceptionMsg()
|
||||
error "Failed getting wallet account transactions", msg
|
||||
|
||||
|
||||
proc sendTransaction*(from_address: string, to: string, assetAddress: string, value: string, password: string): string =
|
||||
proc sendTransaction*(tx: EthSend, password: string): string =
|
||||
let response = core.sendTransaction($(%tx), password)
|
||||
|
||||
try:
|
||||
var args: JsonNode
|
||||
if (assetAddress == ZERO_ADDRESS or assetAddress == ""):
|
||||
var weiValue = value.parseFloat() * float(1000000000000000000)
|
||||
var stringValue = fmt"{weiValue:<.0f}"
|
||||
stringValue.removeSuffix('.')
|
||||
var hexValue = parseBiggestInt(stringValue).toHex()
|
||||
hexValue.removePrefix('0')
|
||||
args = %* {
|
||||
"value": fmt"0x{hexValue}",
|
||||
"from": from_address,
|
||||
"to": to
|
||||
}
|
||||
else:
|
||||
var bigIntValue = u256(value)
|
||||
# TODO get decimals from the token
|
||||
bigIntValue = bigIntValue * u256(1000000000000000000)
|
||||
# Just using mainnet SNT to get the method ABI
|
||||
let
|
||||
contract = getContract("snt")
|
||||
transfer = Transfer(to: parseAddress(to), value: bigIntValue)
|
||||
transferEncoded = contract.methods["transfer"].encodeAbi(transfer)
|
||||
|
||||
args = %* {
|
||||
"from": from_address,
|
||||
"to": assetAddress,
|
||||
"data": transferEncoded
|
||||
}
|
||||
|
||||
let response = sendTransaction($args, password)
|
||||
let parsedResponse = parseJson(response)
|
||||
|
||||
result = parsedResponse["result"].getStr
|
||||
trace "Transaction sent succesfully", hash=result
|
||||
except Exception as e:
|
||||
error "Error submitting transaction", msg=e.msg
|
||||
result = e.msg
|
||||
except:
|
||||
let err = Json.decode(response, StatusGoErrorExtended)
|
||||
raise newException(StatusGoException, "Error sending transaction: " & err.error.message)
|
||||
|
||||
trace "Transaction sent succesfully", hash=result
|
||||
|
||||
proc getBalance*(address: string): string =
|
||||
let payload = %* [address, "latest"]
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import eventemitter, json, strformat, strutils, chronicles, sequtils, httpclient
|
||||
import json_serialization
|
||||
import eventemitter, json, strformat, strutils, chronicles, sequtils, httpclient, tables
|
||||
import json_serialization, stint
|
||||
from eth/common/utils import parseAddress
|
||||
import libstatus/accounts as status_accounts
|
||||
import libstatus/tokens as status_tokens
|
||||
import libstatus/settings as status_settings
|
||||
import libstatus/wallet as status_wallet
|
||||
import libstatus/accounts/constants as constants
|
||||
from libstatus/types import GeneratedAccount, DerivedAccount, Transaction, Setting, GasPricePrediction
|
||||
import libstatus/contracts as contracts
|
||||
from libstatus/types import GeneratedAccount, DerivedAccount, Transaction, Setting, GasPricePrediction, EthSend, Quantity, `%`, StatusGoException
|
||||
from libstatus/utils as libstatus_utils import eth2Wei, gwei2Wei, first, toUInt64
|
||||
import wallet/balance_manager
|
||||
import wallet/account
|
||||
import wallet/collectibles
|
||||
|
@ -48,8 +50,36 @@ proc initEvents*(self: WalletModel) =
|
|||
proc delete*(self: WalletModel) =
|
||||
discard
|
||||
|
||||
proc sendTransaction*(self: WalletModel, from_value: string, to: string, assetAddress: string, value: string, password: string): string =
|
||||
status_wallet.sendTransaction(from_value, to, assetAddress, value, password)
|
||||
proc sendTransaction*(self: WalletModel, source, to, assetAddress, value, gas, gasPrice, password: string): string =
|
||||
var
|
||||
weiValue = eth2Wei(parseFloat(value), 18) # ETH
|
||||
data = ""
|
||||
toAddr = parseAddress(to)
|
||||
let gasPriceInWei = gwei2Wei(parseFloat(gasPrice))
|
||||
|
||||
# TODO: this code needs to be tested with testnet assets (to be implemented in
|
||||
# a future PR
|
||||
if assetAddress != ZERO_ADDRESS and not assetAddress.isEmptyOrWhitespace:
|
||||
let
|
||||
token = self.tokens.first("address", assetAddress)
|
||||
contract = getContract("snt")
|
||||
transfer = Transfer(to: toAddr, value: weiValue)
|
||||
weiValue = eth2Wei(parseFloat(value), token["decimals"].getInt)
|
||||
data = contract.methods["transfer"].encodeAbi(transfer)
|
||||
toAddr = parseAddress(assetAddress)
|
||||
|
||||
let tx = EthSend(
|
||||
source: parseAddress(source),
|
||||
to: toAddr.some,
|
||||
gas: (if gas.isEmptyOrWhitespace: Quantity.none else: Quantity(cast[uint64](parseFloat(gas).toUInt64)).some),
|
||||
gasPrice: (if gasPrice.isEmptyOrWhitespace: int.none else: gwei2Wei(parseFloat(gasPrice)).truncate(int).some),
|
||||
value: weiValue.some,
|
||||
data: data
|
||||
)
|
||||
try:
|
||||
result = status_wallet.sendTransaction(tx, password)
|
||||
except StatusGoException as e:
|
||||
raise
|
||||
|
||||
proc getDefaultCurrency*(self: WalletModel): string =
|
||||
# TODO: this should come from a model? It is going to be used too in the
|
||||
|
|
|
@ -4,7 +4,7 @@ import "../../../imports"
|
|||
import "../../../shared"
|
||||
|
||||
ModalPopup {
|
||||
property string address: ""
|
||||
property alias selectedAccount: accountSelector.selectedAccount
|
||||
id: popup
|
||||
|
||||
//% "Receive"
|
||||
|
@ -27,7 +27,6 @@ ModalPopup {
|
|||
id: qrCodeImage
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: profileModel.qrCode(accountSelector.selectedAccount.address)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height - Style.current.padding
|
||||
|
@ -49,12 +48,18 @@ ModalPopup {
|
|||
width: 240
|
||||
dropdownWidth: parent.width - (Style.current.padding * 2)
|
||||
dropdownAlignment: Select.MenuAlignment.Center
|
||||
onSelectedAccountChanged: {
|
||||
if (selectedAccount.address) {
|
||||
qrCodeImage.source = profileModel.qrCode(selectedAccount.address)
|
||||
txtWalletAddress.text = selectedAccount.address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Input {
|
||||
id: txtWalletAddress
|
||||
//% "Wallet address"
|
||||
label: qsTrId("wallet-address")
|
||||
text: accountSelector.selectedAccount.address
|
||||
anchors.top: accountSelector.bottom
|
||||
anchors.topMargin: Style.current.padding
|
||||
copyToClipboard: true
|
||||
|
|
|
@ -1,27 +1,187 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.13
|
||||
import QtQuick.Dialogs 1.3
|
||||
import "../../../imports"
|
||||
import "../../../shared"
|
||||
import "../../../shared/status"
|
||||
import "./components"
|
||||
|
||||
ModalPopup {
|
||||
id: popup
|
||||
id: root
|
||||
|
||||
//% "Send"
|
||||
title: qsTrId("command-button-send")
|
||||
height: 700
|
||||
height: 504
|
||||
|
||||
onOpened: {
|
||||
sendModalContent.amountInput.selectedAmount = ""
|
||||
sendModalContent.passwordInput.text = ""
|
||||
sendModalContent.amountInput.forceActiveFocus(Qt.MouseFocusReason)
|
||||
property MessageDialog sendingError: MessageDialog {
|
||||
id: sendingError
|
||||
title: qsTr("Error sending the transaction")
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
property MessageDialog sendingSuccess: MessageDialog {
|
||||
id: sendingSuccess
|
||||
//% "Success sending the transaction"
|
||||
title: qsTrId("success-sending-the-transaction")
|
||||
icon: StandardIcon.NoIcon
|
||||
standardButtons: StandardButton.Ok
|
||||
onAccepted: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
SendModalContent {
|
||||
id: sendModalContent
|
||||
closePopup: function () {
|
||||
popup.close()
|
||||
onClosed: {
|
||||
stack.reset()
|
||||
}
|
||||
|
||||
function sendTransaction() {
|
||||
let responseStr = walletModel.sendTransaction(selectFromAccount.selectedAccount.address,
|
||||
selectRecipient.selectedRecipient.address,
|
||||
txtAmount.selectedAsset.address,
|
||||
txtAmount.selectedAmount,
|
||||
gasSelector.selectedGasLimit,
|
||||
gasSelector.selectedGasPrice,
|
||||
transactionSigner.enteredPassword)
|
||||
let response = JSON.parse(responseStr)
|
||||
|
||||
if (response.error) {
|
||||
if (response.error.includes("could not decrypt key with given password")){
|
||||
transactionSigner.validationError = qsTr("Wrong password")
|
||||
return
|
||||
}
|
||||
sendingError.text = response.error
|
||||
return sendingError.open()
|
||||
}
|
||||
|
||||
sendingSuccess.text = qsTr("Transaction sent to the blockchain. You can watch the progress on Etherscan: %2/%1").arg(response.result).arg(walletModel.etherscanLink)
|
||||
sendingSuccess.open()
|
||||
}
|
||||
|
||||
TransactionStackView {
|
||||
id: stack
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.current.padding
|
||||
anchors.rightMargin: Style.current.padding
|
||||
onGroupActivated: {
|
||||
root.title = group.headerText
|
||||
btnNext.label = group.footerText
|
||||
}
|
||||
TransactionFormGroup {
|
||||
id: group1
|
||||
headerText: qsTr("Send")
|
||||
footerText: qsTr("Continue")
|
||||
|
||||
AccountSelector {
|
||||
id: selectFromAccount
|
||||
accounts: walletModel.accounts
|
||||
selectedAccount: walletModel.currentAccount
|
||||
currency: walletModel.defaultCurrency
|
||||
width: stack.width
|
||||
label: qsTr("From account")
|
||||
reset: function() {
|
||||
accounts = Qt.binding(function() { return walletModel.accounts })
|
||||
selectedAccount = Qt.binding(function() { return walletModel.currentAccount })
|
||||
}
|
||||
}
|
||||
SeparatorWithIcon {
|
||||
id: separator
|
||||
anchors.top: selectFromAccount.bottom
|
||||
anchors.topMargin: 19
|
||||
}
|
||||
RecipientSelector {
|
||||
id: selectRecipient
|
||||
accounts: walletModel.accounts
|
||||
contacts: profileModel.addedContacts
|
||||
label: qsTr("Recipient")
|
||||
anchors.top: separator.bottom
|
||||
anchors.topMargin: 10
|
||||
width: stack.width
|
||||
reset: function() {
|
||||
accounts = Qt.binding(function() { return walletModel.accounts })
|
||||
contacts = Qt.binding(function() { return profileModel.addedContacts })
|
||||
selectedRecipient = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
TransactionFormGroup {
|
||||
id: group2
|
||||
headerText: qsTr("Send")
|
||||
footerText: qsTr("Preview")
|
||||
|
||||
AssetAndAmountInput {
|
||||
id: txtAmount
|
||||
selectedAccount: selectFromAccount.selectedAccount
|
||||
defaultCurrency: walletModel.defaultCurrency
|
||||
getFiatValue: walletModel.getFiatValue
|
||||
getCryptoValue: walletModel.getCryptoValue
|
||||
width: stack.width
|
||||
reset: function() {
|
||||
selectedAccount = Qt.binding(function() { return selectFromAccount.selectedAccount })
|
||||
}
|
||||
}
|
||||
GasSelector {
|
||||
id: gasSelector
|
||||
anchors.top: txtAmount.bottom
|
||||
anchors.topMargin: Style.current.bigPadding
|
||||
slowestGasPrice: parseFloat(walletModel.safeLowGasPrice)
|
||||
fastestGasPrice: parseFloat(walletModel.fastestGasPrice)
|
||||
getGasEthValue: walletModel.getGasEthValue
|
||||
getFiatValue: walletModel.getFiatValue
|
||||
defaultCurrency: walletModel.defaultCurrency
|
||||
width: stack.width
|
||||
reset: function() {
|
||||
slowestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.safeLowGasPrice) })
|
||||
fastestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.fastestGasPrice) })
|
||||
}
|
||||
}
|
||||
}
|
||||
TransactionFormGroup {
|
||||
id: group3
|
||||
headerText: qsTr("Transaction preview")
|
||||
footerText: qsTr("Sign with password")
|
||||
|
||||
TransactionPreview {
|
||||
id: pvwTransaction
|
||||
width: stack.width
|
||||
fromAccount: selectFromAccount.selectedAccount
|
||||
gas: {
|
||||
const value = walletModel.getGasEthValue(gasSelector.selectedGasPrice, gasSelector.selectedGasLimit)
|
||||
const fiatValue = walletModel.getFiatValue(value, "ETH", walletModel.defaultCurrency)
|
||||
return { value, "symbol": "ETH", fiatValue }
|
||||
}
|
||||
toAccount: selectRecipient.selectedRecipient
|
||||
asset: txtAmount.selectedAsset
|
||||
amount: { "value": txtAmount.selectedAmount, "fiatValue": txtAmount.selectedFiatAmount }
|
||||
currency: walletModel.defaultCurrency
|
||||
reset: function() {
|
||||
fromAccount = Qt.binding(function() { return selectFromAccount.selectedAccount })
|
||||
toAccount = Qt.binding(function() { return selectRecipient.selectedRecipient })
|
||||
asset = Qt.binding(function() { return txtAmount.selectedAsset })
|
||||
amount = Qt.binding(function() { return { "value": txtAmount.selectedAmount, "fiatValue": txtAmount.selectedFiatAmount } })
|
||||
const value = walletModel.getGasEthValue(gasSelector.selectedGasPrice, gasSelector.selectedGasLimit)
|
||||
const fiatValue = walletModel.getFiatValue(value, "ETH", walletModel.defaultCurrency)
|
||||
gas = Qt.binding(function() {
|
||||
const value = walletModel.getGasEthValue(gasSelector.selectedGasPrice, gasSelector.selectedGasLimit)
|
||||
const fiatValue = walletModel.getFiatValue(value, "ETH", walletModel.defaultCurrency)
|
||||
return { value, "symbol": "ETH", fiatValue }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
TransactionFormGroup {
|
||||
id: group4
|
||||
headerText: qsTr("Sign with password")
|
||||
footerText: qsTr("Send %1 %2").arg(txtAmount.selectedAmount).arg(!!txtAmount.selectedAsset ? txtAmount.selectedAsset.symbol : "")
|
||||
|
||||
TransactionSigner {
|
||||
id: transactionSigner
|
||||
width: stack.width
|
||||
signingPhrase: walletModel.signingPhrase
|
||||
reset: function() {
|
||||
signingPhrase = Qt.binding(function() { return walletModel.signingPhrase })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,38 +192,45 @@ ModalPopup {
|
|||
StyledButton {
|
||||
id: btnBack
|
||||
anchors.left: parent.left
|
||||
//% "Back"
|
||||
label: qsTrId("back")
|
||||
visible: !btnPreview.visible
|
||||
width: 44
|
||||
height: 44
|
||||
visible: !stack.isFirstGroup
|
||||
label: ""
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
border.width: 0
|
||||
radius: width / 2
|
||||
color: btnBack.disabled ? Style.current.grey :
|
||||
btnBack.hovered ? Qt.darker(btnBack.btnColor, 1.1) : btnBack.btnColor
|
||||
|
||||
SVGImage {
|
||||
width: 20.42
|
||||
height: 15.75
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../../img/arrow-right.svg"
|
||||
rotation: 180
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
btnPreview.visible = true
|
||||
sendModalContent.showInputs()
|
||||
stack.back()
|
||||
}
|
||||
}
|
||||
StyledButton {
|
||||
id: btnPreview
|
||||
id: btnNext
|
||||
anchors.right: parent.right
|
||||
//% "Preview"
|
||||
label: qsTrId("preview")
|
||||
label: qsTr("Next")
|
||||
disabled: !stack.currentGroup.isValid
|
||||
onClicked: {
|
||||
if (!sendModalContent.validate()) {
|
||||
return
|
||||
const isValid = stack.currentGroup.validate()
|
||||
|
||||
if (stack.currentGroup.validate()) {
|
||||
if (stack.isLastGroup) {
|
||||
return root.sendTransaction()
|
||||
}
|
||||
stack.next()
|
||||
}
|
||||
visible = false
|
||||
sendModalContent.showPreview()
|
||||
}
|
||||
}
|
||||
StyledButton {
|
||||
id: btnSend
|
||||
anchors.right: parent.right
|
||||
visible: !btnPreview.visible
|
||||
//% "Send"
|
||||
label: qsTrId("command-button-send") + " " + sendModalContent.amountInput.selectedAmount + " " + sendModalContent.amountInput.selectedAsset.symbol
|
||||
onClicked: {
|
||||
if (!sendModalContent.validatePassword()) {
|
||||
return
|
||||
}
|
||||
sendModalContent.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ Item {
|
|||
|
||||
ReceiveModal{
|
||||
id: receiveModal
|
||||
address: currentAccount.address
|
||||
selectedAccount: currentAccount
|
||||
}
|
||||
|
||||
SetCurrencyModal{
|
||||
|
|
|
@ -1,173 +0,0 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Dialogs 1.3
|
||||
import "../../../../imports"
|
||||
import "../../../../shared"
|
||||
|
||||
Item {
|
||||
id: sendModalContent
|
||||
property var closePopup: function(){}
|
||||
property alias amountInput: txtAmount
|
||||
property alias passwordInput: transactionSigner.passwordInput
|
||||
|
||||
property string passwordValidationError: ""
|
||||
|
||||
function send() {
|
||||
if (!validate() || !validatePassword()) {
|
||||
return;
|
||||
}
|
||||
let result = walletModel.onSendTransaction(selectFromAccount.selectedAccount.address,
|
||||
selectRecipient.selectedRecipient,
|
||||
txtAmount.selectedAsset.address,
|
||||
txtAmount.text,
|
||||
transactionSigner.passwordInput.text)
|
||||
|
||||
if (!result.startsWith('0x')) {
|
||||
// It's an error
|
||||
sendingError.text = result
|
||||
return sendingError.open()
|
||||
}
|
||||
|
||||
//% "Transaction sent to the blockchain. You can watch the progress on Etherscan: %2/%1"
|
||||
sendingSuccess.text = qsTrId("transaction-sent-to-the-blockchain--you-can-watch-the-progress-on-etherscan---2--1").arg(result).arg(walletModel.etherscanLink)
|
||||
sendingSuccess.open()
|
||||
}
|
||||
|
||||
function validatePassword() {
|
||||
if (transactionSigner.passwordInput.text === "") {
|
||||
//% "You need to enter a password"
|
||||
passwordValidationError = qsTrId("you-need-to-enter-a-password")
|
||||
} else if (transactionSigner.passwordInput.text.length < 4) {
|
||||
//% "Password needs to be 4 characters or more"
|
||||
passwordValidationError = qsTrId("password-needs-to-be-4-characters-or-more")
|
||||
} else {
|
||||
passwordValidationError = ""
|
||||
}
|
||||
|
||||
return passwordValidationError === ""
|
||||
}
|
||||
|
||||
function validate() {
|
||||
const isRecipientValid = selectRecipient.validate()
|
||||
const isAssetAndAmountValid = txtAmount.validate()
|
||||
|
||||
return isRecipientValid && isAssetAndAmountValid
|
||||
}
|
||||
|
||||
function showPreview() {
|
||||
pvwTransaction.visible = true
|
||||
transactionSigner.visible = true
|
||||
txtAmount.visible = selectFromAccount.visible = selectRecipient.visible = gasSelector.visible = false
|
||||
}
|
||||
|
||||
function showInputs() {
|
||||
pvwTransaction.visible = false
|
||||
transactionSigner.visible = false
|
||||
txtAmount.visible = selectFromAccount.visible = selectRecipient.visible = gasSelector.visible = true
|
||||
}
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
MessageDialog {
|
||||
id: sendingError
|
||||
title: "Error sending the transaction"
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
onAccepted: {
|
||||
sendModalContent.showInputs()
|
||||
}
|
||||
}
|
||||
MessageDialog {
|
||||
id: sendingSuccess
|
||||
//% "Success sending the transaction"
|
||||
title: qsTrId("success-sending-the-transaction")
|
||||
icon: StandardIcon.NoIcon
|
||||
standardButtons: StandardButton.Ok
|
||||
onAccepted: {
|
||||
closePopup()
|
||||
sendModalContent.showInputs()
|
||||
}
|
||||
}
|
||||
|
||||
AssetAndAmountInput {
|
||||
id: txtAmount
|
||||
selectedAccount: walletModel.currentAccount
|
||||
defaultCurrency: walletModel.defaultCurrency
|
||||
anchors.top: parent.top
|
||||
getFiatValue: walletModel.getFiatValue
|
||||
getCryptoValue: walletModel.getCryptoValue
|
||||
}
|
||||
|
||||
AccountSelector {
|
||||
id: selectFromAccount
|
||||
accounts: walletModel.accounts
|
||||
currency: walletModel.defaultCurrency
|
||||
anchors.top: txtAmount.bottom
|
||||
anchors.topMargin: Style.current.padding
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
//% "From account"
|
||||
label: qsTrId("from-account")
|
||||
onSelectedAccountChanged: {
|
||||
txtAmount.selectedAccount = selectFromAccount.selectedAccount
|
||||
}
|
||||
}
|
||||
|
||||
GasSelector {
|
||||
id: gasSelector
|
||||
anchors.top: selectFromAccount.bottom
|
||||
anchors.topMargin: Style.current.bigPadding
|
||||
slowestGasPrice: walletModel.safeLowGasPrice
|
||||
fastestGasPrice: walletModel.fastestGasPrice
|
||||
getGasEthValue: walletModel.getGasEthValue
|
||||
getFiatValue: walletModel.getFiatValue
|
||||
defaultCurrency: walletModel.defaultCurrency
|
||||
}
|
||||
|
||||
RecipientSelector {
|
||||
id: selectRecipient
|
||||
accounts: walletModel.accounts
|
||||
contacts: profileModel.addedContacts
|
||||
//% "Recipient"
|
||||
label: qsTrId("recipient")
|
||||
anchors.top: gasSelector.bottom
|
||||
anchors.topMargin: Style.current.padding
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
||||
TransactionPreview {
|
||||
id: pvwTransaction
|
||||
visible: false
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
fromAccount: selectFromAccount.selectedAccount
|
||||
gas: {
|
||||
const value = walletModel.getGasEthValue(gasSelector.selectedGasPrice, gasSelector.selectedGasLimit)
|
||||
const fiatValue = walletModel.getFiatValue(value, "ETH", walletModel.defaultCurrency)
|
||||
return { value, "symbol": "ETH", fiatValue }
|
||||
}
|
||||
toAccount: selectRecipient.selectedRecipient
|
||||
asset: txtAmount.selectedAsset
|
||||
amount: { "value": txtAmount.selectedAmount, "fiatValue": txtAmount.selectedFiatAmount }
|
||||
currency: walletModel.defaultCurrency
|
||||
}
|
||||
|
||||
TransactionSigner {
|
||||
id: transactionSigner
|
||||
visible: false
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: pvwTransaction.bottom
|
||||
anchors.topMargin: Style.current.smallPadding
|
||||
signingPhrase: walletModel.signingPhrase
|
||||
validationError: sendModalContent.passwordValidationError
|
||||
}
|
||||
}
|
||||
|
||||
/*##^##
|
||||
Designer {
|
||||
D{i:0;autoSize:true;formeditorColor:"#ffffff";height:480;width:640}
|
||||
}
|
||||
##^##*/
|
|
@ -32,6 +32,7 @@ Theme {
|
|||
property color currentUserTextColor: white
|
||||
property color secondaryBackground: "#23252F"
|
||||
property color inputBackground: secondaryBackground
|
||||
property color inputBorderFocus: blue
|
||||
property color inputColor: darkGrey
|
||||
property color modalBackground: background
|
||||
property color backgroundHover: "#252528"
|
||||
|
|
|
@ -31,6 +31,7 @@ Theme {
|
|||
property color currentUserTextColor: white
|
||||
property color secondaryBackground: lightBlue
|
||||
property color inputBackground: grey
|
||||
property color inputBorderFocus: blue
|
||||
property color inputColor: black
|
||||
property color modalBackground: white2
|
||||
property color backgroundHover: grey
|
||||
|
|
|
@ -331,6 +331,7 @@ DISTFILES += \
|
|||
shared/AccountSelector.qml \
|
||||
shared/AddButton.qml \
|
||||
shared/Address.qml \
|
||||
shared/FormGroup.qml \
|
||||
shared/IconButton.qml \
|
||||
shared/Input.qml \
|
||||
shared/LabelValueRow.qml \
|
||||
|
@ -344,6 +345,7 @@ DISTFILES += \
|
|||
shared/SearchBox.qml \
|
||||
shared/Select.qml \
|
||||
shared/Separator.qml \
|
||||
shared/SeparatorWithIcon.qml \
|
||||
shared/SplitViewHandle.qml \
|
||||
shared/StatusTabButton.qml \
|
||||
shared/StyledButton.qml \
|
||||
|
@ -355,6 +357,8 @@ DISTFILES += \
|
|||
shared/SVGImage.qml \
|
||||
shared/TextWithLabel.qml \
|
||||
shared/TransactionPreview.qml \
|
||||
shared/TransactionFormGroup.qml \
|
||||
shared/TransactionStackView.qml \
|
||||
shared/img/check.svg \
|
||||
shared/img/close.svg \
|
||||
shared/img/loading.png \
|
||||
|
|
|
@ -10,9 +10,7 @@ Item {
|
|||
property string label: qsTrId("choose-account")
|
||||
property bool showAccountDetails: true
|
||||
property var accounts
|
||||
property var selectedAccount: {
|
||||
"address": "", "name": "", "iconColor": "", "fiatBalance": ""
|
||||
}
|
||||
property var selectedAccount
|
||||
property string currency: "usd"
|
||||
height: select.height +
|
||||
(selectedAccountDetails.visible ? selectedAccountDetails.height : 0)
|
||||
|
@ -22,10 +20,39 @@ Item {
|
|||
property string showAssetBalance: ""
|
||||
property int dropdownWidth: width
|
||||
property alias dropdownAlignment: select.menuAlignment
|
||||
property bool isValid: true
|
||||
property var reset: function() {}
|
||||
|
||||
function resetInternal() {
|
||||
accounts = undefined
|
||||
selectedAccount = undefined
|
||||
isValid = true
|
||||
}
|
||||
|
||||
onSelectedAccountChanged: {
|
||||
if (!selectedAccount) {
|
||||
return
|
||||
}
|
||||
if (selectedAccount.iconColor) {
|
||||
selectedIconImgOverlay.color = selectedAccount.iconColor
|
||||
}
|
||||
if (selectedAccount.name) {
|
||||
selectedTextField.text = selectedAccount.name
|
||||
}
|
||||
if (selectedAccount.address) {
|
||||
textSelectedAddress.text = selectedAccount.address + " • "
|
||||
}
|
||||
if (selectedAccount.fiatBalance) {
|
||||
textSelectedAddressFiatBalance.text = selectedAccount.fiatBalance + " " + currency.toUpperCase()
|
||||
}
|
||||
if (selectedAccount.assets) {
|
||||
rptAccounts.model = selectedAccount.assets
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: rptAccounts
|
||||
visible: showAssetBalance !== ""
|
||||
model: selectedAccount.assets
|
||||
delegate: StyledText {
|
||||
visible: symbol === root.showAssetBalance.toUpperCase()
|
||||
anchors.bottom: select.top
|
||||
|
@ -64,19 +91,19 @@ Item {
|
|||
source: "../app/img/walletIcon.svg"
|
||||
}
|
||||
ColorOverlay {
|
||||
id: selectedIconImgOverlay
|
||||
anchors.fill: selectedIconImg
|
||||
source: selectedIconImg
|
||||
color: selectedAccount.iconColor
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: selectedTextField
|
||||
text: selectedAccount.name
|
||||
elide: Text.ElideRight
|
||||
anchors.left: selectedIconImg.right
|
||||
anchors.leftMargin: 8
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: select.selectedItemRightMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: select.contentWidth - (Style.current.padding + selectedIconImg.width + anchors.leftMargin)
|
||||
font.pixelSize: 15
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
height: 22
|
||||
|
@ -94,7 +121,6 @@ Item {
|
|||
|
||||
StyledText {
|
||||
id: textSelectedAddress
|
||||
text: selectedAccount.address + " • "
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideMiddle
|
||||
height: 16
|
||||
|
@ -102,7 +128,7 @@ Item {
|
|||
color: Style.current.secondaryText
|
||||
}
|
||||
StyledText {
|
||||
text: selectedAccount.fiatBalance + " " + root.currency.toUpperCase()
|
||||
id: textSelectedAddressFiatBalance
|
||||
font.pixelSize: 12
|
||||
height: 16
|
||||
color: Style.current.secondaryText
|
||||
|
@ -117,7 +143,7 @@ Item {
|
|||
property bool isLastItem: index === accounts.rowCount() - 1
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.selectedAccount.address === "") {
|
||||
if (!root.selectedAccount && isFirstItem) {
|
||||
root.selectedAccount = { address, name, iconColor, assets, fiatBalance }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,17 @@ Item {
|
|||
property string validationError: "Error"
|
||||
property alias label: inpAddress.label
|
||||
property string selectedAddress
|
||||
property var isValid: false
|
||||
|
||||
height: inpAddress.height
|
||||
|
||||
function resetInternal() {
|
||||
selectedAddress = ""
|
||||
inpAddress.resetInternal()
|
||||
metrics.text = ""
|
||||
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
|
||||
|
@ -26,6 +34,7 @@ Item {
|
|||
(inputValue && inputValue.startsWith("0x") && Utils.isValidAddress(inputValue)) ||
|
||||
isValidEns(inputValue)
|
||||
inpAddress.validationError = isValid ? "" : validationError
|
||||
root.isValid = isValid
|
||||
return isValid
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,14 @@ Item {
|
|||
property var sources: []
|
||||
property string selectedSource: sources[0] || "Address"
|
||||
property int dropdownWidth: 220
|
||||
property var reset: function() {}
|
||||
height: select.height
|
||||
|
||||
function resetInternal() {
|
||||
sources = []
|
||||
selectedSource = sources[0] || "Address"
|
||||
}
|
||||
|
||||
Select {
|
||||
id: select
|
||||
anchors.left: parent.left
|
||||
|
|
|
@ -21,6 +21,18 @@ Item {
|
|||
property var getFiatValue: function () {}
|
||||
property var getCryptoValue: function () {}
|
||||
property bool isDirty: false
|
||||
property bool isValid: false
|
||||
property var reset: function() {}
|
||||
|
||||
function resetInternal() {
|
||||
selectAsset.resetInternal()
|
||||
selectedAccount = undefined
|
||||
txtFiatBalance.text = "0.00"
|
||||
inputAmount.resetInternal()
|
||||
txtBalanceDesc.color = Style.current.secondaryText
|
||||
txtBalance.color = Qt.binding(function() { return txtBalance.hovered ? Style.current.textColor : Style.current.secondaryText })
|
||||
isValid = false
|
||||
}
|
||||
|
||||
id: root
|
||||
|
||||
|
@ -57,14 +69,19 @@ Item {
|
|||
txtBalanceDesc.color = Style.current.secondaryText
|
||||
txtBalance.color = Qt.binding(function() { return txtBalance.hovered ? Style.current.textColor : Style.current.secondaryText })
|
||||
}
|
||||
root.isValid = isValid
|
||||
return isValid
|
||||
}
|
||||
|
||||
onSelectedAccountChanged: {
|
||||
if (!selectAsset.selectedAsset) {
|
||||
return
|
||||
}
|
||||
txtBalance.text = Utils.stripTrailingZeros(selectAsset.selectedAsset.value)
|
||||
selectAsset.assets = Qt.binding(function() {
|
||||
if (selectedAccount) {
|
||||
return selectedAccount.assets
|
||||
}
|
||||
})
|
||||
txtBalance.text = Qt.binding(function() {
|
||||
return selectAsset.selectedAsset ? Utils.stripTrailingZeros(selectAsset.selectedAsset.value) : ""
|
||||
})
|
||||
}
|
||||
|
||||
Item {
|
||||
|
@ -91,9 +108,6 @@ Item {
|
|||
font.weight: Font.Medium
|
||||
font.pixelSize: 13
|
||||
color: hovered ? Style.current.textColor : Style.current.secondaryText
|
||||
onTextChanged: {
|
||||
root.validate(true)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
@ -106,7 +120,7 @@ Item {
|
|||
txtBalance.hovered = true
|
||||
}
|
||||
onClicked: {
|
||||
inputAmount.text = selectAsset.selectedAsset.value
|
||||
inputAmount.text = Utils.stripTrailingZeros(selectAsset.selectedAsset.value)
|
||||
txtFiatBalance.text = root.getFiatValue(inputAmount.text, selectAsset.selectedAsset.symbol, root.defaultCurrency)
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +137,6 @@ Item {
|
|||
validationErrorAlignment: TextEdit.AlignRight
|
||||
validationErrorTopMargin: 8
|
||||
Keys.onReleased: {
|
||||
root.isDirty = true
|
||||
let amount = inputAmount.text.trim()
|
||||
|
||||
if (isNaN(amount)) {
|
||||
|
@ -136,13 +149,13 @@ Item {
|
|||
}
|
||||
}
|
||||
onTextChanged: {
|
||||
root.isDirty = true
|
||||
root.validate(true)
|
||||
}
|
||||
}
|
||||
|
||||
AssetSelector {
|
||||
id: selectAsset
|
||||
assets: root.selectedAccount.assets
|
||||
width: 86
|
||||
height: 28
|
||||
anchors.top: inputAmount.top
|
||||
|
@ -150,11 +163,15 @@ Item {
|
|||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.current.smallPadding
|
||||
onSelectedAssetChanged: {
|
||||
if (!selectAsset.selectedAsset) {
|
||||
return
|
||||
}
|
||||
txtBalance.text = Utils.stripTrailingZeros(selectAsset.selectedAsset.value)
|
||||
if (inputAmount.text === "" || isNaN(inputAmount.text)) {
|
||||
return
|
||||
}
|
||||
txtFiatBalance.text = root.getFiatValue(inputAmount.text, selectAsset.selectedAsset.symbol, root.defaultCurrency)
|
||||
root.validate(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,11 @@ Item {
|
|||
width: 86
|
||||
height: 24
|
||||
|
||||
function resetInternal() {
|
||||
assets = undefined
|
||||
selectedAsset = undefined
|
||||
}
|
||||
|
||||
onSelectedAssetChanged: {
|
||||
if (selectedAsset && selectedAsset.symbol) {
|
||||
iconImg.source = "../app/img/tokens/" + selectedAsset.symbol.toUpperCase() + ".png"
|
||||
|
@ -18,13 +23,26 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
onAssetsChanged: {
|
||||
if (!assets) {
|
||||
return
|
||||
}
|
||||
selectedAsset = {
|
||||
name: assets.rowData(0, "name"),
|
||||
symbol: assets.rowData(0, "symbol"),
|
||||
value: assets.rowData(0, "value"),
|
||||
fiatBalanceDisplay: assets.rowData(0, "fiatBalanceDisplay"),
|
||||
address: assets.rowData(0, "address"),
|
||||
fiatBalance: assets.rowData(0, "fiatBalance")
|
||||
}
|
||||
}
|
||||
|
||||
Select {
|
||||
id: select
|
||||
model: root.assets
|
||||
width: parent.width
|
||||
bgColor: Style.current.transparent
|
||||
bgColorHover: Style.current.secondaryHover
|
||||
|
||||
model: root.assets
|
||||
caretRightMargin: 7
|
||||
select.radius: 6
|
||||
select.height: root.height
|
||||
|
@ -62,11 +80,6 @@ Item {
|
|||
property bool isFirstItem: index === 0
|
||||
property bool isLastItem: index === assets.rowCount() - 1
|
||||
|
||||
Component.onCompleted: {
|
||||
if (isFirstItem) {
|
||||
root.selectedAsset = { address, name, value, symbol, fiatBalanceDisplay, fiatBalance }
|
||||
}
|
||||
}
|
||||
width: parent.width
|
||||
height: 72
|
||||
SVGImage {
|
||||
|
|
|
@ -8,10 +8,20 @@ Item {
|
|||
id: root
|
||||
property var contacts
|
||||
property var selectedContact
|
||||
height: select.height + (validationErrorText.visible ? validationErrorText.height : 0)
|
||||
height: select.height
|
||||
property int dropdownWidth: width
|
||||
property string validationError: validationErrorText.text
|
||||
property alias validationErrorAlignment: validationErrorText.horizontalAlignment
|
||||
//% "Please select a contact"
|
||||
property string validationError: qsTrId("please-select-a-contact")
|
||||
property alias validationErrorAlignment: select.validationErrorAlignment
|
||||
property bool isValid: false
|
||||
property var reset: function() {}
|
||||
|
||||
function resetInternal() {
|
||||
contacts = undefined
|
||||
selectedContact = undefined
|
||||
select.validationError = ""
|
||||
isValid = false
|
||||
}
|
||||
|
||||
onContactsChanged: {
|
||||
//% "Select a contact"
|
||||
|
@ -19,16 +29,9 @@ Item {
|
|||
}
|
||||
|
||||
function validate() {
|
||||
const isValid = root.selectedContact && root.selectedContact.address
|
||||
if (!isValid) {
|
||||
select.select.border.color = Style.current.danger
|
||||
select.select.border.width = 1
|
||||
validationErrorText.visible = true
|
||||
} else {
|
||||
select.select.border.color = Style.current.transparent
|
||||
select.select.border.width = 0
|
||||
validationErrorText.visible = false
|
||||
}
|
||||
const isValid = !!(selectedContact && selectedContact.address)
|
||||
select.validationError = !isValid ? validationError : ""
|
||||
root.isValid = isValid
|
||||
return isValid
|
||||
}
|
||||
|
||||
|
@ -46,14 +49,14 @@ Item {
|
|||
anchors.leftMargin: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 32
|
||||
width: !!selectedContact.identicon ? 32 : 0
|
||||
visible: !!selectedContact.identicon
|
||||
source: selectedContact.identicon ? selectedContact.identicon : ""
|
||||
width: (!!selectedContact && !!selectedContact.identicon) ? 32 : 0
|
||||
visible: !!selectedContact && !!selectedContact.identicon
|
||||
source: (!!selectedContact && !!selectedContact.identicon) ? selectedContact.identicon : ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: selectedTextField
|
||||
text: selectedContact.name
|
||||
text: !!selectedContact ? selectedContact.name : ""
|
||||
anchors.left: iconImg.right
|
||||
anchors.leftMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
@ -79,21 +82,6 @@ Item {
|
|||
menu.delegate: menuItem
|
||||
menu.width: dropdownWidth
|
||||
}
|
||||
TextEdit {
|
||||
id: validationErrorText
|
||||
visible: false
|
||||
//% "Please select a contact"
|
||||
text: qsTrId("please-select-a-contact")
|
||||
anchors.top: select.bottom
|
||||
anchors.topMargin: 8
|
||||
selectByMouse: true
|
||||
readOnly: true
|
||||
font.pixelSize: 12
|
||||
height: 16
|
||||
color: Style.current.danger
|
||||
width: parent.width
|
||||
horizontalAlignment: TextEdit.AlignRight
|
||||
}
|
||||
|
||||
Component {
|
||||
id: menuItem
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import "../imports"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property var isValid: true
|
||||
property var validate: function() {
|
||||
let isValid = true
|
||||
for (let i=0; i<children.length; i++) {
|
||||
const component = children[i]
|
||||
if (component.hasOwnProperty("validate") && typeof component.validate === "function") {
|
||||
isValid = component.validate()
|
||||
}
|
||||
}
|
||||
root.isValid = isValid
|
||||
return isValid
|
||||
}
|
||||
color: Style.current.background
|
||||
function reset() {
|
||||
for (let i=0; i<children.length; i++) {
|
||||
const component = children[i]
|
||||
try {
|
||||
if (component.hasOwnProperty("resetInternal") && typeof component.resetInternal === "function") {
|
||||
component.resetInternal()
|
||||
}
|
||||
if (component.hasOwnProperty("reset") && typeof component.reset === "function") {
|
||||
component.reset()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Error resetting component", i, ":", e.message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
StackView.onActivated: {
|
||||
// parent refers to the StackView
|
||||
parent.groupActivated(this)
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (let i=0; i<children.length; i++) {
|
||||
const component = children[i]
|
||||
if (component.hasOwnProperty("isValid")) {
|
||||
component.isValidChanged.connect(updateGroupValidity)
|
||||
root.isValid = root.isValid && component.isValid // set the initial state
|
||||
}
|
||||
}
|
||||
}
|
||||
function updateGroupValidity() {
|
||||
let isValid = true
|
||||
for (let i=0; i<children.length; i++) {
|
||||
const component = children[i]
|
||||
if (component.hasOwnProperty("isValid")) {
|
||||
isValid = isValid && component.isValid
|
||||
}
|
||||
}
|
||||
root.isValid = isValid
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ Item {
|
|||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: sliderWrapper.height + Style.current.smallPadding + txtNetworkFee.height + buttonAdvanced.height
|
||||
property string validationError: "Please enter a number"
|
||||
property double slowestGasPrice: 0
|
||||
property double fastestGasPrice: 100
|
||||
property double stepSize: ((root.fastestGasPrice - root.slowestGasPrice) / 10).toFixed(1)
|
||||
|
@ -18,11 +17,26 @@ Item {
|
|||
property string defaultCurrency: "USD"
|
||||
property alias selectedGasPrice: inputGasPrice.text
|
||||
property alias selectedGasLimit: inputGasLimit.text
|
||||
property string greaterThan0ErrorMessage: qsTr("Must be greater than 0")
|
||||
//% "This needs to be a number"
|
||||
property string invalidInputErrorMessage: qsTrId("this-needs-to-be-a-number")
|
||||
property string noInputErrorMessage: qsTr("Please enter an amount")
|
||||
property bool isValid: true
|
||||
property var reset: function() {}
|
||||
|
||||
function defaultGasPrice() {
|
||||
return ((50 * (root.fastestGasPrice - root.slowestGasPrice) / 100) + root.slowestGasPrice)
|
||||
}
|
||||
|
||||
function resetInternal() {
|
||||
slowestGasPrice = 0
|
||||
fastestGasPrice = 100
|
||||
inputGasLimit.text = "21000"
|
||||
customNetworkFeeDialog.isValid = true
|
||||
inputGasPrice.text = Qt.binding(defaultGasPrice)
|
||||
gasSlider.value = Qt.binding(defaultGasPrice)
|
||||
}
|
||||
|
||||
function updateGasEthValue() {
|
||||
// causes error on application load without this null check
|
||||
if (!inputGasPrice || !inputGasLimit) {
|
||||
|
@ -35,10 +49,6 @@ Item {
|
|||
labelGasPriceSummaryAdvanced.text = summary
|
||||
}
|
||||
|
||||
function validate(value) {
|
||||
return !isNaN(value)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: txtNetworkFee
|
||||
anchors.top: parent.top
|
||||
|
@ -157,24 +167,63 @@ Item {
|
|||
title: qsTrId("custom-network-fee")
|
||||
height: 286
|
||||
width: 400
|
||||
property bool isValid: true
|
||||
|
||||
onIsValidChanged: {
|
||||
root.isValid = isValid
|
||||
}
|
||||
|
||||
function validate() {
|
||||
// causes error on application load without a null check
|
||||
if (!inputGasLimit || !inputGasPrice) {
|
||||
return
|
||||
}
|
||||
inputGasLimit.validationError = ""
|
||||
inputGasPrice.validationError = ""
|
||||
const noInputLimit = inputGasLimit.text === ""
|
||||
const noInputPrice = inputGasPrice.text === ""
|
||||
if (noInputLimit) {
|
||||
inputGasLimit.validationError = root.noInputErrorMessage
|
||||
}
|
||||
if (noInputPrice) {
|
||||
inputGasPrice.validationError = root.noInputErrorMessage
|
||||
}
|
||||
if (isNaN(inputGasLimit.text)) {
|
||||
inputGasLimit.validationError = invalidInputErrorMessage
|
||||
}
|
||||
if (isNaN(inputGasPrice.text)) {
|
||||
inputGasPrice.validationError = invalidInputErrorMessage
|
||||
}
|
||||
let inputLimit = parseFloat(inputGasLimit.text || "0.00")
|
||||
let inputPrice = parseFloat(inputGasPrice.text || "0.00")
|
||||
if (inputLimit === 0.00) {
|
||||
inputGasLimit.validationError = root.greaterThan0ErrorMessage
|
||||
}
|
||||
if (inputPrice === 0.00) {
|
||||
inputGasPrice.validationError = root.greaterThan0ErrorMessage
|
||||
}
|
||||
const isValid = inputGasLimit.validationError === "" && inputGasPrice.validationError === ""
|
||||
customNetworkFeeDialog.isValid = isValid
|
||||
return isValid
|
||||
}
|
||||
|
||||
Input {
|
||||
id: inputGasLimit
|
||||
//% "Gas limit"
|
||||
label: qsTrId("gas-limit")
|
||||
text: "22000"
|
||||
text: "21000"
|
||||
customHeight: 56
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: inputGasPrice.left
|
||||
anchors.rightMargin: Style.current.padding
|
||||
placeholderText: "21000"
|
||||
validationErrorAlignment: TextEdit.AlignRight
|
||||
validationErrorTopMargin: 8
|
||||
onTextChanged: {
|
||||
if (root.validate(inputGasLimit.text.trim())) {
|
||||
inputGasLimit.validationError = ""
|
||||
if (customNetworkFeeDialog.validate()) {
|
||||
root.updateGasEthValue()
|
||||
return
|
||||
}
|
||||
inputGasLimit.validationError = root.validationError
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,13 +237,14 @@ Item {
|
|||
width: 130
|
||||
customHeight: 56
|
||||
text: root.defaultGasPrice()
|
||||
placeholderText: "21000"
|
||||
onTextChanged: {
|
||||
if (root.validate(inputGasPrice.text.trim())) {
|
||||
inputGasPrice.validationError = ""
|
||||
root.updateGasEthValue()
|
||||
return
|
||||
if (inputGasPrice.text.trim() === "") {
|
||||
inputGasPrice.text = root.defaultGasPrice()
|
||||
}
|
||||
if (customNetworkFeeDialog.validate()) {
|
||||
root.updateGasEthValue()
|
||||
}
|
||||
inputGasPrice.validationError = root.validationError
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
@ -223,12 +273,13 @@ Item {
|
|||
id: applyButton
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.current.smallPadding
|
||||
//% "Apply"
|
||||
label: qsTrId("invalid-key-confirm")
|
||||
disabled: !root.validate(inputGasLimit.text.trim()) || !root.validate(inputGasPrice.text.trim())
|
||||
label: qsTr("Apply")
|
||||
anchors.bottom: parent.bottom
|
||||
disabled: !customNetworkFeeDialog.isValid
|
||||
onClicked: {
|
||||
root.updateGasEthValue()
|
||||
if (customNetworkFeeDialog.validate()) {
|
||||
root.updateGasEthValue()
|
||||
}
|
||||
customNetworkFeeDialog.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,11 @@ Item {
|
|||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
function resetInternal() {
|
||||
inputValue.text = ""
|
||||
validationError = ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: inputLabel
|
||||
text: inputBox.label
|
||||
|
@ -55,15 +60,22 @@ Item {
|
|||
anchors.topMargin: inputBox.hasLabel ? inputBox.labelMargin : 0
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
border.width: !!validationError ? 1 : 0
|
||||
border.color: Style.current.red
|
||||
border.width: (!!validationError || inputValue.focus) ? 1 : 0
|
||||
border.color: {
|
||||
if (!!validationError) {
|
||||
return Style.current.danger
|
||||
}
|
||||
if (inputValue.focus) {
|
||||
return Style.current.inputBorderFocus
|
||||
}
|
||||
return Style.current.transparent
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: inputValue
|
||||
visible: !inputBox.isTextArea && !inputBox.isSelect
|
||||
placeholderText: inputBox.placeholderText
|
||||
placeholderTextColor: inputBox.placeholderTextColor
|
||||
text: inputBox.text
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
|
|
|
@ -17,7 +17,6 @@ Item {
|
|||
font.pixelSize: 15
|
||||
height: parent.height
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.current.padding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: 105
|
||||
|
|
|
@ -50,7 +50,6 @@ Popup {
|
|||
anchors.left: parent.left
|
||||
font.bold: true
|
||||
font.pixelSize: 17
|
||||
anchors.leftMargin: 16
|
||||
anchors.topMargin: Style.current.padding
|
||||
anchors.bottomMargin: Style.current.padding
|
||||
visible: !!title
|
||||
|
|
|
@ -18,7 +18,25 @@ Item {
|
|||
height: (readOnly ? inpReadOnly.height : inpAddress.height) + txtLabel.height
|
||||
//% "Invalid ethereum address"
|
||||
readonly property string addressValidationError: qsTrId("invalid-ethereum-address")
|
||||
property bool isValid: false
|
||||
property var reset: function() {}
|
||||
readonly property var sources: [
|
||||
qsTr("Address"),
|
||||
qsTr("Contact"),
|
||||
qsTr("My account")
|
||||
]
|
||||
|
||||
function resetInternal() {
|
||||
inpAddress.resetInternal()
|
||||
selContact.resetInternal()
|
||||
selAccount.resetInternal()
|
||||
selAddressSource.resetInternal()
|
||||
selContact.reset()
|
||||
selAccount.reset()
|
||||
selAddressSource.reset()
|
||||
isValid = false
|
||||
}
|
||||
|
||||
enum Type {
|
||||
Address,
|
||||
Contact,
|
||||
|
@ -37,6 +55,7 @@ Item {
|
|||
} else if (selAddressSource.selectedSource === "Contact") {
|
||||
isValid = selContact.validate()
|
||||
}
|
||||
root.isValid = isValid
|
||||
return isValid
|
||||
}
|
||||
|
||||
|
@ -108,6 +127,11 @@ Item {
|
|||
}
|
||||
root.selectedRecipient = { address: selectedAddress, type: RecipientSelector.Type.Address }
|
||||
}
|
||||
onIsValidChanged: {
|
||||
if (selAddressSource.selectedSource === "Address") {
|
||||
root.isValid = isValid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContactSelector {
|
||||
|
@ -119,6 +143,9 @@ Item {
|
|||
Layout.preferredWidth: selAddressSource.visible ? root.inputWidth : parent.width
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
reset: function() {
|
||||
contacts = root.contacts
|
||||
}
|
||||
onSelectedContactChanged: {
|
||||
if (root.readOnly) {
|
||||
return
|
||||
|
@ -128,6 +155,11 @@ Item {
|
|||
root.selectedRecipient = { address, name, alias, isContact, identicon, ensVerified, type: RecipientSelector.Type.Contact }
|
||||
}
|
||||
}
|
||||
onIsValidChanged: {
|
||||
if (selAddressSource.selectedSource === "Contact") {
|
||||
root.isValid = isValid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccountSelector {
|
||||
|
@ -140,8 +172,11 @@ Item {
|
|||
Layout.preferredWidth: selAddressSource.visible ? root.inputWidth : parent.width
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
reset: function() {
|
||||
accounts = root.accounts
|
||||
}
|
||||
onSelectedAccountChanged: {
|
||||
if (root.readOnly) {
|
||||
if (root.readOnly || !selectedAccount) {
|
||||
return
|
||||
}
|
||||
const { address, name, iconColor, assets, fiatBalance } = selectedAccount
|
||||
|
@ -151,11 +186,13 @@ Item {
|
|||
AddressSourceSelector {
|
||||
id: selAddressSource
|
||||
visible: !root.readOnly
|
||||
sources: ["Address", "Contact", "My account"]
|
||||
sources: root.sources
|
||||
width: sourceSelectWidth
|
||||
Layout.preferredWidth: root.sourceSelectWidth
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
reset: function() {
|
||||
sources = root.sources
|
||||
}
|
||||
onSelectedSourceChanged: {
|
||||
if (root.readOnly) {
|
||||
return
|
||||
|
@ -167,6 +204,7 @@ Item {
|
|||
selContact.visible = selAccount.visible = false
|
||||
root.height = Qt.binding(function() { return inpAddress.height + txtLabel.height })
|
||||
root.selectedRecipient = { address: inpAddress.selectedAddress, type: RecipientSelector.Type.Address }
|
||||
root.isValid = inpAddress.isValid
|
||||
break;
|
||||
case "Contact":
|
||||
selContact.visible = true
|
||||
|
@ -176,6 +214,7 @@ Item {
|
|||
address = selContact.selectedContact.address
|
||||
name = selContact.selectedContact.name
|
||||
root.selectedRecipient = { address, name, alias, isContact, identicon, ensVerified, type: RecipientSelector.Type.Contact }
|
||||
root.isValid = selContact.isValid
|
||||
break;
|
||||
case "My account":
|
||||
selAccount.visible = true
|
||||
|
@ -185,6 +224,7 @@ Item {
|
|||
address = selAccount.selectedAccount.address
|
||||
name = selAccount.selectedAccount.name
|
||||
root.selectedRecipient = { address, name, iconColor, assets, fiatBalance, type: RecipientSelector.Type.Account }
|
||||
root.isValid = selAccount.isValid
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import QtQuick 2.13
|
|||
// Source: https://forum.qt.io/topic/52161/properly-scaling-svg-images/6
|
||||
|
||||
Image {
|
||||
sourceSize: Qt.size(hiddenImg.sourceSize.width * 2, hiddenImg.sourceSize.height * 2)
|
||||
sourceSize: Qt.size(Math.max(hiddenImg.sourceSize.width * 2, 250), Math.max(hiddenImg.sourceSize.height * 2, 250))
|
||||
Image {
|
||||
id: hiddenImg
|
||||
source: parent.source
|
||||
|
|
|
@ -19,16 +19,19 @@ Item {
|
|||
property color bgColorHover: bgColor
|
||||
property alias selectedItemView: selectedItemContainer.children
|
||||
property int caretRightMargin: Style.current.padding
|
||||
property int caretLeftMargin: 8
|
||||
property int caretLeftMargin: Style.current.halfPadding
|
||||
property alias select: inputRectangle
|
||||
property int menuAlignment: Select.MenuAlignment.Right
|
||||
property Item zeroItemsView: Item {}
|
||||
property int contentWidth: inputRectangle.width - (caret.width + caretRightMargin + caretLeftMargin)
|
||||
property int selectedItemRightMargin: caret.width + caretRightMargin + caretLeftMargin
|
||||
property string validationError: ""
|
||||
property alias validationErrorAlignment: validationErrorText.horizontalAlignment
|
||||
property int validationErrorTopMargin: Style.current.halfPadding
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
id: root
|
||||
height: inputRectangle.height + (hasLabel ? inputLabel.height + labelMargin : 0)
|
||||
height: inputRectangle.height + (hasLabel ? inputLabel.height + labelMargin : 0) + (!!validationError ? (validationErrorText.height + validationErrorTopMargin) : 0)
|
||||
|
||||
StyledText {
|
||||
id: inputLabel
|
||||
|
@ -53,6 +56,8 @@ Item {
|
|||
anchors.topMargin: root.hasLabel ? root.labelMargin : 0
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
border.width: !!validationError ? 1 : 0
|
||||
border.color: Style.current.danger
|
||||
|
||||
Item {
|
||||
id: selectedItemContainer
|
||||
|
@ -134,6 +139,20 @@ Item {
|
|||
}
|
||||
}
|
||||
}
|
||||
TextEdit {
|
||||
id: validationErrorText
|
||||
visible: !!validationError
|
||||
text: validationError
|
||||
anchors.top: inputRectangle.bottom
|
||||
anchors.topMargin: validationErrorTopMargin
|
||||
selectByMouse: true
|
||||
readOnly: true
|
||||
font.pixelSize: 12
|
||||
height: 16
|
||||
color: Style.current.danger
|
||||
width: parent.width
|
||||
horizontalAlignment: TextEdit.AlignRight
|
||||
}
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: inputRectangle
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import QtQuick 2.13
|
||||
import QtGraphicalEffects 1.13
|
||||
import "../imports"
|
||||
|
||||
Item {
|
||||
property int iconMargin: Style.current.padding
|
||||
property alias icon: icon
|
||||
readonly property int separatorWidth: (parent.width / 2) - (icon.height / 2) - iconMargin
|
||||
width: parent.width
|
||||
height: icon.height
|
||||
|
||||
|
||||
Separator {
|
||||
id: separatorLeft
|
||||
width: separatorWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: undefined
|
||||
}
|
||||
|
||||
SVGImage {
|
||||
id: icon
|
||||
height: 14
|
||||
width: 18
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../app/img/arrow-right.svg"
|
||||
rotation: 90
|
||||
|
||||
ColorOverlay {
|
||||
anchors.fill: parent
|
||||
source: parent
|
||||
color: Style.current.textColor
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
Separator {
|
||||
id: separatorRight
|
||||
width: separatorWidth
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.topMargin: undefined
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import "../imports"
|
||||
|
||||
FormGroup {
|
||||
id: root
|
||||
property string headerText
|
||||
property string footerText
|
||||
}
|
|
@ -6,13 +6,22 @@ import "../imports"
|
|||
|
||||
Item {
|
||||
id: root
|
||||
property var fromAccount: ({})
|
||||
property var toAccount: ({ type: "" })
|
||||
property var asset: ({ name: "", symbol: "" })
|
||||
property var amount: ({ value: "", fiatValue: "", currency: "" })
|
||||
property var fromAccount
|
||||
property var toAccount
|
||||
property var asset
|
||||
property var amount
|
||||
property string currency: "USD"
|
||||
property var gas: ({ value: "", symbol: "", fiatValue: "" })
|
||||
property var gas
|
||||
height: content.height
|
||||
property var reset: function() {}
|
||||
|
||||
function resetInternal() {
|
||||
fromAccount = undefined
|
||||
toAccount = undefined
|
||||
asset = undefined
|
||||
amount = undefined
|
||||
gas = undefined
|
||||
}
|
||||
|
||||
Column {
|
||||
id: content
|
||||
|
@ -30,11 +39,11 @@ Item {
|
|||
StyledText {
|
||||
font.pixelSize: 15
|
||||
height: 22
|
||||
text: root.fromAccount.name
|
||||
text: root.fromAccount ? root.fromAccount.name : ""
|
||||
elide: Text.ElideRight
|
||||
anchors.left: parent.left
|
||||
anchors.right: imgFromWallet.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.rightMargin: Style.current.halfPadding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
@ -44,7 +53,6 @@ Item {
|
|||
sourceSize.height: 18
|
||||
sourceSize.width: 18
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.current.padding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../app/img/walletIcon.svg"
|
||||
|
@ -52,22 +60,21 @@ Item {
|
|||
ColorOverlay {
|
||||
anchors.fill: imgFromWallet
|
||||
source: imgFromWallet
|
||||
color: fromAccount.iconColor
|
||||
color: root.fromAccount ? root.fromAccount.iconColor : Style.current.blue
|
||||
}
|
||||
}
|
||||
}
|
||||
LabelValueRow {
|
||||
id: itmTo
|
||||
property var props: { "primaryText": "replace1", "secondaryText": "me1" }
|
||||
//% "Recipient"
|
||||
label: qsTrId("recipient")
|
||||
states: [
|
||||
State {
|
||||
name: "Address"
|
||||
when: root.toAccount.type === RecipientSelector.Type.Address
|
||||
when: !!root.toAccount && root.toAccount.type === RecipientSelector.Type.Address
|
||||
PropertyChanges {
|
||||
target: txtToPrimary
|
||||
text: root.toAccount.address
|
||||
text: root.toAccount ? root.toAccount.address : ""
|
||||
elide: Text.ElideMiddle
|
||||
anchors.leftMargin: 190
|
||||
}
|
||||
|
@ -78,7 +85,7 @@ Item {
|
|||
},
|
||||
State {
|
||||
name: "Contact"
|
||||
when: root.toAccount.type === RecipientSelector.Type.Contact && !!root.toAccount.address
|
||||
when: !!root.toAccount && root.toAccount.type === RecipientSelector.Type.Contact && !!root.toAccount.address
|
||||
PropertyChanges {
|
||||
target: metSecondary
|
||||
text: root.toAccount.ensVerified ? root.toAccount.alias : root.toAccount.address
|
||||
|
@ -101,14 +108,14 @@ Item {
|
|||
},
|
||||
State {
|
||||
name: "Account"
|
||||
when: root.toAccount.type === RecipientSelector.Type.Account && !!root.toAccount.address
|
||||
when: !!root.toAccount && root.toAccount.type === RecipientSelector.Type.Account && !!root.toAccount.address
|
||||
PropertyChanges {
|
||||
target: metSecondary
|
||||
text: root.toAccount.address
|
||||
}
|
||||
PropertyChanges {
|
||||
target: txtToSecondary
|
||||
anchors.rightMargin: Style.current.padding + imgToWallet.width + 8
|
||||
anchors.rightMargin: Style.current.padding + imgToWallet.width + Style.current.halfPadding
|
||||
text: metSecondary.elidedText
|
||||
width: metSecondary.elidedWidth
|
||||
}
|
||||
|
@ -177,7 +184,6 @@ Item {
|
|||
sourceSize.height: 18
|
||||
sourceSize.width: 18
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.current.padding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../app/img/walletIcon.svg"
|
||||
|
@ -192,7 +198,6 @@ Item {
|
|||
id: idtToContact
|
||||
visible: false
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.current.padding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 32
|
||||
height: 32
|
||||
|
@ -213,7 +218,7 @@ Item {
|
|||
text: (root.asset && root.asset.name) ? root.asset.name : ""
|
||||
anchors.left: parent.left
|
||||
anchors.right: txtAssetSymbol.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.rightMargin: Style.current.halfPadding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
@ -225,7 +230,7 @@ Item {
|
|||
text: (root.asset && root.asset.symbol) ? root.asset.symbol : ""
|
||||
color: Style.current.secondaryText
|
||||
anchors.right: imgAsset.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.rightMargin: Style.current.halfPadding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
@ -235,7 +240,6 @@ Item {
|
|||
sourceSize.height: 32
|
||||
sourceSize.width: 32
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.current.padding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../app/img/tokens/" + ((root.asset && root.asset.symbol) ? root.asset.symbol : "ETH") + ".png"
|
||||
|
@ -259,7 +263,7 @@ Item {
|
|||
StyledText {
|
||||
font.pixelSize: 15
|
||||
height: 22
|
||||
text: root.amount.value ? Utils.stripTrailingZeros(root.amount.value) : ""
|
||||
text: (root.amount && root.amount.value) ? Utils.stripTrailingZeros(root.amount.value) : ""
|
||||
anchors.left: parent.left
|
||||
anchors.right: txtAmountSymbol.left
|
||||
anchors.rightMargin: 5
|
||||
|
@ -284,7 +288,7 @@ Item {
|
|||
id: txtAmountFiat
|
||||
font.pixelSize: 15
|
||||
height: 22
|
||||
text: "~" + (root.amount.fiatValue ? root.amount.fiatValue : "0.00")
|
||||
text: "~" + (root.amount && root.amount.fiatValue ? root.amount.fiatValue : "0.00")
|
||||
anchors.right: txtAmountCurrency.left
|
||||
anchors.rightMargin: 5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
@ -298,7 +302,6 @@ Item {
|
|||
text: root.currency.toUpperCase()
|
||||
color: Style.current.secondaryText
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.current.padding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
@ -356,7 +359,6 @@ Item {
|
|||
text: root.currency.toUpperCase()
|
||||
color: Style.current.secondaryText
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Style.current.padding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
|
|
@ -7,9 +7,38 @@ Item {
|
|||
id: root
|
||||
height: signingPhraseItem.height + signingPhrase.height + txtPassword.height + Style.current.smallPadding + Style.current.bigPadding
|
||||
|
||||
property string signingPhrase: "not a real one"
|
||||
property alias passwordInput: txtPassword
|
||||
property string validationError: ""
|
||||
property alias signingPhrase: signingPhrase.text
|
||||
property string enteredPassword
|
||||
property alias validationError: txtPassword.validationError
|
||||
//% "You need to enter a password"
|
||||
property string noInputErrorMessage: qsTrId("you-need-to-enter-a-password")
|
||||
//% "Password needs to be 4 characters or more"
|
||||
property string invalidInputErrorMessage: qsTrId("password-needs-to-be-4-characters-or-more")
|
||||
property bool isValid: false
|
||||
property var reset: function() {}
|
||||
|
||||
function resetInternal() {
|
||||
signingPhrase.text = ""
|
||||
enteredPassword = ""
|
||||
txtPassword.resetInternal()
|
||||
isValid = false
|
||||
}
|
||||
|
||||
function forceActiveFocus(reason) {
|
||||
txtPassword.forceActiveFocus(reason)
|
||||
}
|
||||
|
||||
function validate() {
|
||||
txtPassword.validationError = ""
|
||||
const noInput = txtPassword.text === ""
|
||||
if (noInput) {
|
||||
txtPassword.validationError = noInputErrorMessage
|
||||
} else if (txtPassword.text.length < 4) {
|
||||
txtPassword.validationError = invalidInputErrorMessage
|
||||
}
|
||||
isValid = txtPassword.validationError === ""
|
||||
return isValid
|
||||
}
|
||||
|
||||
Item {
|
||||
id: signingPhraseItem
|
||||
|
@ -73,15 +102,23 @@ Item {
|
|||
}
|
||||
|
||||
Input {
|
||||
id: txtPassword
|
||||
anchors.top: signingPhrase.bottom
|
||||
anchors.topMargin: Style.current.bigPadding
|
||||
id: txtPassword
|
||||
focus: true
|
||||
customHeight: 56
|
||||
//% "Password"
|
||||
label: qsTrId("password")
|
||||
//% "Enter Password"
|
||||
placeholderText: qsTrId("enter-password")
|
||||
textField.echoMode: TextInput.Password
|
||||
validationError: root.validationError
|
||||
validationErrorAlignment: TextEdit.AlignRight
|
||||
validationErrorTopMargin: 8
|
||||
onTextChanged: {
|
||||
if(root.validate()) {
|
||||
root.enteredPassword = this.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import "../imports"
|
||||
|
||||
StackView {
|
||||
id: root
|
||||
default property list<FormGroup> groups
|
||||
property int currentIdx: 0
|
||||
property bool isLastGroup: currentIdx === groups.length - 1
|
||||
property bool isFirstGroup: currentIdx === 0
|
||||
signal groupActivated(Item group)
|
||||
property alias currentGroup: root.currentItem
|
||||
property var next: function() {
|
||||
if (groups && groups.length <= currentIdx + 1) {
|
||||
return
|
||||
}
|
||||
const group = groups[++currentIdx]
|
||||
this.push(group, StackView.Immediate)
|
||||
|
||||
}
|
||||
property var back: function() {
|
||||
if (currentIdx <= 0) {
|
||||
return
|
||||
}
|
||||
this.pop()
|
||||
currentIdx--
|
||||
}
|
||||
|
||||
function reset() {
|
||||
for (let i=0; i<groups.length; i++) {
|
||||
groups[i].reset()
|
||||
}
|
||||
this.pop(null)
|
||||
currentIdx = 0
|
||||
}
|
||||
|
||||
initialItem: groups[currentIdx]
|
||||
anchors.fill: parent
|
||||
|
||||
// The below transitions are pointless, but without them,
|
||||
// the final input in the final TransactionFormGroup will
|
||||
// not be able to receive focus! Seems like a Qt bug...
|
||||
pushEnter: Transition {
|
||||
PropertyAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to:1
|
||||
duration: 1
|
||||
}
|
||||
}
|
||||
pushExit: Transition {
|
||||
PropertyAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to:1
|
||||
duration: 1
|
||||
}
|
||||
}
|
||||
popEnter: Transition {
|
||||
PropertyAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to:1
|
||||
duration: 1
|
||||
}
|
||||
}
|
||||
popExit: Transition {
|
||||
PropertyAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to:1
|
||||
duration: 1
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue