/* * This file is part of Bitsquare. * * Bitsquare is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * Bitsquare is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>. */ package io.bitsquare.gui.main.portfolio.pendingtrades.steps.seller; import io.bitsquare.app.DevFlags; import io.bitsquare.common.util.Tuple3; import io.bitsquare.gui.components.BusyAnimation; import io.bitsquare.gui.components.TextFieldWithCopyIcon; import io.bitsquare.gui.components.TitledGroupBg; import io.bitsquare.gui.main.overlays.popups.Popup; import io.bitsquare.gui.main.portfolio.pendingtrades.PendingTradesViewModel; import io.bitsquare.gui.main.portfolio.pendingtrades.steps.TradeStepView; import io.bitsquare.gui.util.Layout; import io.bitsquare.locale.CurrencyUtil; import io.bitsquare.payment.*; import io.bitsquare.trade.Contract; import io.bitsquare.trade.Trade; import io.bitsquare.user.Preferences; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.layout.GridPane; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import java.util.Optional; import static io.bitsquare.gui.util.FormBuilder.*; public class SellerStep3View extends TradeStepView { private Button confirmButton; private Label statusLabel; private BusyAnimation busyAnimation; private Subscription tradeStatePropertySubscription; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// public SellerStep3View(PendingTradesViewModel model) { super(model); } @Override public void activate() { super.activate(); tradeStatePropertySubscription = EasyBind.subscribe(trade.stateProperty(), state -> { if (state == Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG) { PaymentAccountContractData paymentAccountContractData = model.dataModel.getSellersPaymentAccountContractData(); String key = "confirmPayment" + trade.getId(); String message; String tradeVolumeWithCode = model.formatter.formatVolumeWithCode(trade.getTradeVolume()); String currencyName = CurrencyUtil.getNameByCode(trade.getOffer().getCurrencyCode()); if (paymentAccountContractData instanceof CryptoCurrencyAccountContractData) { String address = ((CryptoCurrencyAccountContractData) paymentAccountContractData).getAddress(); message = "Your trading partner has confirmed that he initiated the " + currencyName + " payment.\n\n" + "Please check on your favorite " + currencyName + " blockchain explorer if the transaction to your receiving address\n" + "" + address + "\n" + "has already sufficient blockchain confirmations.\n" + "The payment amount has to be " + tradeVolumeWithCode + "\n\n" + "You can copy & paste your " + currencyName + " address from the main screen after " + "closing that popup."; } else { if (paymentAccountContractData instanceof USPostalMoneyOrderAccountContractData) message = "Your trading partner has confirmed that he initiated the " + currencyName + " payment.\n\n" + "Please check if you have received " + tradeVolumeWithCode + " with \"US Postal Money Order\" from the BTC buyer.\n\n" + "The trade ID (\"reason for payment\" text) of the transaction is: \"" + trade.getShortId() + "\""; else message = "Your trading partner has confirmed that he initiated the " + currencyName + " payment.\n\n" + "Please go to your online banking web page and check if you have received " + tradeVolumeWithCode + " from the BTC buyer.\n\n" + "The trade ID (\"reason for payment\" text) of the transaction is: \"" + trade.getShortId() + "\""; if (paymentAccountContractData instanceof CashDepositAccountContractData) message += "\n\nBecause the payment is done via Cash/ATM Deposit the BTC buyer has to write \"NO REFUND\" " + "on the paper receipt, tear it in 2 parts and send you a photo by email.\n\n" + "To avoid chargeback risk, only confirm if you received the email and if you are " + "sure the paper receipt is valid.\n" + "If you are not sure, please don't confirm but open a dispute " + "by entering \"cmd + o\" or \"ctrl + o\"."; Optional<String> optionalHolderName = getOptionalHolderName(); if (optionalHolderName.isPresent()) { message = message + "\n\n" + "Please also verify that the senders name in your bank statement matches that one from the " + "trade contract:\n" + "Senders name: " + optionalHolderName.get() + "\n\n" + "If the name is not the same as the one displayed here, please don't confirm but open a dispute " + "by entering \"cmd + o\" or \"ctrl + o\"."; } } if (!DevFlags.DEV_MODE && preferences.showAgain(key)) { preferences.dontShowAgain(key, true); new Popup().headLine("Attention required for trade with ID " + trade.getShortId()) .attention(message) .show(); } } else if (state == Trade.State.SELLER_CONFIRMED_FIAT_PAYMENT_RECEIPT && confirmButton.isDisabled()) { showStatusInfo(); } else if (state == Trade.State.SELLER_SENT_FIAT_PAYMENT_RECEIPT_MSG) { hideStatusInfo(); } }); } @Override public void deactivate() { super.deactivate(); if (tradeStatePropertySubscription != null) { tradeStatePropertySubscription.unsubscribe(); tradeStatePropertySubscription = null; } hideStatusInfo(); } /////////////////////////////////////////////////////////////////////////////////////////// // Content /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void addContent() { addTradeInfoBlock(); TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 3, "Confirm payment receipt", Layout.GROUP_DISTANCE); TextFieldWithCopyIcon field = addLabelTextFieldWithCopyIcon(gridPane, gridRow, "Amount to receive:", model.getFiatVolume(), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; field.setCopyWithoutCurrencyPostFix(true); String myPaymentDetails = ""; String peersPaymentDetails = ""; String myTitle = ""; String peersTitle = ""; boolean isBlockChain = false; String nameByCode = CurrencyUtil.getNameByCode(trade.getOffer().getCurrencyCode()); Contract contract = trade.getContract(); if (contract != null) { PaymentAccountContractData myPaymentAccountContractData = contract.getSellerPaymentAccountContractData(); PaymentAccountContractData peersPaymentAccountContractData = contract.getBuyerPaymentAccountContractData(); if (myPaymentAccountContractData instanceof CryptoCurrencyAccountContractData) { myPaymentDetails = ((CryptoCurrencyAccountContractData) myPaymentAccountContractData).getAddress(); peersPaymentDetails = ((CryptoCurrencyAccountContractData) peersPaymentAccountContractData).getAddress(); myTitle = "Your " + nameByCode + " address:"; peersTitle = "Buyers " + nameByCode + " address:"; isBlockChain = true; } else { myPaymentDetails = myPaymentAccountContractData.getPaymentDetails(); peersPaymentDetails = peersPaymentAccountContractData.getPaymentDetails(); myTitle = "Your trading account:"; peersTitle = "Buyers trading account:"; } } TextFieldWithCopyIcon myPaymentDetailsTextField = addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, myTitle, myPaymentDetails).second; myPaymentDetailsTextField.setMouseTransparent(false); myPaymentDetailsTextField.setTooltip(new Tooltip(myPaymentDetails)); TextFieldWithCopyIcon peersPaymentDetailsTextField = addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, peersTitle, peersPaymentDetails).second; peersPaymentDetailsTextField.setMouseTransparent(false); peersPaymentDetailsTextField.setTooltip(new Tooltip(peersPaymentDetails)); if (!isBlockChain) { addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "Reason for payment:", model.dataModel.getReference()); GridPane.setRowSpan(titledGroupBg, 4); } Tuple3<Button, BusyAnimation, Label> tuple = addButtonBusyAnimationLabelAfterGroup(gridPane, ++gridRow, "Confirm payment receipt"); confirmButton = tuple.first; confirmButton.setOnAction(e -> onPaymentReceived()); busyAnimation = tuple.second; statusLabel = tuple.third; hideStatusInfo(); } /////////////////////////////////////////////////////////////////////////////////////////// // Info /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getInfoText() { if (model.isBlockChainMethod()) { return "The BTC buyer has started the " + model.dataModel.getCurrencyCode() + " payment.\n" + "Check for blockchain confirmations at your altcoin wallet or block explorer and " + "confirm the payment when you have sufficient blockchain confirmations."; } else { return "The BTC buyer has started the " + model.dataModel.getCurrencyCode() + " payment.\n" + "Check at your trading account (e.g. bank account) and confirm when you have " + "received the payment."; } } /////////////////////////////////////////////////////////////////////////////////////////// // Warning /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getWarningText() { setWarningHeadline(); String substitute = model.isBlockChainMethod() ? "on the " + model.dataModel.getCurrencyCode() + "blockchain" : "at your payment provider (e.g. bank)"; return "You still have not confirmed the receipt of the payment!\n" + "Please check " + substitute + " if you have received the payment.\n" + "If you do not confirm receipt until " + model.getDateForOpenDispute() + " the trade will be investigated by the arbitrator."; } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getOpenForDisputeText() { return "You have not confirmed the receipt of the payment!\n" + "The max. period for the trade has elapsed.\n" + "Please contact the arbitrator for opening a dispute."; } @Override protected void applyOnDisputeOpened() { confirmButton.setDisable(true); } //////////////////////////////////////////////////////////////////////////////////////// // UI Handlers /////////////////////////////////////////////////////////////////////////////////////////// private void onPaymentReceived() { log.debug("onPaymentReceived"); if (model.p2PService.isBootstrapped()) { Preferences preferences = model.dataModel.preferences; String key = "confirmPaymentReceived"; if (!DevFlags.DEV_MODE && preferences.showAgain(key)) { PaymentAccountContractData paymentAccountContractData = model.dataModel.getSellersPaymentAccountContractData(); String message = "Have you received the " + CurrencyUtil.getNameByCode(model.dataModel.getCurrencyCode()) + " payment from your trading partner?\n\n"; if (!(paymentAccountContractData instanceof CryptoCurrencyAccountContractData)) { message += "The trade ID (\"reason for payment\" text) of the transaction is: \"" + trade.getShortId() + "\"\n\n"; Optional<String> optionalHolderName = getOptionalHolderName(); if (optionalHolderName.isPresent()) { message += "Please also verify that the senders name in your bank statement matches that one from the trade contract:\n" + "Senders name: " + optionalHolderName.get() + "\n\n" + "If the name is not the same as the one displayed here, please don't confirm but open a " + "dispute by entering \"cmd + o\" or \"ctrl + o\".\n\n"; } } message += "Please note, that as soon you have confirmed the receipt, the locked trade amount will be released " + "to the BTC buyer and the security deposit will be refunded."; new Popup() .headLine("Confirm that you have received the payment") .confirmation(message) .width(700) .actionButtonText("Yes, I have received the payment") .onAction(this::confirmPaymentReceived) .closeButtonText("Cancel") .show(); } else { confirmPaymentReceived(); } } else { new Popup().information("You need to wait until you are fully connected to the network.\n" + "That might take up to about 2 minutes at startup.").show(); } } private void confirmPaymentReceived() { confirmButton.setDisable(true); showStatusInfo(); model.dataModel.onFiatPaymentReceived(() -> { // In case the first send failed we got the support button displayed. // If it succeeds at a second try we remove the support button again. //TODO check for support. in case of a dispute we dont want to hide the button //if (notificationGroup != null) // notificationGroup.setButtonVisible(false); }, errorMessage -> { confirmButton.setDisable(false); hideStatusInfo(); new Popup().warning("Sending message to your trading partner failed.\n" + "Please try again and if it continue to fail report a bug.").show(); }); } private void showStatusInfo() { busyAnimation.play(); statusLabel.setText("Sending confirmation..."); } private void hideStatusInfo() { busyAnimation.stop(); statusLabel.setText(""); } private Optional<String> getOptionalHolderName() { Contract contract = trade.getContract(); if (contract != null) { PaymentAccountContractData paymentAccountContractData = contract.getBuyerPaymentAccountContractData(); if (paymentAccountContractData instanceof BankAccountContractData) return Optional.of(((BankAccountContractData) paymentAccountContractData).getHolderName()); else if (paymentAccountContractData instanceof SepaAccountContractData) return Optional.of(((SepaAccountContractData) paymentAccountContractData).getHolderName()); else return Optional.empty(); } else { return Optional.empty(); } } }