/* * 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.overlays.windows; import com.google.common.base.Joiner; import io.bitsquare.btc.FeePolicy; import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.util.Tuple3; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.components.BusyAnimation; import io.bitsquare.gui.main.MainView; import io.bitsquare.gui.main.account.AccountView; import io.bitsquare.gui.main.account.content.arbitratorselection.ArbitratorSelectionView; import io.bitsquare.gui.main.account.settings.AccountSettingsView; import io.bitsquare.gui.main.overlays.Overlay; import io.bitsquare.gui.main.overlays.popups.Popup; import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.Layout; import io.bitsquare.locale.BSResources; import io.bitsquare.locale.BankUtil; import io.bitsquare.locale.CountryUtil; import io.bitsquare.payment.PaymentAccount; import io.bitsquare.payment.PaymentMethod; import io.bitsquare.trade.offer.Offer; import io.bitsquare.user.Preferences; import io.bitsquare.user.User; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import org.bitcoinj.core.Coin; import org.bitcoinj.utils.Fiat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.util.List; import java.util.Optional; import static io.bitsquare.gui.util.FormBuilder.*; public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> { protected static final Logger log = LoggerFactory.getLogger(OfferDetailsWindow.class); private final BSFormatter formatter; protected final Preferences preferences; private final User user; private final KeyRing keyRing; private final Navigation navigation; private Offer offer; private Coin tradeAmount; private Fiat tradePrice; private Optional<Runnable> placeOfferHandlerOptional = Optional.empty(); private Optional<Runnable> takeOfferHandlerOptional = Optional.empty(); private BusyAnimation busyAnimation; /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// @Inject public OfferDetailsWindow(BSFormatter formatter, Preferences preferences, User user, KeyRing keyRing, Navigation navigation) { this.formatter = formatter; this.preferences = preferences; this.user = user; this.keyRing = keyRing; this.navigation = navigation; type = Type.Confirmation; } public void show(Offer offer, Coin tradeAmount, Fiat tradePrice) { this.offer = offer; this.tradeAmount = tradeAmount; this.tradePrice = tradePrice; rowIndex = -1; width = 950; createGridPane(); addContent(); display(); } public void show(Offer offer) { this.offer = offer; rowIndex = -1; width = 950; createGridPane(); addContent(); display(); } public OfferDetailsWindow onPlaceOffer(Runnable placeOfferHandler) { this.placeOfferHandlerOptional = Optional.of(placeOfferHandler); return this; } public OfferDetailsWindow onTakeOffer(Runnable takeOfferHandler) { this.takeOfferHandlerOptional = Optional.of(takeOfferHandler); return this; } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void onHidden() { if (busyAnimation != null) busyAnimation.stop(); } @Override protected void createGridPane() { super.createGridPane(); gridPane.setPadding(new Insets(35, 40, 30, 40)); gridPane.setStyle("-fx-background-color: -bs-content-bg-grey;" + "-fx-background-radius: 5 5 5 5;" + "-fx-effect: dropshadow(gaussian, #999, 10, 0, 0, 0);" + "-fx-background-insets: 10;" ); } private void addContent() { int rows = 5; List<String> acceptedBanks = offer.getAcceptedBankIds(); boolean showAcceptedBanks = acceptedBanks != null && !acceptedBanks.isEmpty(); List<String> acceptedCountryCodes = offer.getAcceptedCountryCodes(); boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty(); if (!takeOfferHandlerOptional.isPresent()) rows++; if (showAcceptedBanks) rows++; if (showAcceptedCountryCodes) rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, "Offer"); String fiatDirectionInfo = ":"; String btcDirectionInfo = ":"; Offer.Direction direction = offer.getDirection(); String currencyCode = offer.getCurrencyCode(); if (takeOfferHandlerOptional.isPresent()) { addLabelTextField(gridPane, rowIndex, "Offer type:", formatter.getDirectionForTakeOffer(direction, currencyCode), Layout.FIRST_ROW_DISTANCE); fiatDirectionInfo = direction == Offer.Direction.BUY ? " to receive:" : " to spend:"; btcDirectionInfo = direction == Offer.Direction.SELL ? " to receive:" : " to spend:"; } else if (placeOfferHandlerOptional.isPresent()) { addLabelTextField(gridPane, rowIndex, "Offer type:", formatter.getOfferDirectionForCreateOffer(direction, currencyCode), Layout.FIRST_ROW_DISTANCE); fiatDirectionInfo = direction == Offer.Direction.SELL ? " to receive:" : " to spend:"; btcDirectionInfo = direction == Offer.Direction.BUY ? " to receive:" : " to spend:"; } else { addLabelTextField(gridPane, rowIndex, "Offer type:", formatter.getDirectionBothSides(direction, currencyCode), Layout.FIRST_ROW_DISTANCE); } if (takeOfferHandlerOptional.isPresent()) { addLabelTextField(gridPane, ++rowIndex, "Bitcoin amount" + btcDirectionInfo, formatter.formatCoinWithCode(tradeAmount)); addLabelTextField(gridPane, ++rowIndex, formatter.formatVolumeLabel(currencyCode) + fiatDirectionInfo, formatter.formatVolumeWithCode(offer.getVolumeByAmount(tradeAmount))); } else { addLabelTextField(gridPane, ++rowIndex, "Bitcoin amount" + btcDirectionInfo, formatter.formatCoinWithCode(offer.getAmount())); addLabelTextField(gridPane, ++rowIndex, "Min. bitcoin amount:", formatter.formatCoinWithCode(offer.getMinAmount())); String volume = formatter.formatVolumeWithCode(offer.getOfferVolume()); String minVolume = ""; if (!offer.getAmount().equals(offer.getMinAmount())) minVolume = " (min. " + formatter.formatVolumeWithCode(offer.getMinOfferVolume()) + ")"; addLabelTextField(gridPane, ++rowIndex, formatter.formatVolumeLabel(currencyCode) + fiatDirectionInfo, volume + minVolume); } if (takeOfferHandlerOptional.isPresent()) { addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatPrice(tradePrice)); } else { Fiat price = offer.getPrice(); if (offer.getUseMarketBasedPrice()) { addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatPrice(price) + " (distance from market price: " + formatter.formatPercentagePrice(offer.getMarketPriceMargin()) + ")"); } else { addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatPrice(price)); } } final PaymentMethod paymentMethod = offer.getPaymentMethod(); final String offererPaymentAccountId = offer.getOffererPaymentAccountId(); final PaymentAccount paymentAccount = user.getPaymentAccount(offererPaymentAccountId); String bankId = offer.getBankId(); if (bankId == null || bankId.equals("null")) bankId = ""; else bankId = " (" + bankId + ")"; final boolean isSpecificBanks = paymentMethod.equals(PaymentMethod.SPECIFIC_BANKS); final boolean isNationalBanks = paymentMethod.equals(PaymentMethod.NATIONAL_BANK); final boolean isSepa = paymentMethod.equals(PaymentMethod.SEPA); if (offer.isMyOffer(keyRing) && offererPaymentAccountId != null && paymentAccount != null) { addLabelTextField(gridPane, ++rowIndex, "My trading account:", paymentAccount.getAccountName()); } else { final String method = BSResources.get(paymentMethod.getId()); if (isNationalBanks || isSpecificBanks || isSepa) { if (BankUtil.isBankIdRequired(offer.getCountryCode())) addLabelTextField(gridPane, ++rowIndex, "Payment method (offerer's bank ID):", method + bankId); else if (BankUtil.isBankNameRequired(offer.getCountryCode())) addLabelTextField(gridPane, ++rowIndex, "Payment method (offerer's bank name):", method + bankId); } else { addLabelTextField(gridPane, ++rowIndex, "Payment method:", method); } } if (showAcceptedBanks) { if (paymentMethod.equals(PaymentMethod.SAME_BANK)) { addLabelTextField(gridPane, ++rowIndex, "Bank ID (e.g. BIC or SWIFT):", acceptedBanks.get(0)); } else if (isSpecificBanks) { String value = Joiner.on(", ").join(acceptedBanks); Tooltip tooltip = new Tooltip("Accepted banks: " + value); TextField acceptedBanksTextField = addLabelTextField(gridPane, ++rowIndex, "Accepted banks:", value).second; acceptedBanksTextField.setMouseTransparent(false); acceptedBanksTextField.setTooltip(tooltip); } } if (showAcceptedCountryCodes) { String countries; Tooltip tooltip = null; if (CountryUtil.containsAllSepaEuroCountries(acceptedCountryCodes)) { countries = "All Euro countries"; } else { if (acceptedCountryCodes.size() == 1) { countries = CountryUtil.getNameAndCode(acceptedCountryCodes.get(0)); tooltip = new Tooltip(countries); } else { countries = CountryUtil.getCodesString(acceptedCountryCodes); tooltip = new Tooltip(CountryUtil.getNamesByCodesString(acceptedCountryCodes)); } } TextField acceptedCountries = addLabelTextField(gridPane, ++rowIndex, "Accepted taker countries:", countries).second; if (tooltip != null) { acceptedCountries.setMouseTransparent(false); acceptedCountries.setTooltip(tooltip); } } rows = 5; String paymentMethodCountryCode = offer.getCountryCode(); if (paymentMethodCountryCode != null) rows++; if (offer.getOfferFeePaymentTxID() != null) rows++; addTitledGroupBg(gridPane, ++rowIndex, rows, "Details", Layout.GROUP_DISTANCE); addLabelTextFieldWithCopyIcon(gridPane, rowIndex, "Offer ID:", offer.getId(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); addLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, "Offerer's onion address:", offer.getOffererNodeAddress().getFullAddress()); addLabelTextField(gridPane, ++rowIndex, "Creation date:", formatter.formatDateTime(offer.getDate())); addLabelTextField(gridPane, ++rowIndex, "Security deposit:", formatter.formatCoinWithCode(FeePolicy.getSecurityDeposit(offer))); if (paymentMethodCountryCode != null) addLabelTextField(gridPane, ++rowIndex, "Offerer's country of bank:", CountryUtil.getNameAndCode(paymentMethodCountryCode)); addLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, "Accepted arbitrators:", formatter.arbitratorAddressesToString(offer.getArbitratorNodeAddresses())); if (offer.getOfferFeePaymentTxID() != null) addLabelTxIdTextField(gridPane, ++rowIndex, "Offer fee transaction ID:", offer.getOfferFeePaymentTxID()); if (placeOfferHandlerOptional.isPresent()) { addTitledGroupBg(gridPane, ++rowIndex, 1, "Commitment", Layout.GROUP_DISTANCE); addLabelTextField(gridPane, rowIndex, "I agree:", Offer.TAC_OFFERER, Layout.FIRST_ROW_AND_GROUP_DISTANCE); addConfirmAndCancelButtons(true); } else if (takeOfferHandlerOptional.isPresent()) { addTitledGroupBg(gridPane, ++rowIndex, 1, "Contract", Layout.GROUP_DISTANCE); addLabelTextField(gridPane, rowIndex, "Terms and conditions:", Offer.TAC_TAKER, Layout.FIRST_ROW_AND_GROUP_DISTANCE); addConfirmAndCancelButtons(false); } else { Button cancelButton = addButtonAfterGroup(gridPane, ++rowIndex, "Close"); cancelButton.setOnAction(e -> { closeHandlerOptional.ifPresent(Runnable::run); hide(); }); } } private void addConfirmAndCancelButtons(boolean isPlaceOffer) { boolean isBuyOffer = offer.getDirection() == Offer.Direction.BUY; boolean isBuyerRole = isPlaceOffer ? isBuyOffer : !isBuyOffer; String placeOfferButtonText = isBuyerRole ? "Confirm offer to buy bitcoin" : "Confirm offer to sell bitcoin"; String takeOfferButtonText = isBuyerRole ? "Confirm offer to buy bitcoin" : "Confirm offer to sell bitcoin"; ImageView iconView = new ImageView(); iconView.setId(isBuyerRole ? "image-buy-white" : "image-sell-white"); Tuple3<Button, BusyAnimation, Label> placeOfferTuple = addButtonBusyAnimationLabelAfterGroup(gridPane, ++rowIndex, isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); Button button = placeOfferTuple.first; button.setMinHeight(40); button.setPadding(new Insets(0, 20, 0, 20)); button.setGraphic(iconView); button.setGraphicTextGap(10); button.setId(isBuyerRole ? "buy-button-big" : "sell-button-big"); button.setText(isPlaceOffer ? placeOfferButtonText : takeOfferButtonText); busyAnimation = placeOfferTuple.second; Label spinnerInfoLabel = placeOfferTuple.third; Button cancelButton = addButton(gridPane, ++rowIndex, BSResources.get("shared.cancel")); cancelButton.setDefaultButton(false); cancelButton.setId("cancel-button"); cancelButton.setOnAction(e -> { closeHandlerOptional.ifPresent(Runnable::run); hide(); }); button.setOnAction(e -> { if (user.getAcceptedArbitrators().size() > 0) { button.setDisable(true); cancelButton.setDisable(true); busyAnimation.play(); if (isPlaceOffer) { spinnerInfoLabel.setText(BSResources.get("createOffer.fundsBox.placeOfferSpinnerInfo")); placeOfferHandlerOptional.get().run(); } else { spinnerInfoLabel.setText(BSResources.get("takeOffer.fundsBox.takeOfferSpinnerInfo")); takeOfferHandlerOptional.get().run(); } } else { new Popup().warning("You have no arbitrator selected.\n" + "Please select at least one arbitrator.").show(); navigation.navigateTo(MainView.class, AccountView.class, AccountSettingsView.class, ArbitratorSelectionView.class); } }); } }