diff --git a/app/src/main/java/net/mynero/wallet/fragment/send/SendFragment.java b/app/src/main/java/net/mynero/wallet/fragment/send/SendFragment.java index 41e53be..e2141aa 100644 --- a/app/src/main/java/net/mynero/wallet/fragment/send/SendFragment.java +++ b/app/src/main/java/net/mynero/wallet/fragment/send/SendFragment.java @@ -4,6 +4,8 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -137,45 +139,19 @@ public class SendFragment extends Fragment { } }); sendMaxButton.setOnClickListener(view1 -> { - addOutputImageView.setVisibility(View.INVISIBLE); - boolean currentValue = mViewModel.sendingMax.getValue() != null ? mViewModel.sendingMax.getValue() : false; - mViewModel.setSendingMax(!currentValue); + mViewModel.setSendingMax(!isSendAll()); }); createButton.setOnClickListener(view1 -> { - boolean sendAll = mViewModel.sendingMax.getValue() != null ? mViewModel.sendingMax.getValue() : false; - ArrayList> dests = new ArrayList<>(); - for(int i = 0; i < getDestCount(); i++) { - ConstraintLayout entryView = getDestView(i); - EditText amountField = entryView.findViewById(R.id.amount_edittext); - EditText addressField = entryView.findViewById(R.id.address_edittext); - String amount = amountField.getText().toString().trim(); - if(!sendAll) { - if(amount.isEmpty()) { - Toast.makeText(getActivity(), getString(R.string.send_amount_empty), Toast.LENGTH_SHORT).show(); - return; - } + boolean outputsValid = checkDestsValidity(isSendAll()); - long amountRaw = Wallet.getAmountFromString(amount); - long balance = BalanceService.getInstance().getUnlockedBalanceRaw(); - if (amountRaw >= balance || amountRaw <= 0) { - Toast.makeText(getActivity(), getString(R.string.send_amount_invalid), Toast.LENGTH_SHORT).show(); - return; - } - } - - String address = addressField.getText().toString().trim(); - boolean isValidAddress = Wallet.isAddressValid(address); - if(!isValidAddress) { - Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show(); - return; - } - dests.add(new Pair<>(address, amount)); + if(outputsValid) { + Toast.makeText(getActivity(), getString(R.string.creating_tx), Toast.LENGTH_SHORT).show(); + createButton.setEnabled(false); + sendMaxButton.setEnabled(false); + createTx(getRawDests(), isSendAll(), priority); + } else { + Toast.makeText(getActivity(), getString(R.string.creating_tx_failed_invalid_outputs), Toast.LENGTH_SHORT).show(); } - - Toast.makeText(getActivity(), getString(R.string.creating_tx), Toast.LENGTH_SHORT).show(); - createButton.setEnabled(false); - sendMaxButton.setEnabled(false); - createTx(dests, sendAll, priority); }); sendButton.setOnClickListener(view1 -> { @@ -188,21 +164,72 @@ public class SendFragment extends Fragment { }); } + private boolean checkDestsValidity(boolean sendAll) { + List> dests = getRawDests(); + for(Pair dest : dests) { + String address = dest.component1(); + String amount = dest.component2(); + if(!sendAll) { + if(amount.isEmpty()) { + Toast.makeText(getActivity(), getString(R.string.send_amount_empty), Toast.LENGTH_SHORT).show(); + return false; + } + + long amountRaw = Wallet.getAmountFromString(amount); + long balance = BalanceService.getInstance().getUnlockedBalanceRaw(); + if (amountRaw >= balance || amountRaw <= 0) { + Toast.makeText(getActivity(), getString(R.string.send_amount_invalid), Toast.LENGTH_SHORT).show(); + return false; + } + } else if(dests.size() > 1) { + Toast.makeText(getActivity(), getString(R.string.send_amount_invalid_sendall_paytomany), Toast.LENGTH_SHORT).show(); + return false; + } + + UriData uriData = UriData.parse(address); + boolean isValidAddress = uriData != null; + if(!isValidAddress) { + Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show(); + return false; + } + + if(dests.size() > 1 && uriData.hasPaymentId()) { + Toast.makeText(getActivity(), getString(R.string.paymentid_paytomany), Toast.LENGTH_SHORT).show(); + return false; + } + } + + return true; + } + + private boolean destsHasPaymentId() { + List> dests = getRawDests(); + for(Pair dest : dests) { + String address = dest.component1(); + UriData uriData = UriData.parse(address); + if(uriData == null) return false; + if(uriData.hasPaymentId()) return true; + } + return false; + } + private void bindObservers() { mViewModel.sendingMax.observe(getViewLifecycleOwner(), sendingMax -> { if (mViewModel.pendingTransaction.getValue() == null) { if (sendingMax) { - addOutputImageView.setVisibility(View.GONE); prepareOutputsForMaxSend(); sendMaxButton.setText(getText(R.string.undo)); } else { - addOutputImageView.setVisibility(View.VISIBLE); unprepareMaxSend(); sendMaxButton.setText(getText(R.string.send_max)); } } }); + mViewModel.showAddOutputButton.observe(getViewLifecycleOwner(), show -> { + setAddOutputButtonVisibility((show && !destsHasPaymentId()) ? View.VISIBLE : View.INVISIBLE); + }); + mViewModel.pendingTransaction.observe(getViewLifecycleOwner(), pendingTx -> { showConfirmationLayout(pendingTx != null); @@ -220,7 +247,31 @@ public class SendFragment extends Fragment { int index = getDestCount(); ConstraintLayout entryView = (ConstraintLayout)inflater.inflate(R.layout.transaction_output_item, null); ImageButton removeOutputImageButton = entryView.findViewById(R.id.remove_output_imagebutton); - + EditText addressField = entryView.findViewById(R.id.address_edittext); + addressField.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + @Override + public void afterTextChanged(Editable editable) { + int currentOutputs = getDestCount(); + UriData uriData = UriData.parse(editable.toString()); + if(uriData != null) { + // we have valid address + boolean hasPaymentId = uriData.hasPaymentId(); + if(currentOutputs > 1 && hasPaymentId) { + // multiple outputs when pasting/editing in integrated address. this is not allowed + Toast.makeText(getActivity(), getString(R.string.paymentid_paytomany), Toast.LENGTH_SHORT).show(); + addressField.setText(null); + } else if(currentOutputs == 1 && hasPaymentId) { + // show add output button: we are sending to integrated address + mViewModel.setShowAddOutputButton(false); + } + } else if(currentOutputs == 1 && !isSendAll()) { + // when send-all is false and this is our only dest and address is invalid, then show add output button + mViewModel.setShowAddOutputButton(true); + } + } + }); entryView.findViewById(R.id.paste_amount_imagebutton).setOnClickListener(view1 -> { Context ctx = getContext(); if (ctx != null) { @@ -261,6 +312,24 @@ public class SendFragment extends Fragment { return destList.getChildCount(); } + private List> getRawDests() { + ArrayList> dests = new ArrayList<>(); + for(int i = 0; i < getDestCount(); i++) { + ConstraintLayout entryView = getDestView(i); + EditText amountField = entryView.findViewById(R.id.amount_edittext); + EditText addressField = entryView.findViewById(R.id.address_edittext); + String amount = amountField.getText().toString().trim(); + String address = addressField.getText().toString().trim(); + dests.add(new Pair<>(address, amount)); + } + + return dests; + } + + private boolean isSendAll() { + return mViewModel.sendingMax.getValue() != null ? mViewModel.sendingMax.getValue() : false; + } + private ConstraintLayout getDestView(int pos) { return (ConstraintLayout) destList.getChildAt(pos); } @@ -284,7 +353,7 @@ public class SendFragment extends Fragment { private void showConfirmationLayout(boolean show) { if (show) { destList.setVisibility(View.GONE); - addOutputImageView.setVisibility(View.GONE); + setAddOutputButtonVisibility(View.GONE); sendMaxButton.setVisibility(View.GONE); createButton.setVisibility(View.GONE); feeRadioGroup.setVisibility(View.GONE); @@ -296,7 +365,7 @@ public class SendFragment extends Fragment { amountTextView.setVisibility(View.VISIBLE); } else { destList.setVisibility(View.VISIBLE); - addOutputImageView.setVisibility(View.VISIBLE); + setAddOutputButtonVisibility(View.VISIBLE); sendMaxButton.setVisibility(View.VISIBLE); createButton.setVisibility(View.VISIBLE); feeRadioGroup.setVisibility(View.VISIBLE); @@ -322,31 +391,41 @@ public class SendFragment extends Fragment { } private void pasteAddress(ConstraintLayout entryView, String clipboard, boolean pastingAmount) { + if(pastingAmount) { + try { + Double.parseDouble(clipboard); + setAmount(entryView, clipboard); + } catch (Exception e) { + Toast.makeText(getActivity(), getString(R.string.send_amount_invalid), Toast.LENGTH_SHORT).show(); + return; + } + } + UriData uriData = UriData.parse(clipboard); if (uriData != null) { + int currentOutputs = getDestCount(); + if(currentOutputs > 1 && uriData.hasPaymentId()) { + Toast.makeText(getActivity(), getString(R.string.paymentid_paytomany), Toast.LENGTH_SHORT).show(); + return; + } else if(currentOutputs == 1 && uriData.hasPaymentId()) { + mViewModel.setShowAddOutputButton(false); + } EditText addressField = entryView.findViewById(R.id.address_edittext); addressField.setText(uriData.getAddress()); if (uriData.hasAmount()) { - sendMaxButton.setEnabled(false); - EditText amountField = entryView.findViewById(R.id.amount_edittext); - amountField.setText(uriData.getAmount()); + setAmount(entryView, uriData.getAmount()); } } else { - if(pastingAmount) { - try { - Double.parseDouble(clipboard); - sendMaxButton.setEnabled(false); - EditText amountField = entryView.findViewById(R.id.amount_edittext); - amountField.setText(clipboard); - } catch (Exception e) { - Toast.makeText(getActivity(), getString(R.string.send_amount_invalid), Toast.LENGTH_SHORT).show(); - } - } else { - Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show(); - } + Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show(); } } + private void setAmount(ConstraintLayout entryView, String amount) { + sendMaxButton.setEnabled(false); + EditText amountField = entryView.findViewById(R.id.amount_edittext); + amountField.setText(amount); + } + private void createTx(List> dests, boolean sendAll, PendingTransaction.Priority feePriority) { ((MoneroApplication)getActivity().getApplication()).getExecutor().execute(() -> { try { @@ -393,4 +472,8 @@ public class SendFragment extends Fragment { } }); } + + private void setAddOutputButtonVisibility(int visibility) { + addOutputImageView.setVisibility(visibility); + } } \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/fragment/send/SendViewModel.java b/app/src/main/java/net/mynero/wallet/fragment/send/SendViewModel.java index cb807cc..7282c3c 100644 --- a/app/src/main/java/net/mynero/wallet/fragment/send/SendViewModel.java +++ b/app/src/main/java/net/mynero/wallet/fragment/send/SendViewModel.java @@ -9,11 +9,18 @@ import net.mynero.wallet.model.PendingTransaction; public class SendViewModel extends ViewModel { private final MutableLiveData _sendingMax = new MutableLiveData<>(false); public LiveData sendingMax = _sendingMax; + private final MutableLiveData _showAddOutputButton = new MutableLiveData<>(true); + public LiveData showAddOutputButton = _showAddOutputButton; private final MutableLiveData _pendingTransaction = new MutableLiveData<>(null); public LiveData pendingTransaction = _pendingTransaction; public void setSendingMax(boolean value) { _sendingMax.setValue(value); + setShowAddOutputButton(!value); + } + + public void setShowAddOutputButton(boolean value) { + _showAddOutputButton.setValue(value); } public void setPendingTransaction(PendingTransaction pendingTx) { diff --git a/app/src/main/java/net/mynero/wallet/util/UriData.java b/app/src/main/java/net/mynero/wallet/util/UriData.java index 9bf1f3d..9c08cc6 100644 --- a/app/src/main/java/net/mynero/wallet/util/UriData.java +++ b/app/src/main/java/net/mynero/wallet/util/UriData.java @@ -1,6 +1,7 @@ package net.mynero.wallet.util; import net.mynero.wallet.model.Wallet; +import net.mynero.wallet.model.WalletManager; import java.util.HashMap; @@ -46,6 +47,10 @@ public class UriData { return address; } + public boolean hasPaymentId() { + return !Wallet.getPaymentIdFromAddress(this.address, WalletManager.getInstance().getWallet().nettype()).isEmpty(); + } + public String getAmount() { String txAmount = params.get(Constants.URI_ARG_AMOUNT); if (txAmount == null) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 073b75c..62bb89b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,6 +5,8 @@ Incorrect password! Not a valid address + Not a valid amount + Cannot send-all in pay-to-many failed - %1$s @@ -24,7 +26,6 @@ + %1$s confirming Please enter an amount - Please enter a valid amount Max Undo Error sending transaction @@ -61,6 +62,7 @@ Fee: %1$s XMR Receive Creating transaction… + Invalid destination combination Sending transaction… Sent transaction! No camera permission @@ -124,4 +126,6 @@ [ auth ] To Maximum allowed outputs + Cannot send to integrated addresses in a pay-to-many transaction +