Merge pull request #11041 from accumulator/qml_senddialog_enter_recipient
What changed, and why it matters
This commit adds a new 'Enter manually' button to Electrum's mobile-style QML send screen, letting users type a recipient address instead of scanning a QR code. It also makes sure the manual-entry popup is cleaned up when the send dialog closes and that the camera scanner restarts if the user cancels manual entry. There is no obvious security bug in the change itself; it is a user-interface feature.
No immediate action required. As a defensive review, verify that recipient.text is passed through the same sanitization/validation as QR-scanned input and that the manual entry dialog cannot be used to bypass BIP21/address/invoice validation already performed by invoiceParser/piResolver.
Security signals we found
New user input surface added (TextField for recipient)
Input is dispatched through existing invoiceParser/piResolver pipeline rather than custom parsing
Dialog lifecycle cleanup added to prevent dangling object/reference on close
No validation, sandboxing, or privilege changes visible in diff
Evidence from the diff
The patch introduces a manual recipient entry path in electrum/gui/qml/components/SendDialog.qml. A new FlatButton creates an ElDialog containing a TextField bound to root.dispatch(recipient.text), which feeds the existing invoice parsing/resolution pipeline. The dialog lifecycle is managed via a new _manualEntryDialog property: it is closed without transition in onAboutToHide, destroyed on onClosed, and restart() is guarded so QR scanning only resumes after cancellation. WalletMainView.qml adds a closed handler to an error dialog that calls restartSendDialog(). The change reuses existing parsing logic and does not alter validation, cryptography, or network behavior.
Changed components
electrum/gui/qml/components/SendDialog.qmlelectrum/gui/qml/components/WalletMainView.qmlInspect captured patch +104 / −0
### electrum/gui/qml/components/SendDialog.qml
@@ -13,6 +13,10 @@ ElDialog {
property var invoiceParser // type: InvoiceParser
property var piResolver // type: PIResolver
+ // the manual entry dialog, while open. It stays open until the entered text is
+ // accepted (which closes this dialog as well), or until the user cancels it.
+ property var _manualEntryDialog
+
signal txFound(data: string)
signal channelBackupFound(data: string)
@@ -26,6 +30,11 @@ ElDialog {
onAboutToHide: {
console.log('about to hide')
qrscan.stop()
+ if (_manualEntryDialog) {
+ // close without transition, so it is gone before this dialog gets destroyed
+ _manualEntryDialog.exit = null
+ _manualEntryDialog.close()
+ }
}
onTxFound: (data) => {
@@ -59,6 +68,8 @@ ElDialog {
onClosed: destroy()
function restart() {
+ if (_manualEntryDialog) // scanning resumes when the manual entry dialog is cancelled
+ return
qrscan.restart()
}
@@ -101,6 +112,23 @@ ElDialog {
DialogButtonContainer {
Layout.fillWidth: true
+ FlatButton {
+ Layout.fillWidth: true
+ Layout.preferredWidth: 1
+ enabled: !invoiceParser.busy && !piResolver.busy
+ icon.source: '../../icons/pen.png'
+ text: qsTr('Enter manually')
+ onClicked: {
+ qrscan.stop()
+ root._manualEntryDialog = manualEntryDialog.createObject(app)
+ root._manualEntryDialog.rejected.connect(function() {
+ root._manualEntryDialog = null
+ root.restart()
+ })
+ root._manualEntryDialog.open()
+ }
+ }
+
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
@@ -120,6 +148,79 @@ ElDialog {
id: bitcoin
}
+ Component {
+ id: manualEntryDialog
+ ElDialog {
+ id: dialog
+
+ title: qsTr('Enter recipient')
+ iconSource: Qt.resolvedUrl('../../icons/pen.png')
+
+ width: parent.width * 4/5
+ anchors.centerIn: parent
+ padding: 0
+ needsSystemBarPadding: false
+ focus: true
+ // resolving cannot be cancelled, so don't allow closing while busy
+ allowClose: !root.piResolver.busy
+
+ function submit() {
+ // release focus, so the keyboard stays hidden when the field is re-enabled
+ dialog.contentItem.forceActiveFocus()
+ Qt.inputMethod.hide()
+ root.dispatch(recipient.text)
+ }
+
+ ColumnLayout {
+ width: parent.width
+ spacing: 0
+
+ TextField {
+ id: recipient
+ Layout.fillWidth: true
+ Layout.leftMargin: constants.paddingXXLarge
+ Layout.rightMargin: constants.paddingXXLarge
+ Layout.bottomMargin: constants.paddingLarge
+ focus: true
+ enabled: !root.piResolver.busy
+ rightPadding: spinner.visible ? spinner.width + constants.paddingSmall : leftPadding
+ placeholderText: qsTr('Recipient')
+ inputMethodHints: Qt.ImhEmailCharactersOnly | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
+ EnterKey.type: Qt.EnterKeyDone
+ onAccepted: {
+ if (text.trim())
+ dialog.submit()
+ }
+
+ BusyIndicator {
+ id: spinner
+ anchors.right: parent.right
+ anchors.rightMargin: constants.paddingXSmall
+ anchors.verticalCenter: parent.verticalCenter
+ width: constants.iconSizeLarge
+ height: constants.iconSizeLarge
+ visible: root.piResolver.busy
+ running: visible
+ }
+ }
+
+ DialogButtonContainer {
+ Layout.fillWidth: true
+
+ FlatButton {
+ Layout.fillWidth: true
+ enabled: !root.piResolver.busy && recipient.text.trim()
+ icon.source: '../../icons/confirmed.png'
+ text: qsTr('Ok')
+ onClicked: dialog.submit()
+ }
+ }
+ }
+
+ onClosed: destroy()
+ }
+ }
+
// Keep the camera preview out of screenshots and screen recordings.
Binding {
target: AppController
### electrum/gui/qml/components/WalletMainView.qml
@@ -302,6 +302,9 @@ Item {
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: message
})
+ dialog.closed.connect(function() {
+ restartSendDialog()
+ })
dialog.open()
}
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.