engineerapart / cordova-plugin-braintree

:credit_card: A Cordova plugin for the Braintree mobile payment processing SDK.
MIT License
3 stars 12 forks source link

Updated Android support lib reference #5

Open mudam opened 4 years ago

mudam commented 4 years ago

Recently the plugin has been failing on Android: drop-in immediately resolved with userCancelled=true. This seems to be resolved.

Deeeej commented 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.

iOS

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

Android

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)

jamesdixon commented 4 years ago

@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.

mudam commented 4 years ago

@jamesdixon

Please try this fork instead https://github.com/mudam/cordova-plugin-braintree

I've updated ios support since this pr.

jamesdixon commented 4 years ago

@mudam sorry, should have been more clear...I am testing that fork!

Deeeej commented 4 years ago

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?

mudam commented 4 years ago

@jamesdixon

Have you checked all pods are installed correctly? I recall having similar issue, don't know the exact cause yet. Try this:

  1. open project in xcode
  2. remove the offending line from the code
  3. add the line again using xcode code completion
mudam commented 4 years ago

@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.

mudam commented 4 years 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.

Deeeej commented 4 years ago

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?

Deeeej commented 4 years ago

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();
    }
}

}

rastafan commented 4 years ago

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.

jamesdixon commented 4 years ago

@Deeeej nice work. @mudam can you get this merged into your fork? or maybe it deserves its own PR

mudam commented 4 years ago

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.

rastafan commented 4 years ago

@mudam can i suggest to update the docs too? The part about the ios hook seems unnecessary since the hook file has been deleted.

Deeeej commented 4 years ago

@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)

jamesdixon commented 3 years ago

@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.

jamesdixon commented 2 years ago

@mudam have you by chance successfully used 3ds with this plugin?

jamesdixon commented 2 years ago

@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.