diff --git a/js/TxUtils.js b/js/TxUtils.js
index 31f8553c..42a5d449 100644
--- a/js/TxUtils.js
+++ b/js/TxUtils.js
@@ -74,13 +74,29 @@ function isValidOpenAliasAddress(address) {
return true
}
-function makeQRCodeString(addr, amount) {
+function makeQRCodeString(addr, amount, txDescription, recipientName) {
var XMR_URI_SCHEME = "monero:"
var XMR_AMOUNT = "tx_amount"
+ var XMR_RECIPIENT_NAME = "recipient_name"
+ var XMR_TX_DESCRIPTION = "tx_description"
var qrCodeString =""
qrCodeString += (XMR_URI_SCHEME + addr)
if (amount !== undefined && amount !== ""){
qrCodeString += ("?" + XMR_AMOUNT + "=" + amount)
}
+ if (txDescription !== undefined && txDescription !== ""){
+ if (amount == ""){
+ qrCodeString += ("?" + XMR_TX_DESCRIPTION + "=" + encodeURI(txDescription))
+ } else {
+ qrCodeString += ("&" + XMR_TX_DESCRIPTION + "=" + encodeURI(txDescription))
+ }
+ }
+ if (recipientName !== undefined && recipientName !== ""){
+ if (amount == "" && txDescription == ""){
+ qrCodeString += ("?" + XMR_RECIPIENT_NAME + "=" + encodeURI(recipientName))
+ } else {
+ qrCodeString += ("&" + XMR_RECIPIENT_NAME + "=" + encodeURI(recipientName))
+ }
+ }
return qrCodeString
}
diff --git a/main.qml b/main.qml
index 7bde3d50..e736487e 100644
--- a/main.qml
+++ b/main.qml
@@ -1251,6 +1251,15 @@ ApplicationWindow {
return (amount * ticker).toFixed(2);
}
+ function fiatApiConvertToXMR(amount) {
+ const ticker = appWindow.fiatPrice;
+ if(ticker <= 0){
+ fiatApiError("Invalid ticker value: " + ticker);
+ return "?.??";
+ }
+ return (amount / ticker).toFixed(12);
+ }
+
function fiatApiUpdateBalance(balance){
// update balance card
var bFiat = "?.??"
diff --git a/pages/Receive.qml b/pages/Receive.qml
index 68ef5011..5282a106 100644
--- a/pages/Receive.qml
+++ b/pages/Receive.qml
@@ -50,6 +50,7 @@ Rectangle {
color: "transparent"
property var model
property alias receiveHeight: mainLayout.height
+ property var state: "Address"
function renameSubaddressLabel(_index){
inputDialog.labelText = qsTr("Set the label of the selected address:") + translationManager.emptyString;
@@ -60,6 +61,17 @@ Rectangle {
inputDialog.open(appWindow.currentWallet.getSubaddressLabel(appWindow.currentWallet.currentSubaddressAccount, _index))
}
+ function generateQRCodeString() {
+ if (pageReceive.state == "PaymentRequest") {
+ return TxUtils.makeQRCodeString(appWindow.current_address,
+ (amountToReceiveXMR.text != "" && parseFloat(amountToReceiveXMR.text) != 0 ? amountToReceiveXMR.text : ""),
+ (txDescriptionInput.text != "" ? txDescriptionInput.text : ""),
+ (receiverNameInput.text != "" ? receiverNameInput.text : ""));
+ } else {
+ return TxUtils.makeQRCodeString(appWindow.current_address);
+ }
+ }
+
Clipboard { id: clipboard }
/* main layout */
@@ -80,6 +92,26 @@ Rectangle {
spacing: 0
property int qrSize: 220
+ MoneroComponents.Navbar {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.bottomMargin: 10
+
+ MoneroComponents.NavbarItem {
+ active: state == "Address"
+ text: qsTr("Address") + translationManager.emptyString
+ onSelected: state = "Address"
+ }
+
+ MoneroComponents.NavbarItem {
+ active: state == "PaymentRequest"
+ text: qsTr("Payment request") + translationManager.emptyString
+ onSelected: {
+ state = "PaymentRequest";
+ qrCodeTextMouseArea.hoverEnabled = true;
+ }
+ }
+ }
+
Rectangle {
id: qrContainer
color: MoneroComponents.Style.blackTheme ? "white" : "transparent"
@@ -95,16 +127,19 @@ Rectangle {
anchors.margins: 1
smooth: false
fillMode: Image.PreserveAspectFit
- source: "image://qrcode/" + TxUtils.makeQRCodeString(appWindow.current_address)
+ source: "image://qrcode/" + generateQRCodeString();
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onEntered: qrCodeTooltip.tooltipPopup.open()
+ onExited: qrCodeTooltip.tooltipPopup.close()
onClicked: {
if (mouse.button == Qt.LeftButton){
- selectedAddressDetailsColumn.qrSize = selectedAddressDetailsColumn.qrSize == 220 ? 300 : 220;
+ walletManager.saveQrCodeToClipboard(generateQRCodeString());
+ appWindow.showStatusMessage(qsTr("QR code copied to clipboard") + translationManager.emptyString, 3);
} else if (mouse.button == Qt.RightButton){
qrMenu.x = this.mouseX;
qrMenu.y = this.mouseY;
@@ -118,11 +153,258 @@ Rectangle {
id: qrMenu
title: "QrCode"
+ MenuItem {
+ text: qsTr("Copy to clipboard") + translationManager.emptyString;
+ onTriggered: walletManager.saveQrCodeToClipboard(generateQRCodeString())
+ }
+
MenuItem {
text: qsTr("Save as Image") + translationManager.emptyString;
onTriggered: qrFileDialog.open()
}
}
+
+ MoneroComponents.Tooltip {
+ id: qrCodeTooltip
+ text: qsTr("Left click: copy QR code to clipboard") + "
" + qsTr("Right click: save QR code as image file") + translationManager.emptyString
+ }
+ }
+
+ MoneroComponents.TextPlain {
+ id: qrCodeText
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: 6
+ Layout.maximumWidth: 285
+ Layout.minimumHeight: 75
+ verticalAlignment: Text.AlignVCenter
+ visible: paymentRequestGridLayout.visible
+ font.pixelSize: 12
+ color: qrCodeTextMouseArea.containsMouse ? MoneroComponents.Style.orange : MoneroComponents.Style.defaultFontColor
+ text: generateQRCodeString();
+ wrapMode: Text.WrapAnywhere
+ tooltip: qsTr("Copy payment request to clipboard") + translationManager.emptyString
+ themeTransition: false
+
+ MouseArea {
+ id: qrCodeTextMouseArea
+ hoverEnabled: false //true when Payment request navbar button is clicked (fix bug displaying tooltip when navbar button is clicked)
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onEntered: parent.tooltipPopup.open()
+ onExited: parent.tooltipPopup.close()
+ onClicked: {
+ clipboard.setText(qrCodeText.text);
+ appWindow.showStatusMessage(qsTr("Payment request copied to clipboard") + translationManager.emptyString, 3);
+ }
+ }
+ }
+
+ GridLayout {
+ id: paymentRequestGridLayout
+ columns: 3
+ rows: 4
+ visible: pageReceive.state == "PaymentRequest"
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: 6
+ Layout.preferredWidth: 285
+ Layout.maximumWidth: 285
+
+ MoneroComponents.Label {
+ id: amountTitleFiat
+ Layout.bottomMargin: 3
+ Layout.preferredWidth: 90
+ visible: persistentSettings.fiatPriceEnabled
+ fontSize: 14
+ text: qsTr("Amount") + translationManager.emptyString
+ }
+
+ MoneroComponents.Input {
+ id: amountToReceiveFiat
+ Layout.preferredWidth: 165
+ Layout.maximumWidth: 165
+ visible: persistentSettings.fiatPriceEnabled
+ topPadding: 5
+ leftPadding: 5
+ font.family: MoneroComponents.Style.fontMonoRegular.name
+ font.pixelSize: 14
+ font.bold: false
+ horizontalAlignment: TextInput.AlignLeft
+ verticalAlignment: TextInput.AlignVCenter
+ selectByMouse: true
+ color: MoneroComponents.Style.defaultFontColor
+ placeholderText: "0.00"
+
+ background: Rectangle {
+ color: MoneroComponents.Style.blackTheme ? "transparent" : "white"
+ radius: 3
+ border.color: parent.activeFocus ? MoneroComponents.Style.inputBorderColorActive : MoneroComponents.Style.inputBorderColorInActive
+ border.width: 1
+ }
+ onTextEdited: {
+ text = text.trim().replace(",", ".");
+ const match = text.match(/^0+(\d.*)/);
+ if (match) {
+ const cursorPosition = cursorPosition;
+ text = match[1];
+ cursorPosition = Math.max(cursorPosition, 1) - 1;
+ } else if(text.indexOf('.') === 0){
+ text = '0' + text;
+ if (text.length > 2) {
+ cursorPosition = 1;
+ }
+ }
+ if (amountToReceiveFiat.text == "") {
+ amountToReceiveXMR.text = "";
+ } else {
+ amountToReceiveXMR.text = fiatApiConvertToXMR(amountToReceiveFiat.text);
+ }
+ }
+ validator: RegExpValidator {
+ regExp: /^\s*(\d{1,8})?([\.,]\d{1,2})?\s*$/
+ }
+ }
+
+ MoneroComponents.Label {
+ Layout.bottomMargin: 3
+ visible: persistentSettings.fiatPriceEnabled
+ fontSize: 14
+ text: appWindow.fiatApiCurrencySymbol();
+ }
+
+ MoneroComponents.Label {
+ id: amountTitleXMR
+ Layout.bottomMargin: 3
+ Layout.preferredWidth: 90
+ fontSize: 14
+ text: persistentSettings.fiatPriceEnabled ? "" : qsTr("Amount") + translationManager.emptyString
+ }
+
+ MoneroComponents.Input {
+ id: amountToReceiveXMR
+ Layout.preferredWidth: 165
+ Layout.maximumWidth: 165
+ topPadding: 5
+ leftPadding: 5
+ font.family: MoneroComponents.Style.fontMonoRegular.name
+ font.pixelSize: 14
+ font.bold: false
+ horizontalAlignment: TextInput.AlignLeft
+ verticalAlignment: TextInput.AlignVCenter
+ selectByMouse: true
+ color: MoneroComponents.Style.defaultFontColor
+ placeholderText: "0.000000000000"
+
+ background: Rectangle {
+ color: MoneroComponents.Style.blackTheme ? "transparent" : "white"
+ radius: 3
+ border.color: parent.activeFocus ? MoneroComponents.Style.inputBorderColorActive : MoneroComponents.Style.inputBorderColorInActive
+ border.width: 1
+ }
+ onTextEdited: {
+ text = text.trim().replace(",", ".");
+ const match = text.match(/^0+(\d.*)/);
+ if (match) {
+ const cursorPosition = cursorPosition;
+ text = match[1];
+ cursorPosition = Math.max(cursorPosition, 1) - 1;
+ } else if(text.indexOf('.') === 0){
+ text = '0' + text;
+ if (text.length > 2) {
+ cursorPosition = 1;
+ }
+ }
+ if (amountToReceiveXMR.text == "") {
+ amountToReceiveFiat.text = "";
+ } else {
+ amountToReceiveFiat.text = fiatApiConvertToFiat(amountToReceiveXMR.text);
+ }
+ }
+ validator: RegExpValidator {
+ regExp: /^\s*(\d{1,8})?([\.,]\d{1,12})?\s*$/
+ }
+ }
+
+ MoneroComponents.Label {
+ Layout.bottomMargin: 3
+ fontSize: 14
+ text: "XMR"
+ }
+
+ MoneroComponents.Label {
+ id: txDescription
+ Layout.bottomMargin: 3
+ Layout.preferredWidth: 90
+ fontSize: 14
+ text: qsTr("Description") + translationManager.emptyString
+ tooltip: qsTr("What is being payed for (a product, service, donation) (optional)") + translationManager.emptyString
+ tooltipIconVisible: true
+ }
+
+ MoneroComponents.Input {
+ id: txDescriptionInput
+ Layout.preferredWidth: 165
+ Layout.maximumWidth: 165
+ topPadding: 7
+ leftPadding: 7
+ font.pixelSize: 14
+ font.bold: false
+ horizontalAlignment: TextInput.AlignLeft
+ verticalAlignment: TextInput.AlignVCenter
+ selectByMouse: true
+ color: MoneroComponents.Style.defaultFontColor
+ placeholderText: qsTr("Visible to the sender") + translationManager.emptyString
+
+ background: Rectangle {
+ color: MoneroComponents.Style.blackTheme ? "transparent" : "white"
+ radius: 3
+ border.color: parent.activeFocus ? MoneroComponents.Style.inputBorderColorActive : MoneroComponents.Style.inputBorderColorInActive
+ border.width: 1
+ }
+ }
+
+ MoneroComponents.Label {
+ Layout.bottomMargin: 3
+ fontSize: 14
+ text: ""
+ }
+
+ MoneroComponents.Label {
+ id: receiverNameLabel
+ Layout.bottomMargin: 3
+ Layout.preferredWidth: 90
+ fontSize: 14
+ text: qsTr("Your name") + translationManager.emptyString
+ tooltip: qsTr("Your name, company or website (optional)") + translationManager.emptyString
+ tooltipIconVisible: true
+ }
+
+ MoneroComponents.Input {
+ id: receiverNameInput
+ Layout.preferredWidth: 165
+ Layout.maximumWidth: 165
+ topPadding: 7
+ leftPadding: 7
+ font.pixelSize: 14
+ font.bold: false
+ horizontalAlignment: TextInput.AlignLeft
+ verticalAlignment: TextInput.AlignVCenter
+ selectByMouse: true
+ color: MoneroComponents.Style.defaultFontColor
+ placeholderText: qsTr("Visible to the sender") + translationManager.emptyString
+
+ background: Rectangle {
+ color: MoneroComponents.Style.blackTheme ? "transparent" : "white"
+ radius: 3
+ border.color: parent.activeFocus ? MoneroComponents.Style.inputBorderColorActive : MoneroComponents.Style.inputBorderColorInActive
+ border.width: 1
+ }
+ }
+
+ MoneroComponents.Label {
+ Layout.bottomMargin: 3
+ fontSize: 14
+ text: ""
+ }
}
MoneroComponents.TextPlain {
@@ -131,6 +413,7 @@ Rectangle {
Layout.preferredWidth: 220
Layout.maximumWidth: 220
Layout.topMargin: 15
+ visible: pageReceive.state == "Address"
horizontalAlignment: Text.AlignHCenter
text: qsTr("Address #") + subaddressListView.currentIndex + translationManager.emptyString
wrapMode: Text.WordWrap
@@ -147,6 +430,7 @@ Rectangle {
Layout.preferredWidth: 220
Layout.maximumWidth: 220
Layout.topMargin: 10
+ visible: pageReceive.state == "Address"
horizontalAlignment: Text.AlignHCenter
text: "(" + qsTr("no label") + ")" + translationManager.emptyString
wrapMode: Text.WordWrap
@@ -175,6 +459,7 @@ Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: 300
Layout.topMargin: 11
+ visible: pageReceive.state == "Address"
text: appWindow.current_address ? appWindow.current_address : ""
horizontalAlignment: TextInput.AlignHCenter
wrapMode: Text.Wrap
@@ -464,12 +749,14 @@ Rectangle {
selectExisting: false
nameFilters: ["Image (*.png)"]
onAccepted: {
- if(!walletManager.saveQrCode(TxUtils.makeQRCodeString(appWindow.current_address), walletManager.urlToLocalPath(fileUrl))) {
+ if(!walletManager.saveQrCode(generateQRCodeString(), walletManager.urlToLocalPath(fileUrl))) {
console.log("Failed to save QrCode to file " + walletManager.urlToLocalPath(fileUrl) )
receivePageDialog.title = qsTr("Save QrCode") + translationManager.emptyString;
receivePageDialog.text = qsTr("Failed to save QrCode to ") + walletManager.urlToLocalPath(fileUrl) + translationManager.emptyString;
receivePageDialog.icon = StandardIcon.Error
receivePageDialog.open()
+ } else {
+ appWindow.showStatusMessage(qsTr("QR code saved to ") + walletManager.urlToLocalPath(fileUrl) + translationManager.emptyString, 3);
}
}
}
@@ -477,6 +764,7 @@ Rectangle {
function onPageCompleted() {
console.log("Receive page loaded");
+ pageReceive.clearFields();
subaddressListView.model = appWindow.currentWallet.subaddressModel;
if (appWindow.currentWallet) {
@@ -489,7 +777,10 @@ Rectangle {
}
function clearFields() {
- // @TODO: add fields
+ amountToReceiveFiat.text = "";
+ amountToReceiveXMR.text = "";
+ txDescriptionInput.text = "";
+ receiverNameInput.text = "";
}
function onPageClosed() {
diff --git a/pages/Transfer.qml b/pages/Transfer.qml
index 8646b52e..1b1a8260 100644
--- a/pages/Transfer.qml
+++ b/pages/Transfer.qml
@@ -102,7 +102,7 @@ Rectangle {
recipientModel.newRecipient(address, Utils.removeTrailingZeros(amount || ""));
setPaymentId(payment_id || "");
- setDescription((recipient_name ? recipient_name + " " : "") + (tx_description || ""));
+ setDescription((recipient_name ? recipient_name + (tx_description ? " (" + tx_description + ")" : "") : (tx_description || "")));
}
function updateFromQrCode(address, payment_id, amount, tx_description, recipient_name) {
@@ -404,7 +404,7 @@ Rectangle {
onTextChanged: {
const parsed = walletManager.parse_uri_to_object(text);
if (!parsed.error) {
- fillPaymentDetails(parsed.address, parsed.payment_id, parsed.amount, parsed.tx_description);
+ fillPaymentDetails(parsed.address, parsed.payment_id, parsed.amount, parsed.tx_description, parsed.recipient_name);
}
address = text;
}
diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp
index 6ecd6b6e..338c2f0c 100644
--- a/src/libwalletqt/WalletManager.cpp
+++ b/src/libwalletqt/WalletManager.cpp
@@ -31,6 +31,8 @@
#include "wallet/api/wallet2_api.h"
#include "zxcvbn-c/zxcvbn.h"
#include "QRCodeImageProvider.h"
+#include
+#include
#include
#include
#include
@@ -480,6 +482,14 @@ bool WalletManager::saveQrCode(const QString &code, const QString &path) const
return QRCodeImageProvider::genQrImage(code, &size).scaled(size.expandedTo(QSize(240, 240)), Qt::KeepAspectRatio).save(path, "PNG", 100);
}
+void WalletManager::saveQrCodeToClipboard(const QString &code) const
+{
+ QClipboard *clipboard = QGuiApplication::clipboard();
+ QSize size;
+ clipboard->setImage(QRCodeImageProvider::genQrImage(code, &size).scaled(size.expandedTo(QSize(240, 240)), Qt::KeepAspectRatio), QClipboard::Clipboard);
+ clipboard->setImage(QRCodeImageProvider::genQrImage(code, &size).scaled(size.expandedTo(QSize(240, 240)), Qt::KeepAspectRatio), QClipboard::Selection);
+}
+
void WalletManager::checkUpdatesAsync(
const QString &software,
const QString &subdir,
diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h
index 3092a5f3..2b424b73 100644
--- a/src/libwalletqt/WalletManager.h
+++ b/src/libwalletqt/WalletManager.h
@@ -180,6 +180,7 @@ public:
Q_INVOKABLE bool parse_uri(const QString &uri, QString &address, QString &payment_id, uint64_t &amount, QString &tx_description, QString &recipient_name, QVector &unknown_parameters, QString &error) const;
Q_INVOKABLE QVariantMap parse_uri_to_object(const QString &uri) const;
Q_INVOKABLE bool saveQrCode(const QString &, const QString &) const;
+ Q_INVOKABLE void saveQrCodeToClipboard(const QString &) const;
Q_INVOKABLE void checkUpdatesAsync(
const QString &software,
const QString &subdir,