mirror of
https://github.com/monero-project/monero-gui.git
synced 2024-11-16 15:58:11 +00:00
Receive: implement payment request
This commit is contained in:
parent
51828babbb
commit
946fa538b9
6 changed files with 334 additions and 7 deletions
|
@ -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
|
||||
}
|
||||
|
|
9
main.qml
9
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 = "?.??"
|
||||
|
|
|
@ -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") + "<br>" + 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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
#include "wallet/api/wallet2_api.h"
|
||||
#include "zxcvbn-c/zxcvbn.h"
|
||||
#include "QRCodeImageProvider.h"
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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<QString> &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,
|
||||
|
|
Loading…
Reference in a new issue