Receive: implement payment request

This commit is contained in:
rating89us 2021-07-31 10:42:13 +02:00 committed by rating89us
parent 51828babbb
commit 946fa538b9
6 changed files with 334 additions and 7 deletions

View file

@ -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
}

View file

@ -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 = "?.??"

View file

@ -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() {

View file

@ -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;
}

View file

@ -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,

View file

@ -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,