Merge pull request #7005 from jmacxx/sepa_qr_code

Add SEPA QR Code for buyer payment
This commit is contained in:
Alejandro García 2024-03-27 09:09:23 +00:00 committed by GitHub
commit ee987bef1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 115 additions and 47 deletions

View File

@ -101,6 +101,8 @@ shared.dontRemoveOffer=Don't remove offer
shared.editOffer=Edit offer
shared.duplicateOffer=Duplicate offer
shared.openLargeQRWindow=Open large QR code window
shared.showSepaQRCode=Show SEPA QR Code
shared.maximizedToProtectPrivacy=In order to protect your privacy, the QR Code will be shown maximized over the application window.
shared.chooseTradingAccount=Choose trading account
shared.faq=Visit FAQ page
shared.yesCancel=Yes, cancel

View File

@ -1,10 +1,15 @@
package bisq.desktop.components.paymentmethods;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipCheckBox;
import bisq.desktop.main.overlays.windows.QRCodeWindow;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.locale.Country;
import bisq.core.locale.CountryUtil;
import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.payment.CountryBasedPaymentAccount;
@ -14,9 +19,13 @@ import bisq.core.util.validation.InputValidator;
import org.apache.commons.lang3.StringUtils;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import de.jensd.fx.glyphs.materialdesignicons.utils.MaterialDesignIconFactory;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
@ -25,18 +34,50 @@ import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.geometry.Insets;
import javafx.util.StringConverter;
import java.util.List;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox;
@Slf4j
public abstract class GeneralSepaForm extends PaymentMethodForm {
static final String BIC = "BIC";
static final String IBAN = "IBAN";
public static int addFormForBuyer(GridPane gridPane, int gridRow, String amount,
String countryCode, String recipient, String bic, String iban) {
Button showQrCodeButton = new AutoTooltipButton(Res.get("shared.showSepaQRCode"),
MaterialDesignIconFactory.get().createIcon(MaterialDesignIcon.QRCODE, "2.0em"));
GridPane.setRowIndex(showQrCodeButton, gridRow);
GridPane.setColumnIndex(showQrCodeButton, 0);
gridPane.getChildren().add(showQrCodeButton);
GridPane.setMargin(showQrCodeButton, new Insets(66 + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0));
showQrCodeButton.setStyle("-fx-pref-height: 27; -fx-padding: 4 4 4 4;");
GridPane.setColumnIndex(showQrCodeButton, 1);
showQrCodeButton.setOnMouseClicked(e -> GUIUtil.showMaximizedToProtectPrivacyMessage(() ->
new QRCodeWindow(constructQRCodeString(bic, iban, recipient, amount))
.withoutText()
.setWindowDimensions(gridPane.getScene().getWindow().getWidth() * 1.05,
gridPane.getScene().getWindow().getHeight() * 1.05)
.show()));
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner"), recipient);
addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, BIC, bic);
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow,
Res.get("payment.bank.country"), CountryUtil.getNameAndCode(countryCode));
addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, IBAN, iban);
return gridRow;
}
GeneralSepaForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
}
@ -134,4 +175,14 @@ public abstract class GeneralSepaForm extends PaymentMethodForm {
abstract boolean isCountryAccepted(String countryCode);
protected abstract String getIban();
// see https://en.wikipedia.org/wiki/EPC_QR_code
private static String constructQRCodeString(String bic, String iban, String recipient, String amountCcy) {
String paymentBase = "BCD\n001\n1\nSCT\n" + bic + "\n" + recipient + "\n" + iban;
String[] amountSplit = amountCcy.split(" ");
if (amountSplit.length == 2) {
return paymentBase + "\n" + amountSplit[1] + amountSplit[0]; // ccy and amount combined, EPC_QR_code spec
}
return paymentBase;
}
}

View File

@ -45,25 +45,17 @@ import java.util.List;
import java.util.Optional;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
public class SepaForm extends GeneralSepaForm {
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) {
PaymentAccountPayload paymentAccountPayload, String amount) {
SepaAccountPayload sepaAccountPayload = (SepaAccountPayload) paymentAccountPayload;
final String title = Res.get("payment.account.owner");
final String value = sepaAccountPayload.getHolderName();
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, title, value);
addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1,
Res.get("payment.bank.country"),
CountryUtil.getNameAndCode(sepaAccountPayload.getCountryCode()));
// IBAN, BIC will not be translated
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, IBAN, sepaAccountPayload.getIban());
addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, BIC, sepaAccountPayload.getBic());
return gridRow;
return GeneralSepaForm.addFormForBuyer(gridPane, gridRow, amount,
sepaAccountPayload.getCountryCode(),
sepaAccountPayload.getHolderName(),
sepaAccountPayload.getBic(),
sepaAccountPayload.getIban());
}
private final SepaAccount sepaAccount;

View File

@ -45,25 +45,17 @@ import java.util.List;
import java.util.Optional;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
public class SepaInstantForm extends GeneralSepaForm {
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) {
PaymentAccountPayload paymentAccountPayload, String amount) {
SepaInstantAccountPayload sepaInstantAccountPayload = (SepaInstantAccountPayload) paymentAccountPayload;
final String title = Res.get("payment.account.owner");
final String value = sepaInstantAccountPayload.getHolderName();
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, title, value);
addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1,
Res.get("payment.bank.country"),
CountryUtil.getNameAndCode(sepaInstantAccountPayload.getCountryCode()));
// IBAN, BIC will not be translated
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, IBAN, sepaInstantAccountPayload.getIban());
addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, BIC, sepaInstantAccountPayload.getBic());
return gridRow;
return GeneralSepaForm.addFormForBuyer(gridPane, gridRow, amount,
sepaInstantAccountPayload.getCountryCode(),
sepaInstantAccountPayload.getHolderName(),
sepaInstantAccountPayload.getBic(),
sepaInstantAccountPayload.getIban());
}
private final SepaInstantAccount sepaInstantAccount;

View File

@ -27,32 +27,21 @@ import net.glxn.qrgen.image.ImageType;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import javafx.geometry.HPos;
import java.io.ByteArrayInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class QRCodeWindow extends Overlay<QRCodeWindow> {
private static final Logger log = LoggerFactory.getLogger(QRCodeWindow.class);
private final ImageView qrCodeImageView;
private ImageView qrCodeImageView;
private final String bitcoinAddressOrURI;
private boolean showTextRepresentation = true;
private double prefWidth = 500, prefHeight = 500;
public QRCodeWindow(String bitcoinAddressOrURI) {
this.bitcoinAddressOrURI = bitcoinAddressOrURI;
final byte[] imageBytes = QRCode
.from(bitcoinAddressOrURI)
.withSize(250, 250)
.to(ImageType.PNG)
.stream()
.toByteArray();
Image qrImage = new Image(new ByteArrayInputStream(imageBytes));
qrCodeImageView = new ImageView(qrImage);
type = Type.Information;
width = 468;
headLine(Res.get("qRCodeWindow.headline"));
message(Res.get("qRCodeWindow.msg"));
}
@ -60,8 +49,23 @@ public class QRCodeWindow extends Overlay<QRCodeWindow> {
@Override
public void show() {
createGridPane();
gridPane.setPrefWidth(prefWidth);
gridPane.setMinHeight(prefHeight);
addHeadLine();
Region spacer = new Region();
spacer.setMinHeight(prefHeight / 8);
gridPane.add(spacer, 0, ++rowIndex);
final byte[] imageBytes = QRCode
.from(bitcoinAddressOrURI)
.withSize((int) prefWidth / 2, (int) prefHeight / 2)
.to(ImageType.PNG)
.stream()
.toByteArray();
Image qrImage = new Image(new ByteArrayInputStream(imageBytes));
qrCodeImageView = new ImageView(qrImage);
GridPane.setRowIndex(qrCodeImageView, ++rowIndex);
GridPane.setColumnSpan(qrCodeImageView, 2);
GridPane.setHalignment(qrCodeImageView, HPos.CENTER);
@ -69,11 +73,24 @@ public class QRCodeWindow extends Overlay<QRCodeWindow> {
message = bitcoinAddressOrURI.replace("%20", " ").replace("?", "\n?").replace("&", "\n&");
setTruncatedMessage();
addMessage();
GridPane.setHalignment(messageLabel, HPos.CENTER);
if (showTextRepresentation) {
addMessage();
GridPane.setHalignment(messageLabel, HPos.CENTER);
}
addButtons();
applyStyles();
display();
}
public QRCodeWindow withoutText() {
showTextRepresentation = false;
return this;
}
public QRCodeWindow setWindowDimensions(double width, double height) {
this.prefWidth = width;
this.prefHeight = height;
return this;
}
}

View File

@ -283,10 +283,10 @@ public class BuyerStep2View extends TradeStepView {
gridRow = PerfectMoneyForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
break;
case PaymentMethod.SEPA_ID:
gridRow = SepaForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
gridRow = SepaForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload, model.getFiatVolume());
break;
case PaymentMethod.SEPA_INSTANT_ID:
gridRow = SepaInstantForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
gridRow = SepaInstantForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload, model.getFiatVolume());
break;
case PaymentMethod.FASTER_PAYMENTS_ID:
gridRow = FasterPaymentsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);

View File

@ -743,6 +743,20 @@ public class GUIUtil {
return t.cast(parent);
}
public static void showMaximizedToProtectPrivacyMessage(Runnable runnable) {
String msg = Res.get("shared.maximizedToProtectPrivacy");
String id = "shared.maximizedToProtectPrivacy";
if (preferences.showAgain(id)) {
new Popup().information(msg)
.onClose(runnable)
.useIUnderstandButton()
.show();
DontShowAgainLookup.dontShowAgain(id, true);
} else {
runnable.run();
}
}
public static void showTakeOfferFromUnsignedAccountWarning() {
String key = "confirmTakeOfferFromUnsignedAccount";
new Popup().warning(Res.get("payment.takeOfferFromUnsignedAccount.warning"))