Open mudam opened 4 years ago
I would strongly suggest this change is merged to master as Braintree have recently announced that old versions of the SDK will cease to work after 20th May 2020. This is because old versions of the SDK include a SSL certificate that is expiring, below are the two emails I received. I have tested the pull request and it seems to work fine.
Our records show that you are using a legacy iOS SDK version that is not compatible with an upcoming update to our root SSL certificate provider for API traffic on May 18, 2020. The records showing your use of a legacy SDK are from March 23rd - April 8th which represent (20%) of all API requests to Braintree coming from your iOS app’s client. If you have updated your iOS integration since then, please disregard this email.
If you do not update to a compatible SDK, your users may not be able to complete checkout workflows until you make the necessary update.
What action is required? In order to avoid interruption to your processing, please update your iOS SDK version by May 18 to latest available version, 4.32.1. If you are unable to update to the latest version, please update to the minimum version 4.10.0.
Note: If you are using the iOS Drop-In SDK please update your integration to use the latest version of Braintree/Core as a dependency (4.32.1). The minimum version of Braintree/Core that can be used is 4.10.0.
What should I do if this is a small amount of my client API requests? It’s likely that the client requests we identified are from users on older iOS devices using an older version of your iOS app. That version of your iOS app in turn has a dependency on an impacted version of the Braintree iOS SDK.
We recommend strongly encouraging or requiring your users update their version of your app so that they are not impacted.
Can this deadline be extended? The current SSL certificate is set to expire shortly after the May 18 deadline, which is why we are unable to offer any extensions. It is critical that you update your integration by May 18, 2020
Our records show that you are using a legacy Android SDK version that is not compatible with an upcoming update to our root SSL certificate provider for API traffic on May 18, 2020. The records show your continued use of a legacy SDK are from March 23rd - April 8th which represent (100%) of all API requests to Braintree coming from your Android app’s client. If you have updated your Android integration since then, please disregard this email.
If you do not update to a compatible SDK, your users may not be able to complete checkout workflows until you make the necessary update.
What action is required? In order to avoid interruption to your processing, please update your Android SDK version by May 18 to latest available version, 3.9.0. If you are unable to update to the latest version, please update to the latest version of the Android v2 SDK, version 2.21.0
If you are using the Android Drop-In SDK please update to the latest version, 4.5.0, or at a minimum version 3.7.1.
(same end of email as iOS above)
@Deeeej @mudam just tried installing this and seeing the following when building:
platforms/ios/Scout/Plugins/cordova-plugin-braintree/BraintreePlugin.m:10:9: fatal error: 'BraintreeDropIn.h' file not found
#import <BraintreeDropIn.h>
^~~~~~~~~~~~~~~~~~~
1 error generated.
Any ideas?
EDIT: already tried removing platform/plugin and re-adding.
@jamesdixon
Please try this fork instead https://github.com/mudam/cordova-plugin-braintree
I've updated ios support since this pr.
@mudam sorry, should have been more clear...I am testing that fork!
I've been testing on Android the code at: https://github.com/mudam/cordova-plugin-braintree, it seems to work fine the first time you make a payment, but if you try to make a subsequent payment it seems to fail without an error, doesnt seem to make a difference what the payment type is, for example credit card or paypal. The second time you try to make a payment, you enter the payment details, press the button and as the plugin returns you to the app, it just seems to fail, doesnt call the callback, just closes the UI and nothing happens. Very strange. I've tried debugging without success. Doesnt seem to be a braintree API issue. Any ideas @mudam? Have you seen this behaviour?
@jamesdixon
Have you checked all pods are installed correctly? I recall having similar issue, don't know the exact cause yet. Try this:
@Deeeej
This is also what I get. First time it completes without issues, subsequent payments appear to be cancelled even though the user is presented with the UI and can apparently complete the payment.
It just happens that the drop-in android intent returns cancelled result code. The intent itself is obtained from Braintree API (new DropInRequest
). Maybe it does not get properly handled by cordova when dismissed, leaving something dirty.
This issue is currently present in all forks of the plugin including the original. I suspect it uses some Android apis incorrectly or in the manner that was appropriate long time ago.
@Deeeej See my original comment to this PR. Some time ago I investigated this issue and had the impression I managed to fix it, hence the PR. This seems not to be the case, however.
Thanks @mudam! At least its not just me. Strangely, in an old version of the plugin the behavior doesnt happen, our current version of the app uses: https://github.com/engineerapart/cordova-plugin-braintree and in that version you can make multiple purchases on Android.
Your original comment said: Recently the plugin has been failing on Android: drop-in immediately resolved with userCancelled=true. This seems to be resolved.
I looked but could find any changes in the commits where there is a userCancelled=true line. Perhaps you did fix it but the change never made it in? Do you remember what the fix was and where it was applied?
I appear to have fixed the issue with the below BraintreePlugin.java. I compared it to a previous version that worked and noticed that 'callbackContext' was passed in as a parameter and not assigned as a global variable. I also explicitly set resultMap.put("userCancelled", false); where it should be false (but to be honest, I don't think this has any affect).
package net.justincredible;
import android.util.Log;
import android.app.Activity;
import android.content.Intent;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.braintreepayments.api.BraintreeFragment;
import com.braintreepayments.api.DataCollector;
import com.braintreepayments.api.PayPal;
import com.braintreepayments.api.dropin.DropInActivity;
import com.braintreepayments.api.dropin.DropInRequest;
import com.braintreepayments.api.dropin.DropInResult;
//import com.braintreepayments.api.exceptions.InvalidArgumentException;
import com.braintreepayments.api.interfaces.PaymentMethodNonceCreatedListener;
import com.braintreepayments.api.interfaces.BraintreeErrorListener;
import com.braintreepayments.api.models.CardNonce;
import com.braintreepayments.api.models.PayPalAccountNonce;
import com.braintreepayments.api.models.PayPalRequest;
import com.braintreepayments.api.models.PaymentMethodNonce;
import com.braintreepayments.api.models.ThreeDSecureInfo;
import com.braintreepayments.api.models.ThreeDSecureRequest;
import com.braintreepayments.api.models.VenmoAccountNonce;
import com.braintreepayments.cardform.view.CardForm;
//import com.google.android.gms.wallet.Cart;
//import com.google.android.gms.wallet.LineItem;
import java.util.HashMap;
import java.util.Map;
public final class BraintreePlugin extends CordovaPlugin implements
PaymentMethodNonceCreatedListener, BraintreeErrorListener {
private static final String TAG = "BraintreePlugin";
private static final int DROP_IN_REQUEST = 100;
private static final int PAYMENT_BUTTON_REQUEST = 200;
private static final int CUSTOM_REQUEST = 300;
private static final int PAYPAL_REQUEST = 400;
private DropInRequest dropInRequest = null;
private CallbackContext _callbackContext = null;
private BraintreeFragment braintreeFragment = null;
private String temporaryToken = null;
@Override
public synchronized boolean execute(String action, final JSONArray args, final CallbackContext callbackContext) throws JSONException {
if (action == null) {
Log.e(TAG, "execute ==> exiting for bad action");
return false;
}
Log.w(TAG, "execute ==> " + action + " === " + args);
try {
if (action.equals("initialize")) {
this.initializeBT(args, callbackContext);
}
else if (action.equals("presentDropInPaymentUI")) {
this.presentDropInPaymentUI(args, callbackContext);
}
else if (action.equals("paypalProcess")) {
this.paypalProcess(args);
}
else if (action.equals("paypalProcessVaulted")) {
this.paypalProcessVaulted();
}
else if (action.equals("setupApplePay")) {
this.setupApplePay(callbackContext);
}
else {
// The given action was not handled above.
return false;
}
} catch (Exception exception) {
callbackContext.error("BraintreePlugin uncaught exception: " + exception.getMessage());
}
return true;
}
@Override
public void onError(Exception error) {
Log.e(TAG, "Caught error from BraintreeSDK: " + error.getMessage());
if(_callbackContext != null){
_callbackContext.error("BraintreePlugin uncaught exception: " + error.getMessage());
}
}
// Actions
private synchronized void initializeBT(final JSONArray args, final CallbackContext callbackContext) throws Exception {
// Ensure we have the correct number of arguments.
if (args.length() != 1) {
callbackContext.error("A token is required.");
return;
}
// Obtain the arguments.
String token = args.getString(0);
if (token == null || token.equals("")) {
callbackContext.error("A token is required.");
return;
}
temporaryToken = token;
// After testing, it seems we do not need this!
// try {
// braintreeFragment = BraintreeFragment.newInstance(this.cordova.getActivity(), temporaryToken);
// braintreeFragment.addListener(this);
// } catch (InvalidArgumentException e) {
// // There was an issue with your authorization string.
// Log.e(TAG, "Error creating PayPal interface: " + e.getMessage());
// _callbackContext.error(TAG + ": Error creating PayPal interface: " + e.getMessage());
// }
callbackContext.success();
}
private synchronized void setupApplePay(final CallbackContext callbackContext) throws JSONException {
// Apple Pay available on iOS only
callbackContext.success();
}
private synchronized void presentDropInPaymentUI(final JSONArray args, final CallbackContext callbackContext) throws JSONException {
// Ensure we have the correct number of arguments.
if (args.length() < 2) {
callbackContext.error("amount and email are required.");
return;
}
// Obtain the arguments.
String amount = args.getString(0);
if (amount == null) {
callbackContext.error("amount is required.");
}
// Mandatory email for 3DS
String email= args.getString(1);
if (email == null) {
callbackContext.error("email is required.");
}
dropInRequest = new DropInRequest().clientToken(temporaryToken);
// dropInRequest.cardholderNameStatus(CardForm.FIELD_REQUIRED);
// dropInRequest.vaultManager(true);
// ThreeDSecureRequest threeDRequest = new ThreeDSecureRequest();
// threeDRequest.amount(amount);
// threeDRequest.email(email);
// threeDRequest.versionRequested(ThreeDSecureRequest.VERSION_2);
// dropInRequest.threeDSecureRequest(threeDRequest);
if (dropInRequest == null) {
callbackContext.error("The Braintree client failed to initialize.");
return;
}
if (dropInRequest.isGooglePaymentEnabled()) {
// // TODO: Make this conditional
// dropInRequest.androidPayCart(Cart.newBuilder()
// .setCurrencyCode("GBP")
// .setTotalPrice(amount)
// .addLineItem(LineItem.newBuilder()
// .setCurrencyCode("GBP")
// .setDescription(primaryDescription)
// .setQuantity("1")
// .setUnitPrice(amount)
// .setTotalPrice(amount)
// .build())
// .build()
// );
}
this.cordova.setActivityResultCallback(this);
try {
Intent intent = dropInRequest.getIntent(this.cordova.getActivity());
if (intent == null) {
Log.e(TAG, "presentDropInPaymentUI failed ===> unable to create Braintree DropInRequest");
callbackContext.error(TAG + ": presentDropInPaymentUI failed ===> unable to create Braintree DropInRequest");
return;
}
this.cordova.startActivityForResult(this, intent, DROP_IN_REQUEST);
} catch (Exception e) {
Log.e(TAG, "presentDropInPaymentUI failed with error ===> " + e.getMessage());
callbackContext.error(TAG + ": presentDropInPaymentUI failed with error ===> " + e.getMessage());
}
_callbackContext = callbackContext;
}
private synchronized void paypalProcess(final JSONArray args) throws Exception {
PayPalRequest payPalRequest = new PayPalRequest(args.getString(0));
payPalRequest.currencyCode(args.getString(1));
PayPal.requestOneTimePayment(braintreeFragment, payPalRequest);
}
private synchronized void paypalProcessVaulted() throws Exception {
//PayPal.authorizeAccount(braintreeFragment);
}
// Results
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
Log.i(TAG, "DropIn Activity Result: " + requestCode + ", " + resultCode);
if (_callbackContext == null) {
Log.e(TAG, "onActivityResult exiting ==> callbackContext is invalid");
return;
}
if (requestCode == DROP_IN_REQUEST) {
PaymentMethodNonce paymentMethodNonce = null;
if (resultCode == Activity.RESULT_OK) {
DropInResult result = intent.getParcelableExtra(DropInResult.EXTRA_DROP_IN_RESULT);
paymentMethodNonce = result.getPaymentMethodNonce();
Log.i(TAG, "DropIn Activity Result: paymentMethodNonce = " + paymentMethodNonce);
}
// handle errors here, an exception may be available in
if (intent != null && intent.getSerializableExtra(DropInActivity.EXTRA_ERROR) != null) {
Exception error = (Exception)intent.getSerializableExtra(DropInActivity.EXTRA_ERROR);
Log.e(TAG, "onActivityResult exiting ==> received error: " + error.getMessage() + "\n" + error.getStackTrace());
_callbackContext.error("onActivityResult exiting ==> received error: " + error.getMessage());
return;
}
this.handleDropInPaymentUiResult(resultCode, paymentMethodNonce);
}
else if (requestCode == PAYMENT_BUTTON_REQUEST) {
//TODO
_callbackContext.error("Activity result handler for PAYMENT_BUTTON_REQUEST not implemented.");
}
else if (requestCode == CUSTOM_REQUEST) {
_callbackContext.error("Activity result handler for CUSTOM_REQUEST not implemented.");
//TODO
}
else if (requestCode == PAYPAL_REQUEST) {
_callbackContext.error("Activity result handler for PAYPAL_REQUEST not implemented.");
//TODO
} else {
Log.w(TAG, "onActivityResult exiting ==> requestCode [" + requestCode + "] was unhandled");
}
}
/**
* Helper used to handle the result of the drop-in payment UI.
*
* @param resultCode Indicates the result of the UI.
* @param paymentMethodNonce Contains information about a successful payment.
*/
private void handleDropInPaymentUiResult(int resultCode, PaymentMethodNonce paymentMethodNonce) {
Log.i(TAG, "handleDropInPaymentUiResult resultCode ==> " + resultCode + ", paymentMethodNonce = " + paymentMethodNonce);
if (_callbackContext == null) {
Log.e(TAG, "handleDropInPaymentUiResult exiting ==> callbackContext is invalid");
return;
}
if (resultCode == Activity.RESULT_CANCELED) {
Map<String, Object> resultMapx = new HashMap<String, Object>();
resultMapx.put("userCancelled", true);
_callbackContext.success(new JSONObject(resultMapx));
_callbackContext = null;
return;
}
if (paymentMethodNonce == null) {
_callbackContext.error("Result was not RESULT_CANCELED, but no PaymentMethodNonce was returned from the Braintree SDK (was " + resultCode + ").");
_callbackContext = null;
return;
}
Map<String, Object> resultMap = this.getPaymentUINonceResult(paymentMethodNonce);
_callbackContext.success(new JSONObject(resultMap));
_callbackContext = null;
}
/**
* Helper used to return a dictionary of values from the given payment method nonce.
* Handles several different types of nonces (eg for cards, PayPal, etc).
*
* @param paymentMethodNonce The nonce used to build a dictionary of data from.
* @return The dictionary of data populated via the given payment method nonce.
*/
private Map<String, Object> getPaymentUINonceResult(PaymentMethodNonce paymentMethodNonce) {
Map<String, Object> resultMap = new HashMap<String, Object>();
resultMap.put("userCancelled", false);
resultMap.put("nonce", paymentMethodNonce.getNonce());
resultMap.put("type", paymentMethodNonce.getTypeLabel());
resultMap.put("localizedDescription", paymentMethodNonce.getDescription());
// Card
if (paymentMethodNonce instanceof CardNonce) {
CardNonce cardNonce = (CardNonce)paymentMethodNonce;
Map<String, Object> innerMap = new HashMap<String, Object>();
innerMap.put("lastTwo", cardNonce.getLastTwo());
innerMap.put("network", cardNonce.getCardType());
resultMap.put("card", innerMap);
}
// PayPal
if (paymentMethodNonce instanceof PayPalAccountNonce) {
PayPalAccountNonce payPalAccountNonce = (PayPalAccountNonce)paymentMethodNonce;
Map<String, Object> innerMap = new HashMap<String, Object>();
resultMap.put("email", payPalAccountNonce.getEmail());
resultMap.put("firstName", payPalAccountNonce.getFirstName());
resultMap.put("lastName", payPalAccountNonce.getLastName());
resultMap.put("phone", payPalAccountNonce.getPhone());
//resultMap.put("billingAddress", paypalAccountNonce.getBillingAddress()); //TODO
//resultMap.put("shippingAddress", paypalAccountNonce.getShippingAddress()); //TODO
resultMap.put("clientMetadataId", payPalAccountNonce.getClientMetadataId());
resultMap.put("payerId", payPalAccountNonce.getPayerId());
resultMap.put("paypalAccount", innerMap);
}
// 3D Secure
if (paymentMethodNonce instanceof CardNonce) {
CardNonce cardNonce = (CardNonce) paymentMethodNonce;
ThreeDSecureInfo threeDSecureInfo = cardNonce.getThreeDSecureInfo();
if (threeDSecureInfo != null) {
Map<String, Object> innerMap = new HashMap<String, Object>();
innerMap.put("liabilityShifted", threeDSecureInfo.isLiabilityShifted());
innerMap.put("liabilityShiftPossible", threeDSecureInfo.isLiabilityShiftPossible());
innerMap.put("enrolled", threeDSecureInfo.getEnrolled());
resultMap.put("threeDSecureCard", innerMap);
}
}
// Venmo
if (paymentMethodNonce instanceof VenmoAccountNonce) {
VenmoAccountNonce venmoAccountNonce = (VenmoAccountNonce) paymentMethodNonce;
Map<String, Object> innerMap = new HashMap<String, Object>();
innerMap.put("username", venmoAccountNonce.getUsername());
resultMap.put("venmoAccount", innerMap);
}
return resultMap;
}
@Override
public void onPaymentMethodNonceCreated(PaymentMethodNonce paymentMethodNonce) {
Log.i(TAG, "onPaymentMethodNonceCreated ==> paymentMethodNonce = " + paymentMethodNonce);
if (_callbackContext == null) {
Log.e(TAG, "onPaymentMethodNonceCreated exiting ==> callbackContext is invalid");
return;
}
try {
JSONObject json = new JSONObject();
json.put("userCancelled", false);
json.put("nonce", paymentMethodNonce.getNonce().toString());
//json.put("deviceData", DataCollector.collectDeviceData(braintreeFragment));
//json.put("deviceData", DataCollector.collectDeviceData(braintreeFragment, this));
if (paymentMethodNonce instanceof PayPalAccountNonce) {
PayPalAccountNonce pp = (PayPalAccountNonce) paymentMethodNonce;
json.put("payerId", pp.getPayerId().toString());
json.put("firstName", pp.getFirstName().toString());
json.put("lastName", pp.getLastName().toString());
}
_callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, json));
} catch (Exception e) {
Log.e(TAG, "onPaymentMethodNonceCreated ==> error:" + e.getMessage());
e.printStackTrace();
}
}
}
I appear to have fixed the issue with the below BraintreePlugin.java. I compared it to a previous version that worked and noticed that 'callbackContext' was passed in as a parameter and not assigned as a global variable. I also explicitly set resultMap.put("userCancelled", false); where it should be false (but to be honest, I don't think this has any affect).
package net.justincredible; import android.util.Log; import android.app.Activity; import android.content.Intent; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import com.braintreepayments.api.BraintreeFragment; import com.braintreepayments.api.DataCollector; import com.braintreepayments.api.PayPal; import com.braintreepayments.api.dropin.DropInActivity; import com.braintreepayments.api.dropin.DropInRequest; import com.braintreepayments.api.dropin.DropInResult; //import com.braintreepayments.api.exceptions.InvalidArgumentException; import com.braintreepayments.api.interfaces.PaymentMethodNonceCreatedListener; import com.braintreepayments.api.interfaces.BraintreeErrorListener; import com.braintreepayments.api.models.CardNonce; import com.braintreepayments.api.models.PayPalAccountNonce; import com.braintreepayments.api.models.PayPalRequest; import com.braintreepayments.api.models.PaymentMethodNonce; import com.braintreepayments.api.models.ThreeDSecureInfo; import com.braintreepayments.api.models.ThreeDSecureRequest; import com.braintreepayments.api.models.VenmoAccountNonce; import com.braintreepayments.cardform.view.CardForm; //import com.google.android.gms.wallet.Cart; //import com.google.android.gms.wallet.LineItem; import java.util.HashMap; import java.util.Map; public final class BraintreePlugin extends CordovaPlugin implements PaymentMethodNonceCreatedListener, BraintreeErrorListener { private static final String TAG = "BraintreePlugin"; private static final int DROP_IN_REQUEST = 100; private static final int PAYMENT_BUTTON_REQUEST = 200; private static final int CUSTOM_REQUEST = 300; private static final int PAYPAL_REQUEST = 400; private DropInRequest dropInRequest = null; private CallbackContext _callbackContext = null; private BraintreeFragment braintreeFragment = null; private String temporaryToken = null; @Override public synchronized boolean execute(String action, final JSONArray args, final CallbackContext callbackContext) throws JSONException { if (action == null) { Log.e(TAG, "execute ==> exiting for bad action"); return false; } Log.w(TAG, "execute ==> " + action + " === " + args); try { if (action.equals("initialize")) { this.initializeBT(args, callbackContext); } else if (action.equals("presentDropInPaymentUI")) { this.presentDropInPaymentUI(args, callbackContext); } else if (action.equals("paypalProcess")) { this.paypalProcess(args); } else if (action.equals("paypalProcessVaulted")) { this.paypalProcessVaulted(); } else if (action.equals("setupApplePay")) { this.setupApplePay(callbackContext); } else { // The given action was not handled above. return false; } } catch (Exception exception) { callbackContext.error("BraintreePlugin uncaught exception: " + exception.getMessage()); } return true; } @Override public void onError(Exception error) { Log.e(TAG, "Caught error from BraintreeSDK: " + error.getMessage()); if(_callbackContext != null){ _callbackContext.error("BraintreePlugin uncaught exception: " + error.getMessage()); } } // Actions private synchronized void initializeBT(final JSONArray args, final CallbackContext callbackContext) throws Exception { // Ensure we have the correct number of arguments. if (args.length() != 1) { callbackContext.error("A token is required."); return; } // Obtain the arguments. String token = args.getString(0); if (token == null || token.equals("")) { callbackContext.error("A token is required."); return; } temporaryToken = token; // After testing, it seems we do not need this! // try { // braintreeFragment = BraintreeFragment.newInstance(this.cordova.getActivity(), temporaryToken); // braintreeFragment.addListener(this); // } catch (InvalidArgumentException e) { // // There was an issue with your authorization string. // Log.e(TAG, "Error creating PayPal interface: " + e.getMessage()); // _callbackContext.error(TAG + ": Error creating PayPal interface: " + e.getMessage()); // } callbackContext.success(); } private synchronized void setupApplePay(final CallbackContext callbackContext) throws JSONException { // Apple Pay available on iOS only callbackContext.success(); } private synchronized void presentDropInPaymentUI(final JSONArray args, final CallbackContext callbackContext) throws JSONException { // Ensure we have the correct number of arguments. if (args.length() < 2) { callbackContext.error("amount and email are required."); return; } // Obtain the arguments. String amount = args.getString(0); if (amount == null) { callbackContext.error("amount is required."); } // Mandatory email for 3DS String email= args.getString(1); if (email == null) { callbackContext.error("email is required."); } dropInRequest = new DropInRequest().clientToken(temporaryToken); // dropInRequest.cardholderNameStatus(CardForm.FIELD_REQUIRED); // dropInRequest.vaultManager(true); // ThreeDSecureRequest threeDRequest = new ThreeDSecureRequest(); // threeDRequest.amount(amount); // threeDRequest.email(email); // threeDRequest.versionRequested(ThreeDSecureRequest.VERSION_2); // dropInRequest.threeDSecureRequest(threeDRequest); if (dropInRequest == null) { callbackContext.error("The Braintree client failed to initialize."); return; } if (dropInRequest.isGooglePaymentEnabled()) { // // TODO: Make this conditional // dropInRequest.androidPayCart(Cart.newBuilder() // .setCurrencyCode("GBP") // .setTotalPrice(amount) // .addLineItem(LineItem.newBuilder() // .setCurrencyCode("GBP") // .setDescription(primaryDescription) // .setQuantity("1") // .setUnitPrice(amount) // .setTotalPrice(amount) // .build()) // .build() // ); } this.cordova.setActivityResultCallback(this); try { Intent intent = dropInRequest.getIntent(this.cordova.getActivity()); if (intent == null) { Log.e(TAG, "presentDropInPaymentUI failed ===> unable to create Braintree DropInRequest"); callbackContext.error(TAG + ": presentDropInPaymentUI failed ===> unable to create Braintree DropInRequest"); return; } this.cordova.startActivityForResult(this, intent, DROP_IN_REQUEST); } catch (Exception e) { Log.e(TAG, "presentDropInPaymentUI failed with error ===> " + e.getMessage()); callbackContext.error(TAG + ": presentDropInPaymentUI failed with error ===> " + e.getMessage()); } _callbackContext = callbackContext; } private synchronized void paypalProcess(final JSONArray args) throws Exception { PayPalRequest payPalRequest = new PayPalRequest(args.getString(0)); payPalRequest.currencyCode(args.getString(1)); PayPal.requestOneTimePayment(braintreeFragment, payPalRequest); } private synchronized void paypalProcessVaulted() throws Exception { //PayPal.authorizeAccount(braintreeFragment); } // Results @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); Log.i(TAG, "DropIn Activity Result: " + requestCode + ", " + resultCode); if (_callbackContext == null) { Log.e(TAG, "onActivityResult exiting ==> callbackContext is invalid"); return; } if (requestCode == DROP_IN_REQUEST) { PaymentMethodNonce paymentMethodNonce = null; if (resultCode == Activity.RESULT_OK) { DropInResult result = intent.getParcelableExtra(DropInResult.EXTRA_DROP_IN_RESULT); paymentMethodNonce = result.getPaymentMethodNonce(); Log.i(TAG, "DropIn Activity Result: paymentMethodNonce = " + paymentMethodNonce); } // handle errors here, an exception may be available in if (intent != null && intent.getSerializableExtra(DropInActivity.EXTRA_ERROR) != null) { Exception error = (Exception)intent.getSerializableExtra(DropInActivity.EXTRA_ERROR); Log.e(TAG, "onActivityResult exiting ==> received error: " + error.getMessage() + "\n" + error.getStackTrace()); _callbackContext.error("onActivityResult exiting ==> received error: " + error.getMessage()); return; } this.handleDropInPaymentUiResult(resultCode, paymentMethodNonce); } else if (requestCode == PAYMENT_BUTTON_REQUEST) { //TODO _callbackContext.error("Activity result handler for PAYMENT_BUTTON_REQUEST not implemented."); } else if (requestCode == CUSTOM_REQUEST) { _callbackContext.error("Activity result handler for CUSTOM_REQUEST not implemented."); //TODO } else if (requestCode == PAYPAL_REQUEST) { _callbackContext.error("Activity result handler for PAYPAL_REQUEST not implemented."); //TODO } else { Log.w(TAG, "onActivityResult exiting ==> requestCode [" + requestCode + "] was unhandled"); } } /** * Helper used to handle the result of the drop-in payment UI. * * @param resultCode Indicates the result of the UI. * @param paymentMethodNonce Contains information about a successful payment. */ private void handleDropInPaymentUiResult(int resultCode, PaymentMethodNonce paymentMethodNonce) { Log.i(TAG, "handleDropInPaymentUiResult resultCode ==> " + resultCode + ", paymentMethodNonce = " + paymentMethodNonce); if (_callbackContext == null) { Log.e(TAG, "handleDropInPaymentUiResult exiting ==> callbackContext is invalid"); return; } if (resultCode == Activity.RESULT_CANCELED) { Map<String, Object> resultMapx = new HashMap<String, Object>(); resultMapx.put("userCancelled", true); _callbackContext.success(new JSONObject(resultMapx)); _callbackContext = null; return; } if (paymentMethodNonce == null) { _callbackContext.error("Result was not RESULT_CANCELED, but no PaymentMethodNonce was returned from the Braintree SDK (was " + resultCode + ")."); _callbackContext = null; return; } Map<String, Object> resultMap = this.getPaymentUINonceResult(paymentMethodNonce); _callbackContext.success(new JSONObject(resultMap)); _callbackContext = null; } /** * Helper used to return a dictionary of values from the given payment method nonce. * Handles several different types of nonces (eg for cards, PayPal, etc). * * @param paymentMethodNonce The nonce used to build a dictionary of data from. * @return The dictionary of data populated via the given payment method nonce. */ private Map<String, Object> getPaymentUINonceResult(PaymentMethodNonce paymentMethodNonce) { Map<String, Object> resultMap = new HashMap<String, Object>(); resultMap.put("userCancelled", false); resultMap.put("nonce", paymentMethodNonce.getNonce()); resultMap.put("type", paymentMethodNonce.getTypeLabel()); resultMap.put("localizedDescription", paymentMethodNonce.getDescription()); // Card if (paymentMethodNonce instanceof CardNonce) { CardNonce cardNonce = (CardNonce)paymentMethodNonce; Map<String, Object> innerMap = new HashMap<String, Object>(); innerMap.put("lastTwo", cardNonce.getLastTwo()); innerMap.put("network", cardNonce.getCardType()); resultMap.put("card", innerMap); } // PayPal if (paymentMethodNonce instanceof PayPalAccountNonce) { PayPalAccountNonce payPalAccountNonce = (PayPalAccountNonce)paymentMethodNonce; Map<String, Object> innerMap = new HashMap<String, Object>(); resultMap.put("email", payPalAccountNonce.getEmail()); resultMap.put("firstName", payPalAccountNonce.getFirstName()); resultMap.put("lastName", payPalAccountNonce.getLastName()); resultMap.put("phone", payPalAccountNonce.getPhone()); //resultMap.put("billingAddress", paypalAccountNonce.getBillingAddress()); //TODO //resultMap.put("shippingAddress", paypalAccountNonce.getShippingAddress()); //TODO resultMap.put("clientMetadataId", payPalAccountNonce.getClientMetadataId()); resultMap.put("payerId", payPalAccountNonce.getPayerId()); resultMap.put("paypalAccount", innerMap); } // 3D Secure if (paymentMethodNonce instanceof CardNonce) { CardNonce cardNonce = (CardNonce) paymentMethodNonce; ThreeDSecureInfo threeDSecureInfo = cardNonce.getThreeDSecureInfo(); if (threeDSecureInfo != null) { Map<String, Object> innerMap = new HashMap<String, Object>(); innerMap.put("liabilityShifted", threeDSecureInfo.isLiabilityShifted()); innerMap.put("liabilityShiftPossible", threeDSecureInfo.isLiabilityShiftPossible()); innerMap.put("enrolled", threeDSecureInfo.getEnrolled()); resultMap.put("threeDSecureCard", innerMap); } } // Venmo if (paymentMethodNonce instanceof VenmoAccountNonce) { VenmoAccountNonce venmoAccountNonce = (VenmoAccountNonce) paymentMethodNonce; Map<String, Object> innerMap = new HashMap<String, Object>(); innerMap.put("username", venmoAccountNonce.getUsername()); resultMap.put("venmoAccount", innerMap); } return resultMap; } @Override public void onPaymentMethodNonceCreated(PaymentMethodNonce paymentMethodNonce) { Log.i(TAG, "onPaymentMethodNonceCreated ==> paymentMethodNonce = " + paymentMethodNonce); if (_callbackContext == null) { Log.e(TAG, "onPaymentMethodNonceCreated exiting ==> callbackContext is invalid"); return; } try { JSONObject json = new JSONObject(); json.put("userCancelled", false); json.put("nonce", paymentMethodNonce.getNonce().toString()); //json.put("deviceData", DataCollector.collectDeviceData(braintreeFragment)); //json.put("deviceData", DataCollector.collectDeviceData(braintreeFragment, this)); if (paymentMethodNonce instanceof PayPalAccountNonce) { PayPalAccountNonce pp = (PayPalAccountNonce) paymentMethodNonce; json.put("payerId", pp.getPayerId().toString()); json.put("firstName", pp.getFirstName().toString()); json.put("lastName", pp.getLastName().toString()); } _callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, json)); } catch (Exception e) { Log.e(TAG, "onPaymentMethodNonceCreated ==> error:" + e.getMessage()); e.printStackTrace(); } }
}
I can confirm that this edit actually fixes the "canceled=true" problem. Tried on android, seems to work fine on mudam fork.
@Deeeej nice work. @mudam can you get this merged into your fork? or maybe it deserves its own PR
Thanks @Deeeej , great stuff. With your permission I'll add this to my fork and probably create new PR.
@jamesdixon have you tried this code as well? I'll do quick test and proceed to publishing.
@mudam can i suggest to update the docs too? The part about the ios hook seems unnecessary since the hook file has been deleted.
@mudam Of course, glad to help in any way I can. It's worth noting in my change I also commented the lines: // dropInRequest.cardholderNameStatus(CardForm.FIELD_REQUIRED); // dropInRequest.vaultManager(true);
As a) The vault manager doesnt seem to apply here (?) b) Card holder name is not a required field (especially as 3DS is not turned on)
@mudam are you guys seeing that vaultManager
is enabled? We just had a client complain that their customer deleted a payment method and can no longer be billed. I am seeing that the vault manager is enabled and according to the Braintree docs, it looks like it has to be explicitly enabled. I do not see any calls to it.
@mudam have you by chance successfully used 3ds with this plugin?
@MedITSolutionsKurman I noticed you have a fork with 3ds but having trouble getting it to compile for iOS. Lots of build errors. Are you actively using your fork? Desperate for something with 3ds support.
Recently the plugin has been failing on Android: drop-in immediately resolved with
userCancelled=true
. This seems to be resolved.