diff --git a/storybook/pages/SendRecipientInputPage.qml b/storybook/pages/SendRecipientInputPage.qml
new file mode 100644
index 0000000000..215f7900d0
--- /dev/null
+++ b/storybook/pages/SendRecipientInputPage.qml
@@ -0,0 +1,76 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import StatusQ.Core.Theme 0.1
+import StatusQ.Controls 0.1
+
+import Storybook 1.0
+
+import utils 1.0
+
+import shared.popups.send.controls 1.0
+
+SplitView {
+ id: root
+
+ Logs { id: logs }
+
+ SplitView {
+ orientation: Qt.Vertical
+ SplitView.fillWidth: true
+
+ Rectangle {
+ SplitView.fillHeight: true
+ SplitView.fillWidth: true
+ color: Theme.palette.baseColor2
+
+ SendRecipientInput {
+ anchors.centerIn: parent
+ interactive: ctrlInteractive.checked
+ checkMarkVisible: ctrlCheckmark.checked
+ Component.onCompleted: forceActiveFocus()
+
+ onClearClicked: logs.logEvent("SendRecipientInput::clearClicked", [], arguments)
+ onValidateInputRequested: logs.logEvent("SendRecipientInput::validateInputRequested", [], arguments)
+ }
+ }
+
+ LogsAndControlsPanel {
+ id: logsAndControlsPanel
+
+ SplitView.minimumHeight: 100
+ SplitView.preferredHeight: 200
+
+ logsView.logText: logs.logText
+
+ ColumnLayout {
+ TextEdit {
+ readOnly: true
+ selectByMouse: true
+ text: "valid address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4"
+ }
+
+ Switch {
+ id: ctrlInteractive
+ text: "Interactive"
+ checked: true
+ }
+
+ Switch {
+ id: ctrlCheckmark
+ text: "Checkmark visible"
+ checked: false
+ }
+ }
+ }
+ }
+}
+
+// category: Controls
+
+// https://www.figma.com/design/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=9707-106469&t=MeyLezc91kfFYcm9-0
+// https://www.figma.com/design/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=10259-120493&t=MeyLezc91kfFYcm9-0
+// https://www.figma.com/design/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=9019-88679&t=MeyLezc91kfFYcm9-0
+// https://www.figma.com/design/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=9707-105782&t=MeyLezc91kfFYcm9-0
+
diff --git a/storybook/qmlTests/tests/tst_SendRecipientInput.qml b/storybook/qmlTests/tests/tst_SendRecipientInput.qml
new file mode 100644
index 0000000000..2c6548b093
--- /dev/null
+++ b/storybook/qmlTests/tests/tst_SendRecipientInput.qml
@@ -0,0 +1,183 @@
+import QtQuick 2.15
+import QtTest 1.15
+
+import StatusQ 0.1
+import StatusQ.Core.Utils 0.1 as StatusQUtils
+
+import shared.popups.send.controls 1.0
+
+Item {
+ id: root
+ width: 600
+ height: 400
+
+ Component {
+ id: componentUnderTest
+ SendRecipientInput {
+ anchors.centerIn: parent
+ focus: true
+ }
+ }
+
+ SignalSpy {
+ id: signalSpyClearClicked
+ target: controlUnderTest
+ signalName: "clearClicked"
+ }
+
+ SignalSpy {
+ id: signalSpyValidateInputRequested
+ target: controlUnderTest
+ signalName: "validateInputRequested"
+ }
+
+ property SendRecipientInput controlUnderTest: null
+
+ TestCase {
+ name: "SendRecipientInput"
+ when: windowShown
+
+ function init() {
+ controlUnderTest = createTemporaryObject(componentUnderTest, root)
+ signalSpyClearClicked.clear()
+ signalSpyValidateInputRequested.clear()
+ QClipboardProxy.clear()
+ }
+
+ function test_basicGeometry() {
+ verify(!!controlUnderTest)
+ verify(controlUnderTest.width > 0)
+ verify(controlUnderTest.height > 0)
+ }
+
+ function test_textInput() {
+ verify(!!controlUnderTest)
+
+ verify(controlUnderTest.input.edit.focus)
+ verify(controlUnderTest.interactive)
+ compare(controlUnderTest.placeholderText, qsTr("Enter an ENS name or address"))
+
+ keyClick(Qt.Key_0)
+ keyClick(Qt.Key_X)
+ keyClick(Qt.Key_D)
+ keyClick(Qt.Key_E)
+ keyClick(Qt.Key_A)
+ keyClick(Qt.Key_D)
+ keyClick(Qt.Key_B)
+ keyClick(Qt.Key_E)
+ keyClick(Qt.Key_E)
+ keyClick(Qt.Key_F)
+
+ const plainText = StatusQUtils.StringUtils.plainText(controlUnderTest.text)
+
+ // input's text should be what we typed,
+ compare(plainText, "0xdeadbeef")
+ // ... and for each letter pressed the signal `validateInputRequested` should be emitted
+ compare(signalSpyValidateInputRequested.count, plainText.length)
+ }
+
+ function test_interactive() {
+ verify(!!controlUnderTest)
+ verify(controlUnderTest.input.edit.focus)
+ verify(controlUnderTest.interactive)
+
+ controlUnderTest.interactive = false
+
+ keyClick(Qt.Key_A)
+ keyClick(Qt.Key_B)
+ keyClick(Qt.Key_C)
+
+ const plainText = StatusQUtils.StringUtils.plainText(controlUnderTest.text)
+
+ // when non-interactive, any text input should be ignored
+ compare(plainText, "")
+ }
+
+ function test_pasteButton() {
+ verify(!!controlUnderTest)
+ const pasteButton = findChild(controlUnderTest, "pasteButton")
+ verify(!!pasteButton)
+ compare(pasteButton.visible, false)
+
+ // copy sth to clipboard
+ controlUnderTest.text = "0xdeadbeef"
+ controlUnderTest.input.edit.selectAll()
+ controlUnderTest.input.edit.copy()
+
+ // paste button should be visible if input is empty and clipboard is not
+ compare(pasteButton.visible, false)
+ controlUnderTest.input.edit.clear()
+ compare(pasteButton.visible, true)
+ }
+
+ function test_pasteUsingButton() {
+ verify(!!controlUnderTest)
+ const pasteButton = findChild(controlUnderTest, "pasteButton")
+ verify(!!pasteButton)
+ tryCompare(pasteButton, "visible", false)
+
+ const richTextToTest = "this is bold text"
+
+ // copy rich text to clipboard
+ controlUnderTest.text = richTextToTest
+ controlUnderTest.input.edit.selectAll()
+ controlUnderTest.input.edit.copy()
+ controlUnderTest.input.edit.clear()
+ compare(controlUnderTest.input.edit.length , 0)
+
+ compare(pasteButton.visible, true)
+ mouseClick(pasteButton)
+ verify(!controlUnderTest.text.includes("this is bold text"))
+ }
+
+ function test_pasteUsingKbdShortcut() {
+ verify(!!controlUnderTest)
+ const pasteButton = findChild(controlUnderTest, "pasteButton")
+ verify(!!pasteButton)
+ tryCompare(pasteButton, "visible", false)
+
+ const richTextToTest = "this is bold text"
+
+ // copy rich text to clipboard
+ controlUnderTest.text = richTextToTest
+ controlUnderTest.input.edit.selectAll()
+ controlUnderTest.input.edit.copy()
+ controlUnderTest.input.edit.clear()
+ compare(controlUnderTest.input.edit.length , 0)
+
+ compare(pasteButton.visible, true)
+ keySequence(StandardKey.Paste)
+ verify(!controlUnderTest.text.includes("this is bold text"))
+ }
+
+ function test_clearButton() {
+ verify(!!controlUnderTest)
+ verify(controlUnderTest.input.edit.focus)
+ verify(controlUnderTest.interactive)
+
+ const clearButton = findChild(controlUnderTest, "clearButton")
+ compare(clearButton.visible, false)
+
+ controlUnderTest.text = "0xdeadbeef"
+
+ // clear button should be visible with some text
+ verify(clearButton.visible)
+ mouseClick(clearButton)
+ compare(StatusQUtils.StringUtils.plainText(controlUnderTest.text), "")
+ compare(signalSpyClearClicked.count, 1)
+
+ // and when interactive
+ controlUnderTest.interactive = false
+ compare(clearButton.visible, false)
+ }
+
+ function test_checkmark() {
+ verify(!!controlUnderTest)
+ compare(controlUnderTest.checkMarkVisible, false)
+ controlUnderTest.checkMarkVisible = true
+ const checkmarkIcon = findChild(controlUnderTest, "checkmarkIcon")
+ verify(!!checkmarkIcon)
+ verify(checkmarkIcon.visible)
+ }
+ }
+}
diff --git a/storybook/stubs/shared/stores/send/TransactionStore.qml b/storybook/stubs/shared/stores/send/TransactionStore.qml
index a0d9b11536..70fdfe0e76 100644
--- a/storybook/stubs/shared/stores/send/TransactionStore.qml
+++ b/storybook/stubs/shared/stores/send/TransactionStore.qml
@@ -71,8 +71,9 @@ QtObject {
return textAddrss
}
- function resolveENS(value) {
- return root.mainModuleInst.resolvedENS("", "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4", "") // return some valid address
+ function resolveENS(value: string) {
+ if (!!value && value.endsWith(".eth"))
+ root.mainModuleInst.resolvedENS("", "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4", "") // return some valid address
}
function getAsset(assetsList, symbol) {
diff --git a/ui/StatusQ/include/StatusQ/QClipboardProxy.h b/ui/StatusQ/include/StatusQ/QClipboardProxy.h
index cb530308b8..f95592dc8c 100644
--- a/ui/StatusQ/include/StatusQ/QClipboardProxy.h
+++ b/ui/StatusQ/include/StatusQ/QClipboardProxy.h
@@ -55,6 +55,7 @@ public:
Q_INVOKABLE bool isValidImageUrl(const QUrl &url, const QStringList &acceptedExtensions) const;
Q_INVOKABLE qint64 getFileSize(const QUrl &url) const;
Q_INVOKABLE void copyTextToClipboard(const QString& text);
+ Q_INVOKABLE void clear();
signals:
void contentChanged();
diff --git a/ui/StatusQ/src/QClipboardProxy.cpp b/ui/StatusQ/src/QClipboardProxy.cpp
index fa4212b572..ab95751265 100644
--- a/ui/StatusQ/src/QClipboardProxy.cpp
+++ b/ui/StatusQ/src/QClipboardProxy.cpp
@@ -92,3 +92,8 @@ void QClipboardProxy::copyTextToClipboard(const QString &text)
{
m_clipboard->setText(text);
}
+
+void QClipboardProxy::clear()
+{
+ m_clipboard->clear();
+}
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml b/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml
index 15442dd2a1..8179c0b5f1 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml
@@ -10,6 +10,6 @@ StatusFlatRoundButton {
icon.height: 16
implicitWidth: 24
implicitHeight: 24
- icon.color: Theme.palette.baseColor1
+ icon.color: Theme.palette.directColor9
backgroundHoverColor: "transparent"
}
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml b/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml
index 52a2127c47..5cbfe440f5 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusInput.qml
@@ -455,7 +455,7 @@ Item {
onKeyPressed: {
root.keyPressed(event);
}
- onEditChanged: {
+ onEditClicked: {
root.editClicked();
}
onEditingFinished: {
diff --git a/ui/imports/shared/popups/send/controls/SendRecipientInput.qml b/ui/imports/shared/popups/send/controls/SendRecipientInput.qml
new file mode 100644
index 0000000000..b7e0225a78
--- /dev/null
+++ b/ui/imports/shared/popups/send/controls/SendRecipientInput.qml
@@ -0,0 +1,76 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import StatusQ 0.1
+import StatusQ.Controls 0.1
+import StatusQ.Core 0.1
+import StatusQ.Core.Theme 0.1
+
+StatusInput {
+ id: root
+
+ property bool interactive: true
+ property bool checkMarkVisible
+
+ signal clearClicked()
+ signal validateInputRequested()
+
+ placeholderText: qsTr("Enter an ENS name or address")
+ input.background.color: Theme.palette.indirectColor1
+ input.background.border.width: 0
+ input.implicitHeight: 56
+ rightPadding: 12
+ input.clearable: false // custom button below
+ input.edit.readOnly: !root.interactive
+ multiline: false
+ input.edit.textFormat: TextEdit.RichText
+
+ input.rightComponent: RowLayout {
+ StatusButton {
+ objectName: "pasteButton"
+ font.weight: Font.Normal
+ borderColor: Theme.palette.primaryColor1
+ borderWidth: 1
+ size: StatusBaseButton.Size.Tiny
+ text: qsTr("Paste")
+ visible: root.input.edit.length === 0 && root.input.edit.canPaste
+ focusPolicy: Qt.NoFocus
+ onClicked: {
+ root.input.edit.forceActiveFocus()
+ root.text = QClipboardProxy.text // paste plain text
+ root.input.edit.cursorPosition = root.input.edit.length
+ root.validateInputRequested()
+ }
+ }
+ StatusIcon {
+ objectName: "checkmarkIcon"
+ Layout.preferredWidth: 16
+ Layout.preferredHeight: 16
+ icon: "tiny/checkmark"
+ color: Theme.palette.primaryColor1
+ visible: root.checkMarkVisible
+ }
+ StatusClearButton {
+ objectName: "clearButton"
+ visible: root.input.edit.length !== 0 && root.interactive
+ onClicked: {
+ root.input.edit.clear()
+ root.clearClicked()
+ }
+ }
+ }
+
+ Connections {
+ target: root.input
+ function onKeyPressed(event) {
+ if (event.matches(StandardKey.Paste)) {
+ event.accepted = true
+ root.text = QClipboardProxy.text // paste plain text
+ }
+ }
+ }
+
+ Keys.onTabPressed: event.accepted = true
+ Keys.onReleased: root.validateInputRequested()
+}
diff --git a/ui/imports/shared/popups/send/controls/qmldir b/ui/imports/shared/popups/send/controls/qmldir
index 669d7bd88a..48d32ecbbf 100644
--- a/ui/imports/shared/popups/send/controls/qmldir
+++ b/ui/imports/shared/popups/send/controls/qmldir
@@ -10,3 +10,4 @@ BalanceExceeded 1.0 BalanceExceeded.qml
CollectibleBackButtonWithInfo 1.0 CollectibleBackButtonWithInfo.qml
CollectibleNestedDelegate 1.0 CollectibleNestedDelegate.qml
HeaderTitleText 1.0 HeaderTitleText.qml
+SendRecipientInput 1.0 SendRecipientInput.qml
diff --git a/ui/imports/shared/popups/send/views/RecipientView.qml b/ui/imports/shared/popups/send/views/RecipientView.qml
index ba9e7a6f79..1d03d038c8 100644
--- a/ui/imports/shared/popups/send/views/RecipientView.qml
+++ b/ui/imports/shared/popups/send/views/RecipientView.qml
@@ -175,24 +175,14 @@ Loader {
Component {
id: addressRecipient
- StatusInput {
- id: recipientInput
+ SendRecipientInput {
width: parent.width
height: visible ? implicitHeight: 0
visible: !root.isBridgeTx && !!root.selectedAsset
-
- placeholderText: qsTr("Enter an ENS name or address")
- input.background.color: Theme.palette.indirectColor1
- input.background.border.width: 0
- input.implicitHeight: 56
- input.clearable: false // custom button below
- input.edit.readOnly: !root.interactive
- multiline: false
- input.edit.textFormat: TextEdit.RichText
text: root.addressText
function validateInput() {
- const plainText = store.plainText(recipientInput.text)
+ const plainText = store.plainText(text)
root.isLoading()
if (Utils.isValidEns(plainText)) {
d.isPending = true
@@ -202,38 +192,10 @@ Loader {
}
}
- input.rightComponent: RowLayout {
- StatusButton {
- font.weight: Font.Normal
- borderColor: Theme.palette.primaryColor1
- size: StatusBaseButton.Size.Tiny
- text: qsTr("Paste")
- visible: recipientInput.input.edit.length === 0 && recipientInput.input.edit.canPaste
- focusPolicy: Qt.NoFocus
- onClicked: {
- recipientInput.input.edit.forceActiveFocus()
- recipientInput.input.edit.paste()
- recipientInput.input.edit.cursorPosition = recipientInput.input.edit.length
- recipientInput.validateInput()
- }
- }
- StatusIcon {
- Layout.preferredWidth: 16
- Layout.preferredHeight: 16
- icon: "tiny/checkmark"
- color: Theme.palette.primaryColor1
- visible: root.ready
- }
- StatusClearButton {
- visible: recipientInput.input.edit.length !== 0 && root.interactive
- onClicked: {
- recipientInput.input.edit.clear()
- d.clearValues()
- }
- }
- }
- Keys.onTabPressed: event.accepted = true
- Keys.onReleased: recipientInput.validateInput()
+ interactive: root.interactive
+ checkMarkVisible: root.ready
+ onClearClicked: d.clearValues()
+ onValidateInputRequested: validateInput()
}
}