feat(SendModal): New AmountToSend component

In comparison to the previous version it provides simpler API
and better validation based on AmountValidator.

Old version will be removed after full integration of the new version.
This commit is contained in:
Michał Cieślak 2024-07-17 17:46:10 +02:00 committed by Michał
parent 84be1d9da7
commit 613fa4d19f
4 changed files with 567 additions and 0 deletions

View File

@ -0,0 +1,138 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Qt.labs.settings 1.0
import shared.popups.send.views 1.0
SplitView {
orientation: Qt.Vertical
SplitView.fillWidth: true
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
AmountToSendNew {
id: amountToSend
anchors.centerIn: parent
interactive: interactiveCheckBox.checked
markAsInvalid: markAsInvalidCheckBox.checked
caption: "Amount to send"
decimalPoint: decimalPointRadioButton.checked ? "." : ","
price: parseFloat(priceTextField.text)
multiplierIndex: multiplierIndexSpinBox.value
formatFiat: balance => `${balance.toLocaleString(Qt.locale())} USD`
formatBalance: balance => `${balance.toLocaleString(Qt.locale())} ETH`
}
}
Pane {
id: logsAndControlsPanel
SplitView.minimumHeight: 350
ColumnLayout {
spacing: 15
RowLayout {
Label {
text: "Price"
}
TextField {
id: priceTextField
text: "812.323"
}
}
RowLayout {
Label {
text: "Decimal point"
}
RadioButton {
id: decimalPointRadioButton
text: "."
}
RadioButton {
text: ","
checked: true
}
}
RowLayout {
Label {
text: "Multiplier index"
}
SpinBox {
id: multiplierIndexSpinBox
editable: true
value: 18
to: 30
}
}
RowLayout {
CheckBox {
id: interactiveCheckBox
text: "Interactive"
checked: true
}
CheckBox {
id: markAsInvalidCheckBox
text: "Mark as invalid"
}
}
Label {
font.bold: true
text: `fiat mode: ${amountToSend.fiatMode}, ` +
`valid: ${amountToSend.valid}, ` +
`empty: ${amountToSend.empty}, ` +
`amount: ${amountToSend.amount}`
}
RowLayout {
Label {
text: `Set value`
}
TextField {
id: amountTextField
text: "0.0012"
}
Button {
text: "SET"
onClicked: {
amountToSend.setValue(amountTextField.text)
}
}
}
}
}
Settings {
property alias multiplier: multiplierIndexSpinBox.value
}
}
// category: Components

View File

@ -0,0 +1,173 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtTest 1.15
import shared.popups.send.views 1.0
Item {
id: root
Component {
id: componentUnderTest
AmountToSendNew {
decimalPoint: "."
}
}
property AmountToSendNew amountToSend
TestCase {
name: "AmountToSendNew"
when: windowShown
function type(key, times = 1) {
for (let i = 0; i < times; i++) {
keyPress(key)
keyRelease(key)
}
}
function init() {
amountToSend = createTemporaryObject(componentUnderTest, root)
}
function test_empty() {
compare(amountToSend.valid, false)
compare(amountToSend.empty, true)
compare(amountToSend.amount, "0")
compare(amountToSend.fiatMode, false)
}
function test_settingValueInCryptoMode() {
const textField = findChild(amountToSend, "amountToSend_textField")
amountToSend.multiplierIndex = 3
amountToSend.setValue("2.5")
compare(textField.text, "2.5")
compare(amountToSend.amount, "2500")
compare(amountToSend.valid, true)
amountToSend.setValue("2.12345678")
compare(textField.text, "2.123")
compare(amountToSend.amount, "2123")
compare(amountToSend.valid, true)
amountToSend.setValue("2.1239")
compare(textField.text, "2.124")
compare(amountToSend.amount, "2124")
compare(amountToSend.valid, true)
amountToSend.setValue(".1239")
compare(textField.text, "0.124")
compare(amountToSend.amount, "124")
compare(amountToSend.valid, true)
amountToSend.setValue("1.0000")
compare(textField.text, "1")
compare(amountToSend.amount, "1000")
compare(amountToSend.valid, true)
amountToSend.setValue("0.0000")
compare(textField.text, "0")
compare(amountToSend.amount, "0")
compare(amountToSend.valid, true)
amountToSend.setValue("x")
compare(textField.text, "NaN")
compare(amountToSend.amount, "0")
compare(amountToSend.valid, false)
// exceeding maxium allowed integral part
amountToSend.setValue("1234567890000")
compare(textField.text, "1234567890000")
compare(amountToSend.amount, "0")
compare(amountToSend.valid, false)
}
function test_settingValueInFiatMode() {
const textField = findChild(amountToSend, "amountToSend_textField")
const mouseArea = findChild(amountToSend, "amountToSend_mouseArea")
amountToSend.price = 0.5
amountToSend.multiplierIndex = 3
mouseClick(mouseArea)
compare(amountToSend.fiatMode, true)
amountToSend.setValue("2.5")
compare(textField.text, "2.50")
compare(amountToSend.amount, "5000")
compare(amountToSend.valid, true)
amountToSend.setValue("2.12345678")
compare(textField.text, "2.12")
compare(amountToSend.amount, "4240")
compare(amountToSend.valid, true)
amountToSend.setValue("2.129")
compare(textField.text, "2.13")
compare(amountToSend.amount, "4260")
compare(amountToSend.valid, true)
// exceeding maxium allowed integral part
amountToSend.setValue("1234567890000")
compare(textField.text, "1234567890000.00")
compare(amountToSend.amount, "0")
compare(amountToSend.valid, false)
}
function test_switchingMode() {
const textField = findChild(amountToSend, "amountToSend_textField")
const mouseArea = findChild(amountToSend, "amountToSend_mouseArea")
amountToSend.price = 0.5
amountToSend.multiplierIndex = 3
amountToSend.setValue("10.5")
compare(amountToSend.amount, "10500")
mouseClick(mouseArea)
compare(amountToSend.fiatMode, true)
compare(textField.text, "5.25")
compare(amountToSend.amount, "10500")
mouseClick(mouseArea)
compare(amountToSend.fiatMode, false)
compare(textField.text, "10.5")
compare(amountToSend.amount, "10500")
mouseClick(mouseArea)
compare(amountToSend.fiatMode, true)
amountToSend.price = 0.124
amountToSend.setValue("1")
compare(textField.text, "1.00")
mouseClick(mouseArea)
compare(amountToSend.fiatMode, false)
compare(textField.text, "8.065")
compare(amountToSend.amount, "8065")
}
function test_clear() {
const textField = findChild(amountToSend, "amountToSend_textField")
amountToSend.setValue("10.5")
amountToSend.clear()
compare(amountToSend.amount, "0")
compare(textField.text, "")
}
}
}

View File

@ -0,0 +1,255 @@
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 as SQUtils
import StatusQ.Validators 0.1
import utils 1.0
import shared.controls 1.0
Control {
id: root
/* Crypto value in a base unit as a string integer, e.g. 1000000000000000000
* for 1 ETH */
readonly property alias amount: d.amountBaseUnit
/* In fiat mode the input value is meant to be a fiat value, conversely,
* crypto value otherwise. */
readonly property alias fiatMode: d.fiatMode
/* Indicates if input represent valid number. E.g. empty input or containing
* only decimal point is not valid. */
readonly property alias valid: textField.acceptableInput
readonly property bool empty: textField.length === 0
// TODO: remove, temporarily for backward compatibility. External components
// should not rely on formatted amount because formatting rules are internal
// detail of that component.
readonly property alias text: textField.text
/* Decimal point character to be dispalyed. Both "." and "," will be
* replaced by the provided decimal point on the fly */
property alias decimalPoint: validator.decimalPoint
/* Number of fiat decimal places used to limit allowed decimal places in
* fiatMode */
property int fiatDecimalPlaces: 2
/* Specifies how divisible given cryptocurrency is, e.g. 18 for ETH. Used
* for limiting allowed decimal places and computing final amout as an
* integer value */
property int multiplierIndex: 18
/* Price of one unit of given cryptocurrency (e.g. price for 1 ETH) */
property real price: 1.0
property alias caption: captionText.text
property bool interactive: true
/* Allows mark input as invalid when it's valid number but doesn't satisfy
* arbitrary external criteria, e.g. is higher than maximum expected value. */
property bool markAsInvalid: false
/* Methods for formatting crypto and fiat value expecting double values,
e.g. 1.0 for 1 ETH or 1.0 for 1 USD. */
property var formatFiat: balance =>
`${balance.toLocaleString(Qt.locale())} FIAT`
property var formatBalance: balance =>
`${balance.toLocaleString(Qt.locale())} CRYPTO`
/* Allows to set value to be displayed. The value is expected to be a not
localized string like "1", "1.1" or "0.000000023400234222". Provided
value will be formatted and displayed. Depending on the fiatMode flag
it will affect output amount appropriately. */
function setValue(valueString) {
if (!valueString)
valueString = "0"
const decimalPlaces = d.fiatMode ? root.fiatDecimalPlaces
: root.multiplierIndex
const stringNumber = SQUtils.AmountsArithmetic.fromString(
valueString).toFixed(decimalPlaces)
const trimmed = d.fiatMode
? stringNumber
: d.removeDecimalTrailingZeros(stringNumber)
textField.text = d.localize(trimmed)
}
function clear() {
textField.clear()
}
function forceActiveFocus() {
textField.forceActiveFocus()
}
QtObject {
id: d
property bool fiatMode: false
readonly property string inputDelocalized:
root.valid && textField.length !== 0
? textField.text.replace(",", ".") : "0"
function removeDecimalTrailingZeros(num) {
if (!num.includes("."))
return num
return num.replace(/\.?0*$/g, "")
}
function localize(num) {
return num.replace(".", root.decimalPoint)
}
readonly property string amountBaseUnit: {
if (d.fiatMode)
return secondaryValue
const multiplier = SQUtils.AmountsArithmetic.fromExponent(
root.multiplierIndex)
return SQUtils.AmountsArithmetic.times(
SQUtils.AmountsArithmetic.fromString(inputDelocalized),
multiplier).toFixed()
}
readonly property string secondaryValue: {
const price = isNaN(root.price) ? 0 : root.price
if (!d.fiatMode)
return SQUtils.AmountsArithmetic.times(
SQUtils.AmountsArithmetic.fromString(inputDelocalized),
SQUtils.AmountsArithmetic.fromNumber(
price * (10 ** root.fiatDecimalPlaces))).round().toFixed()
const multiplier = SQUtils.AmountsArithmetic.fromExponent(
root.multiplierIndex)
return SQUtils.AmountsArithmetic.div(
SQUtils.AmountsArithmetic.times(
SQUtils.AmountsArithmetic.fromString(inputDelocalized),
multiplier),
SQUtils.AmountsArithmetic.fromNumber(price)).round().toFixed()
}
}
contentItem: ColumnLayout {
StatusBaseText {
id: captionText
Layout.fillWidth: true
visible: text.length > 0
font.pixelSize: 13
lineHeight: 18
lineHeightMode: Text.FixedHeight
color: Theme.palette.directColor1
elide: Text.ElideRight
}
RowLayout {
StyledTextField {
id: textField
objectName: "amountToSend_textField"
Layout.fillWidth: true
implicitHeight: 44
padding: 0
background: null
readOnly: !root.interactive
color: text.length === 0 || (root.valid && !root.markAsInvalid)
? Theme.palette.directColor1
: Theme.palette.dangerColor1
placeholderText: {
if (!d.fiatMode || root.fiatDecimalPlaces === 0)
return "0"
return "0" + root.decimalPoint
+ "0".repeat(root.fiatDecimalPlaces)
}
font.pixelSize: Utils.getFontSizeBasedOnLetterCount(text)
validator: AmountValidator {
id: validator
maxDecimalDigits: d.fiatMode ? root.fiatDecimalPlaces
: root.multiplierIndex
}
}
}
StatusBaseText {
id: bottomItem
objectName: "bottomItemText"
Layout.fillWidth: true
text: {
const divisor = SQUtils.AmountsArithmetic.fromExponent(
d.fiatMode ? root.multiplierIndex
: root.fiatDecimalPlaces)
const divided = SQUtils.AmountsArithmetic.div(
SQUtils.AmountsArithmetic.fromString(
d.secondaryValue), divisor)
const asNumber = SQUtils.AmountsArithmetic.toNumber(divided)
return d.fiatMode ? root.formatBalance(asNumber)
: root.formatFiat(asNumber)
}
elide: Text.ElideMiddle
font.pixelSize: 13
color: Theme.palette.directColor5
MouseArea {
objectName: "amountToSend_mouseArea"
anchors.fill: parent
cursorShape: enabled ? Qt.PointingHandCursor : undefined
enabled: root.interactive
onClicked: {
const secondaryValue = d.secondaryValue
d.fiatMode = !d.fiatMode
if (textField.length === 0)
return
const decimalPlaces = d.fiatMode ? root.fiatDecimalPlaces
: root.multiplierIndex
const divisor = SQUtils.AmountsArithmetic.fromExponent(
decimalPlaces)
const stringNumber = SQUtils.AmountsArithmetic.div(
SQUtils.AmountsArithmetic.fromString(secondaryValue),
divisor).toFixed(decimalPlaces)
const trimmed = d.fiatMode
? stringNumber
: d.removeDecimalTrailingZeros(stringNumber)
textField.text = d.localize(trimmed)
}
}
}
}
}

View File

@ -1,5 +1,6 @@
AmountToReceive 1.0 AmountToReceive.qml
AmountToSend 1.0 AmountToSend.qml
AmountToSendNew 1.0 AmountToSendNew.qml
FeesView 1.0 FeesView.qml
NetworkCardsComponent 1.0 NetworkCardsComponent.qml
NetworkSelector 1.0 NetworkSelector.qml