diff --git a/storybook/pages/AmountToSendPage.qml b/storybook/pages/AmountToSendPage.qml
index 5be8d0afc6..8d3f7f6aee 100644
--- a/storybook/pages/AmountToSendPage.qml
+++ b/storybook/pages/AmountToSendPage.qml
@@ -38,7 +38,7 @@ SplitView {
maxInputBalance: inputIsFiat ? root.maxCryptoBalance*amountToSendInput.selectedHolding.marketDetails.currencyPrice.amount
: root.maxCryptoBalance
- currentCurrency: "Fiat"
+ currentCurrency: "USD"
formatCurrencyAmount: function(amount, symbol, options, locale) {
const currencyAmount = {
amount: amount,
@@ -57,47 +57,42 @@ SplitView {
LogsAndControlsPanel {
id: logsAndControlsPanel
- SplitView.minimumHeight: 100
+ SplitView.minimumHeight: 250
logsView.logText: logs.logText
- }
- }
- Pane {
- SplitView.minimumWidth: 300
- SplitView.preferredWidth: 300
+ ColumnLayout {
+ Label {
+ Layout.topMargin: 10
+ Layout.fillWidth: true
+ text: "Max Crypto Balance"
+ }
- ColumnLayout {
- Label {
- Layout.topMargin: 10
- Layout.fillWidth: true
- text: "Max Crypto Balance"
- }
+ TextField {
+ id: maxCryptoBalanceText
+ background: Rectangle { border.color: 'lightgrey' }
+ Layout.preferredWidth: 200
+ text: "1000000"
+ }
- TextField {
- id: maxCryptoBalanceText
- background: Rectangle { border.color: 'lightgrey' }
- Layout.preferredWidth: 200
- text: "1000000"
- }
+ Label {
+ Layout.topMargin: 10
+ Layout.fillWidth: true
+ text: "Decimals"
+ }
- Label {
- Layout.topMargin: 10
- Layout.fillWidth: true
- text: "Decimals"
- }
+ TextField {
+ id: decimalsText
+ background: Rectangle { border.color: 'lightgrey' }
+ Layout.preferredWidth: 200
+ text: "6"
+ }
- TextField {
- id: decimalsText
- background: Rectangle { border.color: 'lightgrey' }
- Layout.preferredWidth: 200
- text: "6"
- }
+ CheckBox {
+ id: fiatInput
- CheckBox {
- id: fiatInput
-
- text: "Fiat input value"
+ text: "Fiat input value"
+ }
}
}
}
diff --git a/storybook/pages/SlippageSelectorPage.qml b/storybook/pages/SlippageSelectorPage.qml
index a5a09f2390..eaa758c285 100644
--- a/storybook/pages/SlippageSelectorPage.qml
+++ b/storybook/pages/SlippageSelectorPage.qml
@@ -47,7 +47,7 @@ SplitView {
ColumnLayout {
Repeater {
- model: [0.1, 0.5, 0.24, 0.8, 120.84]
+ model: [0, 0.1, 0.5, 0.24, 0.8, 120.84]
Button {
text: "set " + modelData
diff --git a/storybook/pages/SwapInputPanelPage.qml b/storybook/pages/SwapInputPanelPage.qml
new file mode 100644
index 0000000000..9707cfab0b
--- /dev/null
+++ b/storybook/pages/SwapInputPanelPage.qml
@@ -0,0 +1,203 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import StatusQ 0.1
+import StatusQ.Core.Theme 0.1
+
+import utils 1.0
+
+import shared.stores 1.0
+import shared.stores.send 1.0
+
+import AppLayouts.Wallet.stores 1.0
+import AppLayouts.Wallet.panels 1.0
+import AppLayouts.Wallet.controls 1.0
+
+import AppLayouts.Wallet.popups.swap 1.0
+
+import Models 1.0
+import Storybook 1.0
+
+import SortFilterProxyModel 0.2
+
+SplitView {
+ id: root
+
+ Logs { id: logs }
+
+ QtObject {
+ id: d
+
+ readonly property SwapInputParamsForm swapInputParamsForm: SwapInputParamsForm {
+ fromTokensKey: ctrlFromTokensKey.text
+ fromTokenAmount: ctrlFromTokenAmount.text
+ toTokenKey: ctrlToTokenKey.text
+ toTokenAmount: ctrlToTokenAmount.text
+ }
+
+ readonly property SwapModalAdaptor adaptor: SwapModalAdaptor {
+ swapStore: SwapStore {
+ readonly property var accounts: WalletAccountsModel {}
+ readonly property var flatNetworks: NetworksModel.flatNetworks
+ readonly property bool areTestNetworksEnabled: false
+ }
+ walletAssetsStore: WalletAssetsStore {
+ id: thisWalletAssetStore
+ walletTokensStore: TokensStore {
+ plainTokensBySymbolModel: TokensBySymbolModel {}
+ }
+ readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
+ assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
+ }
+ currencyStore: CurrenciesStore {}
+ swapFormData: d.swapInputParamsForm
+ }
+ }
+
+ Rectangle {
+ SplitView.fillWidth: true
+ SplitView.fillHeight: true
+ color: Theme.palette.baseColor3
+
+ Item {
+ width: 492
+ height: payPanel.height + receivePanel.height + 4
+ anchors.centerIn: parent
+
+ SwapInputPanel {
+ id: payPanel
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ }
+
+ currencyStore: d.adaptor.currencyStore
+ flatNetworksModel: d.adaptor.filteredFlatNetworksModel
+ processedAssetsModel: d.adaptor.processedAssetsModel
+
+ tokenKey: d.swapInputParamsForm.fromTokensKey
+ tokenAmount: d.swapInputParamsForm.fromTokenAmount
+
+ swapSide: SwapInputPanel.SwapSide.Pay
+ fiatInputInteractive: ctrlFiatInputInteractive.checked
+ swapExchangeButtonWidth: swapButton.width
+ loading: ctrlLoading.checked
+ }
+
+ SwapInputPanel {
+ id: receivePanel
+ anchors {
+ left: parent.left
+ right: parent.right
+ bottom: parent.bottom
+ }
+
+ currencyStore: d.adaptor.currencyStore
+ flatNetworksModel: d.adaptor.filteredFlatNetworksModel
+ processedAssetsModel: d.adaptor.processedAssetsModel
+
+ tokenKey: d.swapInputParamsForm.toTokenKey
+ tokenAmount: d.swapInputParamsForm.toTokenAmount
+
+ swapSide: SwapInputPanel.SwapSide.Receive
+ fiatInputInteractive: ctrlFiatInputInteractive.checked
+ swapExchangeButtonWidth: swapButton.width
+ loading: ctrlLoading.checked
+ }
+
+ SwapExchangeButton {
+ id: swapButton
+ anchors.centerIn: parent
+ }
+ }
+ }
+
+ LogsAndControlsPanel {
+ id: logsAndControlsPanel
+
+ SplitView.minimumWidth: 250
+ SplitView.preferredWidth: 250
+
+ logsView.logText: logs.logText
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ RowLayout {
+ Layout.fillWidth: true
+ Label {
+ text: "Pay symbol:"
+ }
+ TextField {
+ Layout.fillWidth: true
+ id: ctrlFromTokensKey
+ }
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ Label {
+ text: "Pay amount:"
+ }
+ TextField {
+ Layout.fillWidth: true
+ id: ctrlFromTokenAmount
+ }
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ Label {
+ text: "Receive symbol:"
+ }
+ TextField {
+ Layout.fillWidth: true
+ id: ctrlToTokenKey
+ text: "STT"
+ }
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ Label {
+ text: "Receive amount:"
+ }
+ TextField {
+ Layout.fillWidth: true
+ id: ctrlToTokenAmount
+ }
+ }
+ Switch {
+ id: ctrlFiatInputInteractive
+ text: "Fiat input interactive"
+ checked: false
+ }
+ Switch {
+ id: ctrlLoading
+ text: "Loading"
+ }
+
+ Label {
+ Layout.fillWidth: true
+ font.weight: Font.Medium
+ text: "Pay:
- Symbol: %1
- Amount: %2
- Valid: %3"
+ .arg(payPanel.selectedHoldingId || "N/A")
+ .arg(payPanel.cryptoValue.toString())
+ .arg(payPanel.cryptoValueValid ? "true" : "false")
+ }
+ Label {
+ Layout.fillWidth: true
+ font.weight: Font.Medium
+ text: "Receive:
- Symbol: %1
- Amount: %2
- Valid: %3"
+ .arg(receivePanel.selectedHoldingId || "N/A")
+ .arg(receivePanel.cryptoValue.toString())
+ .arg(receivePanel.cryptoValueValid ? "true" : "false")
+ }
+
+ Item { Layout.fillHeight: true }
+ }
+ }
+}
+
+// category: Panels
+
+// https://www.figma.com/design/TS0eQX9dAZXqZtELiwKIoK/Swap---Milestone-1?node-id=3404-111405&t=G96tBLQr2j73HT9X-0
diff --git a/storybook/pages/SwapModalPage.qml b/storybook/pages/SwapModalPage.qml
index e4c5af6252..f6dea4746a 100644
--- a/storybook/pages/SwapModalPage.qml
+++ b/storybook/pages/SwapModalPage.qml
@@ -170,7 +170,7 @@ SplitView {
StatusInput {
id: swapInput
Layout.preferredWidth: 100
- label: "Token mount to swap"
+ label: "Token amount to swap"
text: "100"
}
diff --git a/storybook/pages/TokenListViewPage.qml b/storybook/pages/TokenListViewPage.qml
index 5ef1244159..3ebeb7abbe 100644
--- a/storybook/pages/TokenListViewPage.qml
+++ b/storybook/pages/TokenListViewPage.qml
@@ -51,9 +51,7 @@ SplitView {
return currencyStore.formatCurrencyAmount(balance, "USD")
}
formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){
- let bigIntBalance = AmountsArithmetic.fromString(balance)
- let decimalBalance = AmountsArithmetic.toNumber(bigIntBalance, decimals)
- return currencyStore.formatCurrencyAmount(decimalBalance, symbol)
+ return currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
}
}
}
diff --git a/storybook/qmlTests/main.cpp b/storybook/qmlTests/main.cpp
index 1e3aa7b325..96db3ae139 100644
--- a/storybook/qmlTests/main.cpp
+++ b/storybook/qmlTests/main.cpp
@@ -10,6 +10,10 @@ class Setup : public QObject
public slots:
void qmlEngineAvailable(QQmlEngine *engine) {
// custom code that needs QQmlEngine, register QML types, add import paths,...
+
+ QGuiApplication::setOrganizationName(QStringLiteral("Status"));
+ QGuiApplication::setOrganizationDomain(QStringLiteral("status.im"));
+
const QStringList additionalImportPaths {
STATUSQ_MODULE_IMPORT_PATH,
QML_IMPORT_ROOT + QStringLiteral("/../ui/app"),
diff --git a/storybook/qmlTests/tests/tst_StatusAmountInput.qml b/storybook/qmlTests/tests/tst_StatusAmountInput.qml
index 6e823cef17..225f9c32b3 100644
--- a/storybook/qmlTests/tests/tst_StatusAmountInput.qml
+++ b/storybook/qmlTests/tests/tst_StatusAmountInput.qml
@@ -80,6 +80,7 @@ Item {
keyClick(Qt.Key_3)
compare(controlUnderTest.text, "13")
+ compare(controlUnderTest.valid, true)
}
function test_defaultValidation() {
diff --git a/storybook/qmlTests/tests/tst_SwapInputPanel.qml b/storybook/qmlTests/tests/tst_SwapInputPanel.qml
new file mode 100644
index 0000000000..2c6193a7e8
--- /dev/null
+++ b/storybook/qmlTests/tests/tst_SwapInputPanel.qml
@@ -0,0 +1,252 @@
+import QtQuick 2.15
+import QtTest 1.15
+
+import StatusQ 0.1
+
+import AppLayouts.Wallet.stores 1.0
+import AppLayouts.Wallet.panels 1.0
+import AppLayouts.Wallet.popups.swap 1.0
+
+import shared.stores 1.0
+
+import SortFilterProxyModel 0.2
+
+import Models 1.0
+import Storybook 1.0
+
+Item {
+ id: root
+ width: 600
+ height: 400
+
+ QtObject {
+ id: d
+
+ readonly property SwapModalAdaptor adaptor: SwapModalAdaptor {
+ swapStore: SwapStore {
+ readonly property var accounts: WalletAccountsModel {}
+ readonly property var flatNetworks: NetworksModel.flatNetworks
+ readonly property bool areTestNetworksEnabled: false
+ }
+ walletAssetsStore: WalletAssetsStore {
+ id: thisWalletAssetStore
+ walletTokensStore: TokensStore {
+ plainTokensBySymbolModel: TokensBySymbolModel {}
+ }
+ readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
+ assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
+ }
+ currencyStore: CurrenciesStore {}
+ swapFormData: SwapInputParamsForm {}
+ }
+ }
+
+ Component {
+ id: componentUnderTest
+ SwapInputPanel {
+ anchors.centerIn: parent
+
+ currencyStore: d.adaptor.currencyStore
+ flatNetworksModel: d.adaptor.filteredFlatNetworksModel
+ processedAssetsModel: d.adaptor.processedAssetsModel
+ }
+ }
+
+ property SwapInputPanel controlUnderTest: null
+
+ TestCase {
+ name: "SwapInputPanel"
+ when: windowShown
+
+ function test_basicSetupAndDefaults() {
+ controlUnderTest = createTemporaryObject(componentUnderTest, root)
+ verify(!!controlUnderTest)
+ verify(controlUnderTest.width > 0)
+ verify(controlUnderTest.height > 0)
+
+ tryCompare(controlUnderTest, "swapSide", SwapInputPanel.SwapSide.Pay)
+ tryCompare(controlUnderTest, "caption", qsTr("Pay"))
+ tryCompare(controlUnderTest, "selectedHoldingId", "")
+ tryCompare(controlUnderTest, "cryptoValue", 0)
+ tryCompare(controlUnderTest, "cryptoValueRaw", "0")
+ }
+
+ function test_basicSetupReceiveSide() {
+ controlUnderTest = createTemporaryObject(componentUnderTest, root, {swapSide: SwapInputPanel.SwapSide.Receive})
+
+ verify(!!controlUnderTest)
+ verify(controlUnderTest.width > 0)
+ verify(controlUnderTest.height > 0)
+
+ tryCompare(controlUnderTest, "swapSide", SwapInputPanel.SwapSide.Receive)
+ tryCompare(controlUnderTest, "caption", qsTr("Receive"))
+ tryCompare(controlUnderTest, "selectedHoldingId", "")
+ tryCompare(controlUnderTest, "cryptoValue", 0)
+ tryCompare(controlUnderTest, "cryptoValueRaw", "0")
+ }
+
+ function test_basicSetupWithInitialProperties() {
+ controlUnderTest = createTemporaryObject(componentUnderTest, root,
+ {
+ swapSide: SwapInputPanel.SwapSide.Pay,
+ tokenKey: "STT",
+ tokenAmount: "10000000.0000001"
+ })
+ verify(!!controlUnderTest)
+ waitForRendering(controlUnderTest)
+
+ tryCompare(controlUnderTest, "swapSide", SwapInputPanel.SwapSide.Pay)
+ tryCompare(controlUnderTest, "selectedHoldingId", "STT")
+ tryCompare(controlUnderTest, "cryptoValue", 10000000.0000001)
+ verify(controlUnderTest.cryptoValueValid)
+ }
+
+ function test_setTokenKeyAndAmounts_data() {
+ return [
+ { tag: "1.42", tokenAmount: "1.42", valid: true },
+ { tag: "0.00001", tokenAmount: "0.00001", valid: true },
+ { tag: "1234567890", tokenAmount: "1234567890", valid: true },
+ { tag: "1234567890.1234567890", tokenAmount: "1234567890.1234567890", valid: true },
+ { tag: "abc", tokenAmount: "abc", valid: false },
+ { tag: "NaN", tokenAmount: "NaN", valid: false }
+ ]
+ }
+
+ function test_setTokenKeyAndAmounts(data) {
+ const valid = data.valid
+ const tokenAmount = data.tokenAmount
+ const tokenSymbol = "STT"
+
+ controlUnderTest = createTemporaryObject(componentUnderTest, root)
+ verify(!!controlUnderTest)
+ controlUnderTest.tokenKey = tokenSymbol
+ controlUnderTest.tokenAmount = tokenAmount
+
+ tryCompare(controlUnderTest, "selectedHoldingId", tokenSymbol)
+ if (!valid)
+ expectFail(data.tag, "Invalid data expected to fail: %1".arg(tokenAmount))
+ tryCompare(controlUnderTest, "cryptoValue", parseFloat(tokenAmount))
+ tryCompare(controlUnderTest, "cryptoValueValid", true)
+
+ const holdingSelector = findChild(controlUnderTest, "holdingSelector")
+ verify(!!holdingSelector)
+ tryCompare(holdingSelector.selectedItem, "symbol", tokenSymbol)
+
+ const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
+ verify(!!amountToSendInput)
+ tryCompare(amountToSendInput.input, "text", Number(tokenAmount).toLocaleString(Qt.locale(), 'f', -128))
+ }
+
+ function test_enterTokenAmountLocalizedNumber() {
+ controlUnderTest = createTemporaryObject(componentUnderTest, root, {tokenKey: "STT"})
+ verify(!!controlUnderTest)
+ waitForRendering(controlUnderTest)
+ tryCompare(controlUnderTest, "selectedHoldingId", "STT")
+
+ const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
+ verify(!!amountToSendInput)
+ mouseClick(amountToSendInput)
+ waitForRendering(amountToSendInput)
+ verify(amountToSendInput.input.input.edit.activeFocus)
+
+ amountToSendInput.input.locale = Qt.locale("cs_CZ")
+ compare(amountToSendInput.input.locale.name, "cs_CZ")
+
+ // manually entering "1000000,00000042" meaning "1000000,00000042"; `,` being the decimal separator
+ keyClick(Qt.Key_1)
+ for (let i = 0; i < 6; i++)
+ keyClick(Qt.Key_0)
+ keyClick(Qt.Key_Comma)
+ for (let i = 0; i < 6; i++)
+ keyClick(Qt.Key_0)
+ keyClick(Qt.Key_4)
+ keyClick(Qt.Key_2)
+
+ tryCompare(amountToSendInput.input, "text", "1000000,00000042")
+ tryCompare(controlUnderTest, "cryptoValue", 1000000.00000042)
+ verify(controlUnderTest.cryptoValueValid)
+ }
+
+ function test_selectSTTHoldingAndTypeAmount() {
+ controlUnderTest = createTemporaryObject(componentUnderTest, root)
+ verify(!!controlUnderTest)
+
+ const holdingSelector = findChild(controlUnderTest, "holdingSelector")
+ verify(!!holdingSelector)
+ mouseClick(holdingSelector)
+ waitForRendering(holdingSelector)
+
+ const assetSelectorList = findChild(holdingSelector, "assetSelectorList")
+ verify(!!assetSelectorList)
+ waitForRendering(assetSelectorList)
+
+ const sttDelegate = findChild(assetSelectorList, "AssetSelector_ItemDelegate_STT")
+ verify(!!sttDelegate)
+ mouseClick(sttDelegate, 40, 40) // center might be covered by tags
+
+ tryCompare(controlUnderTest, "selectedHoldingId", "STT")
+
+ const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
+ verify(!!amountToSendInput)
+ mouseClick(amountToSendInput)
+ waitForRendering(amountToSendInput)
+ verify(amountToSendInput.input.input.edit.activeFocus)
+
+ keyClick(Qt.Key_1)
+ keyClick(Qt.Key_Period)
+ keyClick(Qt.Key_4)
+ keyClick(Qt.Key_2)
+
+ tryCompare(controlUnderTest, "cryptoValue", 1.42)
+ verify(controlUnderTest.cryptoValueValid)
+ }
+
+ function test_clickingMaxButton() {
+ controlUnderTest = createTemporaryObject(componentUnderTest, root, {tokenKey: "ETH"})
+ verify(!!controlUnderTest)
+ waitForRendering(controlUnderTest)
+ tryCompare(controlUnderTest, "selectedHoldingId", "ETH")
+
+ const maxTagButton = findChild(controlUnderTest, "maxTagButton")
+ verify(!!maxTagButton)
+ waitForRendering(maxTagButton)
+ verify(maxTagButton.visible)
+ mouseClick(maxTagButton)
+
+ const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
+ verify(!!amountToSendInput)
+ waitForRendering(amountToSendInput)
+ const maxValue = amountToSendInput.maxInputBalance
+
+ tryCompare(amountToSendInput.input, "text", maxValue.toLocaleString(Qt.locale(), 'f', -128))
+ tryCompare(controlUnderTest, "cryptoValue", maxValue)
+ verify(controlUnderTest.cryptoValueValid)
+ }
+
+ function test_loadingState() {
+ controlUnderTest = createTemporaryObject(componentUnderTest, root)
+ verify(!!controlUnderTest)
+
+ controlUnderTest.loading = true
+
+ const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
+ verify(!!amountToSendInput)
+
+ const amountInput = findChild(amountToSendInput, "amountInput")
+ verify(!!amountInput)
+ verify(!amountInput.visible)
+
+ const topAmountToSendInputLoadingComponent = findChild(amountToSendInput, "topAmountToSendInputLoadingComponent")
+ verify(!!topAmountToSendInputLoadingComponent)
+ verify(topAmountToSendInputLoadingComponent.visible)
+
+ const bottomItemText = findChild(amountToSendInput, "bottomItemText")
+ verify(!!bottomItemText)
+ verify(!bottomItemText.visible)
+
+ const bottomItemTextLoadingComponent = findChild(amountToSendInput, "bottomItemTextLoadingComponent")
+ verify(!!bottomItemTextLoadingComponent)
+ verify(bottomItemTextLoadingComponent.visible)
+ }
+ }
+}
diff --git a/storybook/qmlTests/tests/tst_SwapModal.qml b/storybook/qmlTests/tests/tst_SwapModal.qml
index 6a55c2687a..8d0ea31ad8 100644
--- a/storybook/qmlTests/tests/tst_SwapModal.qml
+++ b/storybook/qmlTests/tests/tst_SwapModal.qml
@@ -33,7 +33,7 @@ Item {
walletAssetsStore: WalletAssetsStore {
id: thisWalletAssetStore
walletTokensStore: TokensStore {
- readonly property var plainTokensBySymbolModel: TokensBySymbolModel {}
+ plainTokensBySymbolModel: TokensBySymbolModel {}
}
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
diff --git a/storybook/stubs/AppLayouts/Wallet/stores/SwapStore.qml b/storybook/stubs/AppLayouts/Wallet/stores/SwapStore.qml
index e529905281..c80bf1275e 100644
--- a/storybook/stubs/AppLayouts/Wallet/stores/SwapStore.qml
+++ b/storybook/stubs/AppLayouts/Wallet/stores/SwapStore.qml
@@ -1,4 +1,4 @@
-import QtQuick 2.15
+import QtQml 2.15
QtObject {
id: root
diff --git a/storybook/stubs/AppLayouts/Wallet/stores/TokensStore.qml b/storybook/stubs/AppLayouts/Wallet/stores/TokensStore.qml
index a846ebecfe..b8110f532f 100644
--- a/storybook/stubs/AppLayouts/Wallet/stores/TokensStore.qml
+++ b/storybook/stubs/AppLayouts/Wallet/stores/TokensStore.qml
@@ -3,6 +3,8 @@ import QtQuick 2.15
QtObject {
id: root
- property bool displayAssetsBelowBalance: false
+ property var plainTokensBySymbolModel
+ property bool displayAssetsBelowBalance
property var getDisplayAssetsBelowBalanceThresholdDisplayAmount
+ property double tokenListUpdatedAt
}
diff --git a/storybook/stubs/AppLayouts/Wallet/stores/WalletAssetsStore.qml b/storybook/stubs/AppLayouts/Wallet/stores/WalletAssetsStore.qml
index 9dfc349434..0da7038b86 100644
--- a/storybook/stubs/AppLayouts/Wallet/stores/WalletAssetsStore.qml
+++ b/storybook/stubs/AppLayouts/Wallet/stores/WalletAssetsStore.qml
@@ -8,7 +8,7 @@ import Models 1.0
QtObject {
id: root
- property TokensStore walletTokensStore
+ property TokensStore walletTokensStore: TokensStore {}
readonly property var groupedAccountsAssetsModel: GroupedAccountsAssetsModel {}
property var assetsWithFilteredBalances
@@ -56,5 +56,11 @@ QtObject {
joinRole: "communityId"
}
- property var assetsController
+ property var assetsController: QtObject {
+ property int revision
+
+ function filterAcceptsSymbol(symbol) {
+ return true
+ }
+ }
}
diff --git a/storybook/stubs/shared/stores/CurrenciesStore.qml b/storybook/stubs/shared/stores/CurrenciesStore.qml
index 06ec1a4ffc..517dc1aacc 100644
--- a/storybook/stubs/shared/stores/CurrenciesStore.qml
+++ b/storybook/stubs/shared/stores/CurrenciesStore.qml
@@ -1,6 +1,7 @@
import QtQuick 2.15
import StatusQ.Core 0.1
+import StatusQ.Core.Utils 0.1 as SQUtils
QtObject {
id: root
@@ -16,6 +17,12 @@ QtObject {
return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale)
}
+ function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
+ let bigIntBalance = SQUtils.AmountsArithmetic.fromString(balance)
+ let decimalBalance = SQUtils.AmountsArithmetic.toNumber(bigIntBalance, decimals)
+ return formatCurrencyAmount(decimalBalance, symbol)
+ }
+
function getFiatValue(balance, cryptoSymbol) {
return balance
}
diff --git a/storybook/stubs/shared/stores/send/TransactionStore.qml b/storybook/stubs/shared/stores/send/TransactionStore.qml
index 9314dc9869..0308538bed 100644
--- a/storybook/stubs/shared/stores/send/TransactionStore.qml
+++ b/storybook/stubs/shared/stores/send/TransactionStore.qml
@@ -12,7 +12,7 @@ import AppLayouts.Wallet.stores 1.0
QtObject {
id: root
- readonly property var currencyStore: CurrenciesStore{}
+ readonly property CurrenciesStore currencyStore: CurrenciesStore {}
readonly property var senderAccounts: WalletSendAccountsModel {
Component.onCompleted: selectedSenderAccount = senderAccounts.get(0)
}
@@ -280,7 +280,7 @@ QtObject {
},
FastExpressionRole {
name: "currentBalance"
- expression: __getTotalBalance(model.balances, model.decimals, model.symbol, root.selectedSenderAccount)
+ expression: __getTotalBalance(model.balances, model.decimals)
expectedRoles: ["balances", "decimals", "symbol"]
},
FastExpressionRole {
@@ -302,7 +302,7 @@ QtObject {
name.toUpperCase().startsWith(searchString.toUpperCase()) || __searchAddressInList(addressPerChain, searchString)
)
}
- expression: search(symbol, name, addressPerChain, root.assetSearchString)
+ expression: search(model.symbol, model.name, model.addressPerChain, root.assetSearchString)
expectedRoles: ["symbol", "name", "addressPerChain"]
},
ValueFilter {
@@ -339,7 +339,7 @@ QtObject {
}
/* Internal function to calculate total balance */
- function __getTotalBalance(balances, decimals, symbol) {
+ function __getTotalBalance(balances, decimals) {
let totalBalance = 0
for(let i=0; i {
- // additionally accept dot (.) and convert it to the correct decimal point char
- if (event.key === Qt.Key_Period || event.key === Qt.Key_Comma) {
- // Only one decimal point is allowed
- if(root.text.indexOf(root.locale.decimalPoint) === -1)
- root.input.insert(root.input.cursorPosition, root.locale.decimalPoint)
- event.accepted = true
- } else if ((event.key > Qt.Key_9 && event.key <= Qt.Key_BraceRight) || event.key === Qt.Key_Space || event.key === Qt.Key_Tab) {
- event.accepted = true
- }
- }
+ onKeyPressed:
+ (event) => {
+ // additionally accept dot (.) and convert it to the correct decimal point char
+ if (event.key === Qt.Key_Period || event.key === Qt.Key_Comma) {
+ // Only one decimal point is allowed
+ if(root.text.indexOf(root.locale.decimalPoint) === -1) {
+ root.input.insert(root.input.cursorPosition, root.locale.decimalPoint)
+ event.accepted = true
+ }
+ } else if (event.modifiers === Qt.NoModifier && ((event.key > Qt.Key_9 && event.key <= Qt.Key_BraceRight) || event.key === Qt.Key_Space || event.key === Qt.Key_Tab)) {
+ event.accepted = true
+ }
+ }
}
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml
index e40e892f9e..ee71f10ad1 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusBaseInput.qml
@@ -400,7 +400,7 @@ Item {
KeyNavigation.tab: root.tabNavItem
Keys.onPressed: {
edit.keyEvent = event.key
- root.keyPressed(event);
+ root.keyPressed(event)
}
onCursorRectangleChanged: Utils.ensureVisible(flick, cursorRectangle)
onActiveFocusChanged: if (root.pristine) root.pristine = false
diff --git a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml
index 0850b62502..bdcedf343e 100644
--- a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml
+++ b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml
@@ -72,6 +72,7 @@ QtObject {
function stripTrailingZeroes(numStr, locale) {
+ locale = locale || Qt.locale()
let regEx = locale.decimalPoint == "." ? /(\.[0-9]*[1-9])0+$|\.0*$/ : /(\,[0-9]*[1-9])0+$|\,0*$/
return numStr.replace(regEx, '$1')
}
@@ -157,10 +158,10 @@ QtObject {
var optDisplayDecimals = currencyAmount.displayDecimals
var optStripTrailingZeroes = currencyAmount.stripTrailingZeroes
if (options) {
- if (options.noSymbol !== undefined) {
+ if (options.noSymbol !== undefined && options.noSymbol === true) {
optNoSymbol = true
}
- if (options.rawAmount !== undefined) {
+ if (options.rawAmount !== undefined && options.rawAmount === true) {
optRawAmount = true
}
if (options.minDecimals !== undefined && options.minDecimals > optDisplayDecimals) {
@@ -175,8 +176,7 @@ QtObject {
var amountSuffix = ""
let minAmount = 10**-optDisplayDecimals
- if (currencyValue > 0 && currencyValue < minAmount && !optRawAmount)
- {
+ if (currencyValue > 0 && currencyValue < minAmount && !optRawAmount) {
// Handle amounts smaller than resolution
amountStr = "<%1".arg(numberToLocaleString(minAmount, optDisplayDecimals, locale))
} else {
diff --git a/ui/app/AppLayouts/Wallet/controls/SwapExchangeButton.qml b/ui/app/AppLayouts/Wallet/controls/SwapExchangeButton.qml
index b11c0dfd5f..829bd38bef 100644
--- a/ui/app/AppLayouts/Wallet/controls/SwapExchangeButton.qml
+++ b/ui/app/AppLayouts/Wallet/controls/SwapExchangeButton.qml
@@ -2,7 +2,6 @@ import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Controls 0.1
-import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
StatusButton {
@@ -12,6 +11,7 @@ StatusButton {
icon.name: hovered ? "arrow-up" : "arrow-down"
icon.color: Theme.palette.baseColor1
+ focusPolicy: Qt.NoFocus
isRoundIcon: true
radius: height/2
normalColor: Theme.palette.indirectColor3
diff --git a/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml b/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml
new file mode 100644
index 0000000000..c16454ed7a
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml
@@ -0,0 +1,277 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Shapes 1.15
+
+import StatusQ 0.1
+import StatusQ.Components 0.1
+import StatusQ.Controls 0.1
+import StatusQ.Core 0.1
+import StatusQ.Core.Utils 0.1 as SQUtils
+import StatusQ.Core.Theme 0.1
+
+import shared.popups.send.views 1.0
+import shared.popups.send.panels 1.0
+
+import utils 1.0
+import shared.stores 1.0
+
+import SortFilterProxyModel 0.2
+
+Control {
+ id: root
+
+ // input API
+ required property CurrenciesStore currencyStore
+ required property var flatNetworksModel
+ required property var processedAssetsModel
+
+ property string tokenKey
+ onTokenKeyChanged: {
+ if (!!tokenKey)
+ Qt.callLater(d.setSelectedHoldingId, tokenKey, Constants.TokenType.ERC20)
+ }
+ property string tokenAmount
+ onTokenAmountChanged: {
+ if (!!tokenAmount)
+ Qt.callLater(() => amountToSendInput.input.text = Number(tokenAmount).toLocaleString(Qt.locale(), 'f', -128))
+ }
+
+ property int swapSide: SwapInputPanel.SwapSide.Pay
+ property bool fiatInputInteractive
+ property bool loading
+
+ // output API
+ readonly property string selectedHoldingId: d.selectedHoldingId
+ readonly property double cryptoValue: amountToSendInput.cryptoValueToSendFloat
+ readonly property string cryptoValueRaw: amountToSendInput.cryptoValueToSend
+ readonly property bool cryptoValueValid: amountToSendInput.inputNumberValid
+
+ // visual properties
+ property int swapExchangeButtonWidth: 44
+ property string caption: swapSide === SwapInputPanel.SwapSide.Pay ? qsTr("Pay") : qsTr("Receive")
+
+ enum SwapSide {
+ Pay = 0,
+ Receive = 1
+ }
+
+ padding: Style.current.padding
+
+ // by design
+ implicitWidth: 492
+ implicitHeight: 131
+
+ Component.onCompleted: {
+ if (root.swapSide === SwapInputPanel.SwapSide.Pay)
+ amountToSendInput.input.forceActiveFocus()
+ }
+
+ QtObject {
+ id: d
+
+ function setSelectedHoldingId(holdingId, holdingType) {
+ let holding = SQUtils.ModelUtils.getByKey(root.processedAssetsModel, "symbol", holdingId)
+ d.selectedHoldingId = holdingId
+ d.setSelectedHolding(holding, holdingType)
+ }
+
+ function setSelectedHolding(holding, holdingType) {
+ d.selectedHoldingType = holdingType
+ d.selectedHolding = holding
+ holdingSelector.setSelectedItem(holding, holdingType)
+ }
+
+ property var selectedHolding: null
+ property var selectedHoldingType: Constants.TokenType.Unknown
+ property string selectedHoldingId
+
+ readonly property bool isSelectedHoldingValidAsset: !!selectedHolding && selectedHoldingType === Constants.TokenType.ERC20
+ readonly property double maxFiatBalance: isSelectedHoldingValidAsset ? selectedHolding.currentCurrencyBalance : 0
+ readonly property double maxCryptoBalance: isSelectedHoldingValidAsset ? selectedHolding.currentBalance : 0
+ readonly property double maxInputBalance: amountToSendInput.inputIsFiat ? maxFiatBalance : maxCryptoBalance
+ readonly property string inputSymbol: amountToSendInput.inputIsFiat ? root.currencyStore.currentCurrency :
+ !!d.selectedHolding && !!d.selectedHolding.symbol ? d.selectedHolding.symbol: ""
+ readonly property string maxInputBalanceFormatted:
+ root.currencyStore.formatCurrencyAmount(Math.trunc(prepareForMaxSend(d.maxInputBalance, d.inputSymbol)*100)/100, d.inputSymbol, {noSymbol: !amountToSendInput.inputIsFiat})
+
+ function prepareForMaxSend(value, symbol) {
+ if (symbol !== Constants.ethToken) {
+ return value
+ }
+
+ return value - Math.max(0.0001, Math.min(0.01, value * 0.1))
+ }
+
+ property string searchText
+ }
+
+ background: Shape {
+ id: shape
+
+ property int radius: 16
+ property int leftTopRadius: radius
+ property int rightTopRadius: radius
+ property int leftBottomRadius: radius
+ property int rightBottomRadius: radius
+
+ readonly property int cutoutGap: 4
+
+ scale: swapSide === SwapInputPanel.SwapSide.Pay ? -1 : 1
+
+ ShapePath {
+ id: path
+ fillColor: Theme.palette.indirectColor3
+ strokeColor: amountToSendInput.input.input.edit.activeFocus ? Theme.palette.directColor7 : Theme.palette.directColor8
+ strokeWidth: 1
+ capStyle: ShapePath.RoundCap
+
+ startX: shape.leftTopRadius
+ startY: 0
+
+ PathLine {
+ x: shape.width/2 - root.swapExchangeButtonWidth/2 - (shape.cutoutGap/2 + path.strokeWidth)
+ y: 0
+ }
+ PathArc { // the cutout
+ relativeX: root.swapExchangeButtonWidth + (shape.cutoutGap + path.strokeWidth*2)
+ direction: PathArc.Counterclockwise
+ radiusX: root.swapExchangeButtonWidth/2 + path.strokeWidth
+ radiusY: root.swapExchangeButtonWidth/2 - path.strokeWidth/2
+ }
+ PathLine {
+ x: shape.width - shape.rightTopRadius
+ y: 0
+ }
+
+ PathArc {
+ x: shape.width
+ y: shape.rightTopRadius
+ radiusX: shape.rightTopRadius
+ radiusY: shape.rightTopRadius
+ }
+ PathLine {
+ x: shape.width
+ y: shape.height - shape.rightBottomRadius
+ }
+ PathArc {
+ x: shape.width - shape.rightBottomRadius
+ y: shape.height
+ radiusX: shape.rightBottomRadius
+ radiusY: shape.rightBottomRadius
+ }
+ PathLine {
+ x: shape.leftBottomRadius
+ y: shape.height
+ }
+ PathArc {
+ x: 0
+ y: shape.height - shape.leftBottomRadius
+ radiusX: shape.leftBottomRadius
+ radiusY: shape.leftBottomRadius
+ }
+ PathLine {
+ x: 0
+ y: shape.leftTopRadius
+ }
+ PathArc {
+ x: shape.leftTopRadius
+ y: 0
+ radiusX: shape.leftTopRadius
+ radiusY: shape.leftTopRadius
+ }
+ }
+ }
+
+ contentItem: RowLayout {
+ spacing: 20
+ ColumnLayout {
+ Layout.preferredWidth: parent.width*.66
+ Layout.fillHeight: true
+
+ AmountToSend {
+ Layout.fillWidth: true
+ id: amountToSendInput
+ objectName: "amountToSendInput"
+ caption: root.caption
+ interactive: true
+ selectedHolding: d.selectedHolding
+ fiatInputInteractive: root.fiatInputInteractive
+
+ multiplierIndex: d.isSelectedHoldingValidAsset && !!holdingSelector.selectedItem && !!holdingSelector.selectedItem.decimals
+ ? holdingSelector.selectedItem.decimals
+ : 0
+
+ maxInputBalance: (root.swapSide === SwapInputPanel.SwapSide.Receive || !d.isSelectedHoldingValidAsset) ? Number.POSITIVE_INFINITY
+ : d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
+ currentCurrency: root.currencyStore.currentCurrency
+ formatCurrencyAmount: root.currencyStore.formatCurrencyAmount
+ loading: root.loading
+ }
+ }
+ ColumnLayout {
+ Layout.preferredWidth: parent.width*.33
+
+ Item { Layout.fillHeight: true }
+
+ HoldingSelector {
+ id: holdingSelector
+ objectName: "holdingSelector"
+ Layout.rightMargin: d.isSelectedHoldingValidAsset ? -root.padding : 0
+ Layout.alignment: Qt.AlignRight
+ Layout.preferredHeight: 38
+
+ searchPlaceholderText: qsTr("Search asset name or symbol")
+ assetsModel: SortFilterProxyModel {
+ sourceModel: root.processedAssetsModel
+ filters: FastExpressionFilter {
+ function search(symbol, name, searchString) {
+ return (symbol.toUpperCase().includes(searchString.toUpperCase())
+ || name.toUpperCase().includes(searchString.toUpperCase()))
+ }
+ expression: search(model.symbol, model.name, d.searchText)
+ expectedRoles: ["symbol", "name"]
+ }
+ }
+ networksModel: root.flatNetworksModel
+ formatCurrentCurrencyAmount: function(balance) {
+ return root.currencyStore.formatCurrencyAmount(balance, root.currencyStore.currentCurrency)
+ }
+ formatCurrencyAmountFromBigInt: function(balance, symbol, decimals) {
+ return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
+ }
+ onItemSelected: {
+ d.setSelectedHoldingId(holdingId, holdingType)
+ amountToSendInput.input.forceActiveFocus()
+ }
+ onSearchTextChanged: d.searchText = searchText
+ }
+
+ Item { Layout.fillHeight: !itemTag.visible }
+
+ StatusListItemTag {
+ id: itemTag
+ objectName: "maxTagButton"
+ Layout.alignment: Qt.AlignRight
+ Layout.maximumWidth: parent.width
+ Layout.preferredHeight: 22
+ visible: d.isSelectedHoldingValidAsset && root.swapSide === SwapInputPanel.SwapSide.Pay
+ title: d.maxInputBalance > 0 ? qsTr("Max: %1").arg(d.maxInputBalanceFormatted)
+ : qsTr("No balances active")
+ tagClickable: true
+ closeButtonVisible: false
+ titleText.font.pixelSize: 12
+ bgColor: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor3 : Theme.palette.dangerColor2
+ titleText.color: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor1 : Theme.palette.dangerColor1
+ onTagClicked: {
+ const max = d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
+ if (max > 0)
+ amountToSendInput.input.text = max.toLocaleString(Qt.locale(), 'f', -128)
+ else
+ amountToSendInput.input.input.edit.clear()
+ amountToSendInput.input.forceActiveFocus()
+ }
+ }
+ }
+ }
+}
diff --git a/ui/app/AppLayouts/Wallet/panels/qmldir b/ui/app/AppLayouts/Wallet/panels/qmldir
index 4365d91e60..36baf2a03a 100644
--- a/ui/app/AppLayouts/Wallet/panels/qmldir
+++ b/ui/app/AppLayouts/Wallet/panels/qmldir
@@ -5,4 +5,5 @@ ActivityFilterPanel 1.0 ActivityFilterPanel.qml
ManageAssetsPanel 1.0 ManageAssetsPanel.qml
ManageCollectiblesPanel 1.0 ManageCollectiblesPanel.qml
ManageHiddenPanel 1.0 ManageHiddenPanel.qml
-DAppsWorkflow 1.0 DAppsWorkflow.qml
\ No newline at end of file
+DAppsWorkflow 1.0 DAppsWorkflow.qml
+SwapInputPanel 1.0 SwapInputPanel.qml
diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapInputParamsForm.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapInputParamsForm.qml
index 770f6a47d0..a27aac8ff9 100644
--- a/ui/app/AppLayouts/Wallet/popups/swap/SwapInputParamsForm.qml
+++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapInputParamsForm.qml
@@ -1,6 +1,4 @@
-import QtQuick 2.13
-
-import utils 1.0
+import QtQml 2.15
/* This is used so that there is an easy way to fill in the data
needed to launch the Swap Modal with pre-filled requisites. */
@@ -11,5 +9,5 @@ QtObject {
property string fromTokensKey: ""
property string fromTokenAmount: ""
property string toTokenKey: ""
+ property string toTokenAmount
}
-
diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml
index 00189278c8..af700d2a46 100644
--- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml
+++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml
@@ -1,4 +1,4 @@
-import QtQuick 2.13
+import QtQuick 2.15
import QtQuick.Layouts 1.15
import utils 1.0
diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml
index efa65f8eb5..a2fa5cbb10 100644
--- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml
+++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml
@@ -55,8 +55,12 @@ QObject {
return networkString
}
- function formatCurrencyAmount(balance, symbol) {
- return root.currencyStore.formatCurrencyAmount(balance, symbol)
+ function formatCurrencyAmount(balance, symbol, options = null, locale = null) {
+ return root.currencyStore.formatCurrencyAmount(balance, symbol, options, locale)
+ }
+
+ function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
+ return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
}
// TODO: remove once the AccountsModalHeader is reworked!!
@@ -67,9 +71,80 @@ QObject {
return null
}
+ // Model prepared to provide filtered and sorted assets as per the advanced Settings in token management
+ readonly property var processedAssetsModel: SortFilterProxyModel {
+ property real displayAssetsBelowBalanceThresholdAmount: root.walletAssetsStore.walletTokensStore.getDisplayAssetsBelowBalanceThresholdDisplayAmount()
+ sourceModel: __assetsWithFilteredBalances
+ proxyRoles: [
+ FastExpressionRole {
+ name: "isCommunityAsset"
+ expression: !!model.communityId
+ expectedRoles: ["communityId"]
+ },
+ FastExpressionRole {
+ name: "currentBalance"
+ expression: __getTotalBalance(model.balances, model.decimals)
+ expectedRoles: ["balances", "decimals"]
+ },
+ FastExpressionRole {
+ name: "currentCurrencyBalance"
+ expression: {
+ if (!!model.marketDetails) {
+ return model.currentBalance * model.marketDetails.currencyPrice.amount
+ }
+ return 0
+ }
+ expectedRoles: ["marketDetails", "currentBalance"]
+ }
+ ]
+ filters: [
+ FastExpressionFilter {
+ expression: {
+ root.walletAssetsStore.assetsController.revision
+
+ if (!root.walletAssetsStore.assetsController.filterAcceptsSymbol(model.symbol)) // explicitely hidden
+ return false
+ if (model.isCommunityAsset) // do not show community assets
+ return false
+ if (root.walletAssetsStore.walletTokensStore.displayAssetsBelowBalance)
+ return model.currentCurrencyBalance > processedAssetsModel.displayAssetsBelowBalanceThresholdAmount
+ return true
+ }
+ expectedRoles: ["symbol", "isCommunityAsset", "currentCurrencyBalance"]
+ }
+ ]
+ // FIXME sort by assetsController instead, to have the sorting/order as in the main wallet view
+ // sorters: RoleSorter {
+ // roleName: "isCommunityAsset"
+ // }
+ }
+
// Internal properties and functions -----------------------------------------------------------------------------------------------------------------------------
readonly property var __fromToken: ModelUtils.getByKey(root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel, "key", root.swapFormData.fromTokensKey)
+ // Internal model filtering balances by the account selected in the AccountsModalHeader
+ SubmodelProxyModel {
+ id: __assetsWithFilteredBalances
+ sourceModel: root.walletAssetsStore.groupedAccountAssetsModel
+ submodelRoleName: "balances"
+ delegateModel: SortFilterProxyModel {
+ sourceModel: submodel
+
+ filters: [
+ ValueFilter {
+ roleName: "chainId"
+ value: root.swapFormData.selectedNetworkChainId
+ enabled: root.swapFormData.selectedNetworkChainId !== -1
+ }/*,
+ // TODO enable once AccountsModalHeader is reworked!!
+ ValueFilter {
+ roleName: "account"
+ value: root.selectedSenderAccount.address
+ }*/
+ ]
+ }
+ }
+
SubmodelProxyModel {
id: filteredBalancesModel
sourceModel: root.walletAssetsStore.baseGroupedAccountAssetModel
@@ -104,4 +179,14 @@ QObject {
}
return null
}
+
+ /* Internal function to calculate total balance */
+ function __getTotalBalance(balances, decimals) {
+ let totalBalance = 0
+ for(let i=0; i d.indexesThatCanBeShown
- }
- }
Component {
id: expandedItem
StatusListItemTag {
@@ -98,8 +76,9 @@ StatusListItem {
asset.width: 16
asset.height: 16
asset.isImage: true
- asset.name: Style.svg("tiny/%1".arg(iconUrl))
- visible: root.sensor.containsMouse && index <= d.indexesThatCanBeShown
+ asset.name: Style.svg("tiny/%1".arg(model.iconUrl))
+ tagClickable: true
+ onTagClicked: d.selectToken()
}
}
}
diff --git a/ui/imports/shared/popups/send/panels/HoldingItemSelector.qml b/ui/imports/shared/popups/send/panels/HoldingItemSelector.qml
index eb5a602106..9aac3b44dd 100644
--- a/ui/imports/shared/popups/send/panels/HoldingItemSelector.qml
+++ b/ui/imports/shared/popups/send/panels/HoldingItemSelector.qml
@@ -1,17 +1,12 @@
-import QtQuick 2.13
-import QtQuick.Controls 2.13
-import QtQuick.Layouts 1.13
-
-import SortFilterProxyModel 0.2
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
-import StatusQ.Core.Utils 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
-import StatusQ.Core.Backpressure 1.0
-import shared.controls 1.0
import utils 1.0
Item {
@@ -40,12 +35,6 @@ Item {
property int contentIconSize: 21
property int contentTextSize: 28
- function resetInternal() {
- items = null
- selectedItem = null
- hoveredItem = null
- }
-
function openPopup() {
root.comboBoxControl.popup.open()
}
@@ -68,9 +57,8 @@ Item {
property string iconSource: ""
onIconSourceChanged: tokenIcon.image.source = iconSource
- property string text: ""
+ property string text: qsTr("Select asset")
readonly property bool isItemSelected: !!root.selectedItem || !!root.hoveredItem
-
}
StatusComboBox {
@@ -80,9 +68,7 @@ Item {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
- width: Math.min(implicitWidth, parent.width)
-
- control.padding: 4
+ control.padding: 12
control.popup.width: 492
control.popup.x: -root.x
control.popup.verticalPadding: 0
@@ -92,20 +78,21 @@ Item {
model: root.comboBoxModel
control.background: Rectangle {
- color: "transparent"
+ color: !d.isItemSelected ? Theme.palette.primaryColor3 : "transparent"
border.width: d.isItemSelected ? 0 : 1
border.color: Theme.palette.directColor7
- radius: 12
+ radius: 8
+
+ HoverHandler {
+ cursorShape: root.enabled ? Qt.PointingHandCursor : undefined
+ }
}
contentItem: RowLayout {
- id: rowLayout
- implicitHeight: 38
StatusRoundedImage {
id: tokenIcon
Layout.preferredWidth: root.contentIconSize
Layout.preferredHeight: root.contentIconSize
- Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
visible: !!d.iconSource
image.source: d.iconSource
image.onStatusChanged: {
@@ -116,22 +103,17 @@ Item {
}
StatusBaseText {
Layout.fillWidth: true
- Layout.alignment: Qt.AlignVCenter
font.pixelSize: root.contentTextSize
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
- color: Theme.palette.miscColor1
+ color: Theme.palette.primaryColor1
text: d.text
- visible: d.isItemSelected
}
StatusIcon {
- Layout.leftMargin: -3
- Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 16
Layout.preferredHeight: 16
icon: "chevron-down"
- color: Theme.palette.miscColor1
- visible: !!root.selectedItem
+ color: Theme.palette.primaryColor1
}
}
diff --git a/ui/imports/shared/popups/send/panels/HoldingSelector.qml b/ui/imports/shared/popups/send/panels/HoldingSelector.qml
index 7f37f7cddd..3cb300107e 100644
--- a/ui/imports/shared/popups/send/panels/HoldingSelector.qml
+++ b/ui/imports/shared/popups/send/panels/HoldingSelector.qml
@@ -1,4 +1,3 @@
-
import QtQml 2.15
import QtQuick 2.15
import QtQuick.Layouts 1.15
@@ -22,10 +21,8 @@ Item {
id: root
property var assetsModel
- property string selectedSenderAccount
property var collectiblesModel
property var networksModel
- property string currentCurrencySymbol
property bool onlyAssets: true
property string searchText
@@ -41,6 +38,16 @@ Item {
property alias selectedItem: holdingItemSelector.selectedItem
property alias hoveredItem: holdingItemSelector.hoveredItem
+ property string searchPlaceholderText: {
+ if (d.isCurrentBrowsingTypeAsset) {
+ return qsTr("Search for token or enter token address")
+ } else if (d.isBrowsingGroup) {
+ return qsTr("Search %1").arg(d.currentBrowsingGroupName ?? qsTr("collectibles in collection"))
+ } else {
+ return qsTr("Search collectibles")
+ }
+ }
+
function setSelectedItem(item, holdingType) {
d.browsingHoldingType = holdingType
holdingItemSelector.selectedItem = null
@@ -66,8 +73,8 @@ Item {
[qsTr("Assets")] :
[qsTr("Assets"), qsTr("Collectibles")]
- readonly property var updateSearchText: Backpressure.debounce(root, 1000, function(inputText) {
- searchText = inputText
+ readonly property var updateSearchText: Backpressure.debounce(root, 500, function(inputText) {
+ root.searchText = inputText
})
function isAsset(type) {
@@ -103,10 +110,8 @@ Item {
} else if (asset.image) {
// Community assets have a dedicated image streamed from status-go
return asset.image
- } else {
- return Constants.tokenIcon(asset.symbol)
}
- return ""
+ return Constants.tokenIcon(asset.symbol)
}
property var collectibleTextFn: function (item) {
@@ -167,16 +172,6 @@ Item {
]
}
- readonly property string searchPlaceholderText: {
- if (isCurrentBrowsingTypeAsset) {
- return qsTr("Search for token or enter token address")
- } else if (isBrowsingGroup) {
- return qsTr("Search %1").arg(d.currentBrowsingGroupName ?? qsTr("collectibles in collection"))
- } else {
- return qsTr("Search collectibles")
- }
- }
-
// By design values:
readonly property int padding: 16
readonly property int headerTopMargin: 5
@@ -187,7 +182,6 @@ Item {
readonly property int collectibleContentIconSize: 28
readonly property int assetContentTextSize: 28
readonly property int collectibleContentTextSize: 15
-
}
HoldingItemSelector {
@@ -246,6 +240,7 @@ Item {
contentIconSize: d.isAsset(d.currentHoldingType) ? d.assetContentIconSize : d.collectibleContentIconSize
contentTextSize: d.isAsset(d.currentHoldingType) ? d.assetContentTextSize : d.collectibleContentTextSize
comboBoxListViewSection.property: "isCommunityAsset"
+ // TODO allow for different header/sections for the Swap modal
comboBoxListViewSection.delegate: AssetsSectionDelegate {
height: !!text ? 52 : 0 // if we bind to some property instead of hardcoded value it wont work nice when switching tabs or going inside collection and back
width: ListView.view.width
@@ -253,7 +248,10 @@ Item {
text: Helpers.assetsSectionTitle(section, holdingItemSelector.hasCommunityTokens, d.isBrowsingGroup, d.isCurrentBrowsingTypeAsset)
onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp)
}
+ comboBoxControl.popup.onOpened: comboBoxControl.popup.contentItem.headerItem.focusSearch()
comboBoxControl.popup.onClosed: comboBoxControl.popup.contentItem.headerItem.clear()
+
+ comboBoxControl.popup.x: root.width - comboBoxControl.popup.width
}
Component {
@@ -264,6 +262,10 @@ Item {
Component {
id: headerComponent
ColumnLayout {
+ function focusSearch() {
+ searchInput.input.forceActiveFocus()
+ }
+
function clear() {
searchInput.input.edit.clear()
}
@@ -303,7 +305,7 @@ Item {
CollectibleBackButtonWithInfo {
Layout.fillWidth: true
visible: d.isBrowsingGroup
- count: collectiblesModel.count
+ count: collectiblesModel ? collectiblesModel.count : 0
name: d.currentBrowsingGroupName
onBackClicked: {
if (!d.isCurrentBrowsingTypeAsset) {
@@ -325,7 +327,7 @@ Item {
anchors.fill: parent
input.showBackground: false
- placeholderText: d.searchPlaceholderText
+ placeholderText: root.searchPlaceholderText
onTextChanged: Qt.callLater(d.updateSearchText, text)
input.clearable: true
input.implicitHeight: 56
@@ -344,7 +346,7 @@ Item {
TokenBalancePerChainDelegate {
objectName: "AssetSelector_ItemDelegate_" + symbol
width: holdingItemSelector.comboBoxControl.popup.width
- selectedSenderAccount: root.selectedSenderAccount
+ highlighted: !!holdingItemSelector.selectedItem && symbol === holdingItemSelector.selectedItem.symbol
balancesModel: LeftJoinModel {
leftModel: balances
rightModel: root.networksModel
@@ -370,6 +372,7 @@ Item {
CollectibleNestedDelegate {
objectName: "CollectibleSelector_ItemDelegate_" + groupId
width: holdingItemSelector.comboBoxControl.popup.width
+ highlighted: !!holdingItemSelector.selectedItem && uid === holdingItemSelector.selectedItem.uid
onItemSelected: {
if (isGroup) {
d.currentBrowsingGroupName = groupName
diff --git a/ui/imports/shared/popups/send/views/AmountToSend.qml b/ui/imports/shared/popups/send/views/AmountToSend.qml
index 606458f888..19bc56c430 100644
--- a/ui/imports/shared/popups/send/views/AmountToSend.qml
+++ b/ui/imports/shared/popups/send/views/AmountToSend.qml
@@ -4,6 +4,8 @@ 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.Controls 0.1
+import StatusQ.Components 0.1
import StatusQ.Controls.Validators 0.1
import "../controls"
@@ -14,7 +16,7 @@ ColumnLayout {
id: root
readonly property alias input: topAmountToSendInput
- readonly property bool inputNumberValid: !!input.text && !isNaN(d.parsedInput)
+ readonly property bool inputNumberValid: !!input.text && !isNaN(d.parsedInput) && input.valid
readonly property int minSendCryptoDecimals:
!inputIsFiat ? LocaleUtils.fractionalPartLength(d.inputNumber) : 0
@@ -36,6 +38,10 @@ ColumnLayout {
property bool interactive: false
property bool inputIsFiat: false
+ property string caption: isBridgeTx ? qsTr("Amount to bridge") : qsTr("Amount to send")
+
+ property bool fiatInputInteractive: true
+
// Crypto value to send expressed in base units (like wei for ETH),
// as a string representing integer decimal
readonly property alias cryptoValueToSend: d.cryptoValueRawToSend
@@ -45,6 +51,8 @@ ColumnLayout {
property var formatCurrencyAmount:
(amount, symbol, options = null, locale = null) => {}
+ property bool loading
+
signal reCalculateSuggestedRoute()
QtObject {
@@ -88,11 +96,11 @@ ColumnLayout {
}
readonly property string zeroString:
- LocaleUtils.numberToLocaleString(0, 2, LocaleUtils.userInputLocale)
+ LocaleUtils.numberToLocaleString(0, 2, topAmountToSendInput.locale)
readonly property double parsedInput:
LocaleUtils.numberFromLocaleString(topAmountToSendInput.text,
- LocaleUtils.userInputLocale)
+ topAmountToSendInput.locale)
readonly property double inputNumber:
root.inputNumberValid ? d.parsedInput : 0
@@ -108,10 +116,7 @@ ColumnLayout {
}
StatusBaseText {
- Layout.alignment: Qt.AlignLeft | Qt.AlignTop
-
- text: root.isBridgeTx ? qsTr("Amount to bridge")
- : qsTr("Amount to send")
+ text: root.caption
font.pixelSize: 13
lineHeight: 18
lineHeightMode: Text.FixedHeight
@@ -119,17 +124,16 @@ ColumnLayout {
}
RowLayout {
+ Layout.fillWidth: true
id: topItem
property double topAmountToSend: !inputIsFiat ? d.cryptoValueToSend
: d.fiatValueToSend
property string topAmountSymbol: !inputIsFiat ? d.selectedSymbol
: root.currentCurrency
- Layout.alignment: Qt.AlignLeft
-
AmountInputWithCursor {
id: topAmountToSendInput
- Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+ Layout.fillWidth: true
Layout.maximumWidth: 250
Layout.preferredWidth: !!text ? input.edit.paintedWidth + 2
: textMetrics.advanceWidth
@@ -138,31 +142,29 @@ ColumnLayout {
: Theme.palette.dangerColor1
input.edit.readOnly: !root.interactive
+ validationMode: StatusInput.ValidationMode.Always
validators: [
- StatusFloatValidator {
- id: floatValidator
- bottom: 0
- errorMessage: ""
- locale: topAmountToSendInput.locale
- },
StatusValidator {
errorMessage: ""
validate: (text) => {
- const num = parseFloat(text)
+ var num = 0
+ try {
+ num = Number.fromLocaleString(topAmountToSendInput.locale, text)
+ } catch (e) {
+ console.warn(e, "(Error parsing number from text: %1)".arg(text))
+ return false
+ }
- if (isNaN(num))
- return true
-
- return num <= root.maxInputBalance
- }
+ return num > 0 && num <= root.maxInputBalance
+ }
}
]
TextMetrics {
id: textMetrics
text: topAmountToSendInput.placeholderText
- font: topAmountToSendInput.input.placeholder.font
+ font: topAmountToSendInput.placeholderFont
}
Keys.onReleased: {
@@ -172,33 +174,35 @@ ColumnLayout {
if (!isNaN(amount))
d.waitTimer.restart()
}
+
+ visible: !root.loading
+ }
+ LoadingComponent {
+ objectName: "topAmountToSendInputLoadingComponent"
+ Layout.preferredWidth: topAmountToSendInput.width
+ Layout.preferredHeight: topAmountToSendInput.height
+ visible: root.loading
}
}
- Item {
+
+ StatusBaseText {
+ Layout.maximumWidth: parent.width
id: bottomItem
+ objectName: "bottomItemText"
- property double bottomAmountToSend: inputIsFiat ? d.cryptoValueToSend
- : d.fiatValueToSend
- property string bottomAmountSymbol: inputIsFiat ? d.selectedSymbol
- : currentCurrency
-
- Layout.alignment: Qt.AlignLeft | Qt.AlignBottom
- Layout.preferredWidth: txtBottom.width
- Layout.preferredHeight: txtBottom.height
-
- StatusBaseText {
- id: txtBottom
- anchors.top: parent.top
- anchors.left: parent.left
- text: root.formatCurrencyAmount(bottomItem.bottomAmountToSend,
- bottomItem.bottomAmountSymbol)
- font.pixelSize: 13
- color: Theme.palette.directColor5
- }
+ readonly property double bottomAmountToSend: inputIsFiat ? d.cryptoValueToSend
+ : d.fiatValueToSend
+ readonly property string bottomAmountSymbol: inputIsFiat ? d.selectedSymbol
+ : root.currentCurrency
+ elide: Text.ElideMiddle
+ text: root.formatCurrencyAmount(bottomAmountToSend, bottomAmountSymbol)
+ font.pixelSize: 13
+ color: Theme.palette.directColor5
MouseArea {
anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
+ cursorShape: enabled ? Qt.PointingHandCursor : undefined
+ enabled: root.fiatInputInteractive && !!root.selectedHolding
onClicked: {
topAmountToSendInput.validate()
@@ -207,12 +211,19 @@ ColumnLayout {
bottomItem.bottomAmountToSend,
bottomItem.bottomAmountSymbol,
{ noSymbol: true, rawAmount: true },
- LocaleUtils.userInputLocale)
+ topAmountToSendInput.locale)
}
- inputIsFiat = !inputIsFiat
+ root.inputIsFiat = !root.inputIsFiat
d.waitTimer.restart()
}
}
+ visible: !root.loading
+ }
+
+ LoadingComponent {
+ objectName: "bottomItemTextLoadingComponent"
+ Layout.preferredWidth: bottomItem.width
+ Layout.preferredHeight: bottomItem.height
+ visible: root.loading
}
}
-
diff --git a/ui/imports/shared/popups/send/views/TokenListView.qml b/ui/imports/shared/popups/send/views/TokenListView.qml
index 5082d3e38b..ca602f0db3 100644
--- a/ui/imports/shared/popups/send/views/TokenListView.qml
+++ b/ui/imports/shared/popups/send/views/TokenListView.qml
@@ -20,7 +20,6 @@ import "../controls"
Item {
id: root
- property string selectedSenderAccount
property var assets: null
property var collectibles: null
property var networksModel
@@ -221,7 +220,6 @@ Item {
TokenBalancePerChainDelegate {
width: tokenList.width
- selectedSenderAccount: root.selectedSenderAccount
balancesModel: LeftJoinModel {
leftModel: !!model & !!model.balances ? model.balances : null
rightModel: root.networksModel
@@ -230,7 +228,7 @@ Item {
onTokenSelected: function (selectedToken) {
root.tokenSelected(selectedToken.symbol, Constants.TokenType.ERC20)
}
- onTokenHovered: root.tokenHovered(symbol, Constants.TokenType.ERC20, hovered)
+ onTokenHovered: root.tokenHovered(selectedToken.symbol, Constants.TokenType.ERC20, hovered)
formatCurrentCurrencyAmount: function(balance){
return root.formatCurrentCurrencyAmount(balance)
}
@@ -253,13 +251,13 @@ Item {
id: collectiblesDelegate
CollectibleNestedDelegate {
width: tokenList.width
- onItemHovered: root.tokenHovered(selectedItem.uid, tokenType, hovered)
+ onItemHovered: root.tokenHovered(selectedItem.uid, Constants.TokenType.ERC721, hovered)
onItemSelected: {
if (isGroup) {
d.currentBrowsingGroupName = groupName
root.collectibles.currentGroupId = groupId
} else {
- root.tokenSelected(selectedItem.uid, tokenType)
+ root.tokenSelected(selectedItem.uid, Constants.TokenType.ERC721)
}
}
}
diff --git a/ui/imports/shared/stores/CurrenciesStore.qml b/ui/imports/shared/stores/CurrenciesStore.qml
index be5d1ca794..55bde4df3a 100644
--- a/ui/imports/shared/stores/CurrenciesStore.qml
+++ b/ui/imports/shared/stores/CurrenciesStore.qml
@@ -1,6 +1,7 @@
import QtQuick 2.15
import StatusQ.Core 0.1
+import StatusQ.Core.Utils 0.1 as SQUtils
import utils 1.0
import AppLayouts.Profile.stores 1.0
@@ -982,12 +983,18 @@ QtObject {
function formatCurrencyAmount(amount, symbol, options = null, locale = null) {
if (isNaN(amount)) {
- return "N/A"
+ return qsTr("N/A")
}
var currencyAmount = getCurrencyAmount(amount, symbol)
return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale)
}
+ function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
+ let bigIntBalance = SQUtils.AmountsArithmetic.fromString(balance)
+ let decimalBalance = SQUtils.AmountsArithmetic.toNumber(bigIntBalance, decimals)
+ return formatCurrencyAmount(decimalBalance, symbol)
+ }
+
function getFiatValue(cryptoAmount, cryptoSymbol) {
var amount = _profileSectionModuleInst.ensUsernamesModule.getFiatValue(cryptoAmount, cryptoSymbol)
return parseFloat(amount)
diff --git a/ui/imports/shared/stores/send/TransactionStore.qml b/ui/imports/shared/stores/send/TransactionStore.qml
index 7cc219ba32..cd8f1a26eb 100644
--- a/ui/imports/shared/stores/send/TransactionStore.qml
+++ b/ui/imports/shared/stores/send/TransactionStore.qml
@@ -14,7 +14,7 @@ import AppLayouts.Wallet.stores 1.0
QtObject {
id: root
- property CurrenciesStore currencyStore: CurrenciesStore {}
+ property CurrenciesStore currencyStore
property WalletAssetsStore walletAssetStore
property TokensStore tokensStore
@@ -276,10 +276,12 @@ QtObject {
submodelRoleName: "balances"
delegateModel: SortFilterProxyModel {
sourceModel: submodel
- filters: FastExpressionFilter {
- expression: root.selectedSenderAccount.address === model.account
- expectedRoles: ["account"]
- }
+ filters: [
+ ValueFilter {
+ roleName: "account"
+ value: root.selectedSenderAccount.address
+ }
+ ]
}
}
@@ -302,7 +304,7 @@ QtObject {
},
FastExpressionRole {
name: "currentBalance"
- expression: __getTotalBalance(model.balances, model.decimals, root.selectedSenderAccount)
+ expression: __getTotalBalance(model.balances, model.decimals)
expectedRoles: ["balances", "decimals"]
},
FastExpressionRole {
@@ -324,7 +326,7 @@ QtObject {
name.toUpperCase().startsWith(searchString.toUpperCase()) || __searchAddressInList(addressPerChain, searchString)
)
}
- expression: search(symbol, name, addressPerChain, assetSearchString)
+ expression: search(symbol, name, addressPerChain, root.assetSearchString)
expectedRoles: ["symbol", "name", "addressPerChain"]
},
ValueFilter {