braintree / braintree-web-drop-in

Braintree Drop-in for the web
MIT License
200 stars 126 forks source link

"requestPaymentMethod()" Intermittently Throws an Error When Called in "braintree.dropin.create()" Callback #399

Closed adamhenson closed 1 year ago

adamhenson commented 6 years ago

General information

How I Arrived at the Issue

A user who has a "requestable" payment (a payment method available from the vault) arrives on a page and is presented with the below UI (note the Pay button is coming from my application):

screen shot 2018-05-15 at 10 34 56 am

This is based on the example code as shown in the README.md file of this repo:

var submitButton = document.querySelector('#submit-button');

braintree.dropin.create({
  authorization: 'CLIENT_AUTHORIZATION',
  selector: '#dropin-container'
}, function (err, dropinInstance) {
  if (err) {
    // Handle any errors that might've occurred when creating Drop-in
    console.error(err);
    return;
  }
  submitButton.addEventListener('click', function () {
    dropinInstance.requestPaymentMethod(function (err, payload) {
      if (err) {
        // Handle errors in requesting payment method
      }

      // Send payload.nonce to your server
    });
  });
});

Below is the flow that seems awkward to me for this type of user:

  1. The user clicks the "Pay" button which calls instance.requestPaymentMethod() even though the user already has a payment method... so we can get the payment nonce.
  2. The user has to interact with the application again in order to submit the payment. My app is using the same button and changing the text to "Confirm" (see below).
screen shot 2018-05-15 at 10 56 54 am

This a bit awkward because nothing changes except the text of the button from "Pay" to "Confirm" and requires one extra unnecessary click to complete the order. I'm only doing that so the user sees something change.

So, I thought I could simply check if we have a requestable payment on the callback of dropin.create(), and if so - just execute instance.requestPaymentMethod() without any user interaction. Something like the below code is what could accomplish that and is where the bug is reproducible:

var submitButton = document.querySelector('#submit-button');

braintree.dropin.create({
  authorization: 'CLIENT_AUTHORIZATION',
  selector: '#dropin-container'
}, function (err, dropinInstance) {
  if (err) {
    // Handle any errors that might've occurred when creating Drop-in
    console.error(err);
    return;
  }

  if (dropinInstance.isPaymentMethodRequestable()) {
    dropinInstance.requestPaymentMethod(function (err, payload) {
      if (err) {
        // Handle errors in requesting payment method
      }

      // Send payload.nonce to your server
    });
  }

  // do whatever you want...
  // ...
});

Issue description

When calling instance.requestPaymentMethod() within the braintree.dropin.create() callback function an error intermittently occurs (I can reproduce approximately 1 out of every 5 page loads). Above is an example of code that reproduces the issue.

Below is the error output from Chrome Developer Tools:

dropin-1.10.0.min.js:7 Uncaught TypeError: Cannot read property 'requestPaymentMethod' of undefined
at i.requestPaymentMethod (dropin-1.10.0.min.js:7)
at i.requestPaymentMethod (dropin-1.10.0.min.js:5)
at i.requestPaymentMethod (dropin-1.10.0.min.js:1)
at requestPaymentMethod (braintree.js:23)
at braintree.js:50
at dropin-1.10.0.min.js:1 

Strange (probably unreliable) workaround

I did discover that when I change this from the example above:

if (dropinInstance.isPaymentMethodRequestable()) {
  dropinInstance.requestPaymentMethod(function (err, payload) {
    // ...
  });
}

To this:

if (dropinInstance.isPaymentMethodRequestable()) {
  setTimeout(function() {
    dropinInstance.requestPaymentMethod(function (err, payload) {
      // ...
    });
  }, 200);
}

... The issue is no longer reproducible (in my testing with Chrome). This tells me there is probably some kind of race condition in the dropin UI library.

crookedneighbor commented 6 years ago

Thanks for the report. We'll look into this.

adamhenson commented 6 years ago

Thanks! I do appreciate this library. Also, a different workaround that I'm going with is to simply call requestPaymentMethod() on click of my button... and then creating my sale transaction in the same thread. It's a little awkward because the dropin UI changes - for example when adding a new payment - the UI changes to show the list of payments with the new one checked off. Normally, the user would click again to finalize... but I'm skipping over that part by submitting the payment immediately after success of requestPaymentMethod(). Theoretically one could hide the changing dropin UI during this transition as a workaround.

So - just wanted to say for me - this bug isn't super critical.

ShaunDychko commented 5 years ago

Just wanted to mention that this issue still persists in version 1.14.1 of the Drop-in UI, with several developers reporting it with the Braintree Cashier Drupal module in this issue. The call to requestPaymentMethod() is being invoked by the button click callback, the event for which has a Drop-in instance property. In some circumstances (browser / OS version combinations, of which I'm unsure) this property is undefined at the moment the event occurs, but it becomes defined about 200ms later. The button click callback succeeds if requestPaymetMethod() is invoked 200ms after the button click. It should not be necessary to wait 200ms before calling requestPaymentMethod(). Below is the JS integration for the Drop-in UI without the 200ms fix.

(function ($, Drupal, drupalSettings) {

  'use strict';

  /**
   * Callback for the click event on the visible submit button.
   *
   * @param {jQuery.Event} event
   */
  function onInitialButtonClick(event) {
    event.preventDefault();

    event.data.buttonInitial.prop('disabled', true)
      .addClass('is-disabled');

    event.data.instance.requestPaymentMethod(function (requestPaymentMethodErr, payload) {
      if (requestPaymentMethodErr) {
        event.data.buttonInitial.prop('disabled', false)
          .removeClass('is-disabled');
        return;
      }
      event.data.nonceField.val(payload.nonce);
      event.data.buttonFinal.click();
    });
  }

  /**
   * Callback for after the Dropin UI instance is created.
   *
   * @param createErr
   *   The error generated if the Dropin UI could not be created.
   * @param {object} instance
   *   The Braintree Dropin UI instance.
   *
   * @see https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html
   */
  function onInstanceCreate(createErr, instance) {
    var buttonInitial = $('#submit-button');
    var buttonFinal = $('#final-submit');
    var nonceField = $('#payment-method-nonce');

    buttonInitial.prop('disabled', false)
      .removeClass('is-disabled')
      .click({
        instance: instance,
        buttonInitial: buttonInitial,
        buttonFinal: buttonFinal,
        nonceField: nonceField
      }, onInitialButtonClick);
  }

  /**
   * Create the Braintree Dropin UI.
   *
   * @type {{attach: Drupal.behaviors.signupForm.attach}}
   */
  Drupal.behaviors.signupForm = {
    attach: function (context, settings) {

      var createParams = {
        authorization: drupalSettings.braintree_cashier.authorization,
        container: '#dropin-container'
      };

      if (drupalSettings.braintree_cashier.acceptPaypal) {
        createParams.paypal = {
          flow: 'vault'
        };
      }

      braintree.dropin.create(createParams, onInstanceCreate);
    }
  };

})(jQuery, Drupal, drupalSettings);
ShaunDychko commented 5 years ago

For us the solution has been to declare the dropinInstance variable in a scope broader than the button click handler and dropin.create. When the dropinInstance is initialized by the dropin.create callback, it's ready to go when the user eventually clicks on the submit button, so there's no need to use setTimeout().

Here is the updated version of integrating the Drop-in UI, which has fixed the issue. Example code for the Drop-in UI should be updated to show the dropinInstance declared in a scope broader than both dropin.create and the button click handler.

(function ($, Drupal, drupalSettings) {

  'use strict';

  var dropinInstance;
  var buttonInitialSelector = '#submit-button';
  var buttonInitial;
  var buttonFinal;
  var nonceField;

  /**
   * Callback for the click event on the visible submit button.
   *
   * @param {jQuery.Event} event
   */
  function onInitialButtonClick(event) {
    event.preventDefault();

    buttonInitial.prop('disabled', true)
      .addClass('is-disabled');

    dropinInstance.requestPaymentMethod(function (requestPaymentMethodErr, payload) {
      if (requestPaymentMethodErr) {
        buttonInitial.prop('disabled', false)
          .removeClass('is-disabled')
          .click(onInitialButtonClick);
        return;
      }
      nonceField.val(payload.nonce);
      buttonFinal.click();
    });
    // Remove event handler since it was getting submitted multiple times
    // during automated tests.
    $.off('click', buttonInitialSelector, onInitialButtonClick);
  }

  /**
   * Callback for after the Dropin UI instance is created.
   *
   * @param createErr
   *   The error generated if the Dropin UI could not be created.
   * @param {object} instance
   *   The Braintree Dropin UI instance.
   *
   * @see https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html
   */
  function onInstanceCreate(createErr, instance) {
    dropinInstance = instance;

    buttonInitial.prop('disabled', false)
      .removeClass('is-disabled')
      .click(onInitialButtonClick);
  }

  /**
   * Create the Braintree Dropin UI.
   *
   * @type {{attach: Drupal.behaviors.signupForm.attach}}
   */
  Drupal.behaviors.signupForm = {
    attach: function (context, settings) {

      buttonInitial = $(buttonInitialSelector);
      buttonFinal = $('#final-submit');
      nonceField = $('#payment-method-nonce');

      var createParams = {
        authorization: drupalSettings.braintree_cashier.authorization,
        container: '#dropin-container'
      };

      if (drupalSettings.braintree_cashier.acceptPaypal) {
        createParams.paypal = {
          flow: 'vault'
        };
      }

      braintree.dropin.create(createParams, onInstanceCreate);
    }
  };

})(jQuery, Drupal, drupalSettings);
crookedneighbor commented 5 years ago

Thanks for that info. That's very helpful.

pedro-pedrosa commented 5 years ago

I was having a similar issue but it wasn't any sort of race condition, in my case it was a binding issue with the requestPaymentMethod function. I had to define my handler using an es6 arrow function to not mess with the this keyword inside the requestPaymentMethod function.

Just in case anyone stumbles upon this.

rheldt commented 4 years ago

FWIW, I'm running into this issue every so often in drop-in UI version 1.18.0. Chrome 80. Most of the time it works just fine, other times I'm seeing Cannot read property 'requestPaymentMethod' of undefined at initBraintreeAndSubmit.

var braintreeInstance;
document.getElementById("hfBraintreeNonce").value = "";
braintree.dropin.create({
    authorization: "...",
    container: "#braintree-container"
}, function (err, instance) { braintreeInstance = instance; });

function initBraintreeAndSubmit() {
    if (braintreeInstance === null) {
        alert("Braintree dropin instance is not defined.");
        return;
    }
    braintreeInstance.requestPaymentMethod(function (err, payload) {
        if (err) {
            return;
        }
        document.getElementById("hfBraintreeNonce").value = payload.nonce;
        document.getElementById("btnSubmit").click();
    });
}
rheldt commented 4 years ago

... also in drop-in UI version 1.22.1.

crookedneighbor commented 4 years ago

Thanks for the info. I'll take another look at this soon.

a-bormark commented 4 years ago

Please fix this finally!!!

crookedneighbor commented 4 years ago

hi @aleks-b, this is something we're planning to sort out for the upcoming v2 version of Drop-in.

As always, the repo is open source, and if you'd like to poke around and tackle whatever race condition is causing this bug, I'd be happy to review a PR and get this out sooner than that.

mazenkourouche commented 4 years ago

I have also been experiencing a similar issue in 1.22.1 where calling instance.requestPaymentMethod causes this error Unhandled Promise Rejection: SyntaxError: The string did not match the expected pattern.

If it helps, this is the more detailed error showing in chrome Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'Window': Invalid target origin 'null' in a call to 'postMessage'. at t.reply.t.reply (https://assets.braintreegateway.com/web/3.58.0/html/hosted-fields-frame.min.html:1:19254) at https://assets.braintreegateway.com/web/3.58.0/html/hosted-fields-frame.min.html:1:85220

Don't know if anyone experiences this but I can't process any payments with this

robertcbit commented 3 years ago

I know this is an old thread, but if I'm going to start using BrainTree API I need this issue resolved. I'm experiencing the same issue. I'm using: dropin.js - version 1.25.0 I followed the instructions here -> https://developers.braintreepayments.com/start/tutorial-drop-in-node It worked the first few times and suddenly stopped rendering in about a 15 minute window of my very first tests. Getting error: braintreeclient.html:40 Uncaught TypeError: Cannot read property 'requestPaymentMethod' of undefined at HTMLButtonElement. (braintreeclient.html:40)

Tried clearing cache in Chrome, no luck then tried another browser, same result.

Has this issue been resolved?  is the a newer version of dropin.js that is not yet documented?
crookedneighbor commented 3 years ago

Looks to me like that example is not handling any errors in the create process. I'd add:

braintree.dropin.create({
    // Insert your tokenization key here
    authorization: '<use_your_tokenization_key>',
    container: '#dropin-container'
  }, function (createErr, instance) {

    if (createErr) {
      console.log('something went wrong');
      console.log(createErr);
      return;
    }

    button.addEventListener('click', function () {

To debug and make sure that Drop-in was set up succesfully.

armandodlvr commented 1 year ago

closing for inactivity. If you continue to encounter errors, please contact Support

Nhollas commented 1 year ago

1 year later, @armandodlvr @crookedneighbor

https://codesandbox.io/p/sandbox/braintree-change-payment-method-zkf2h5?file=%2Fcomponents%2FBraintree.tsx%3A12%2C5

Open and close the dropin a couple of times in this Sandbox to see this damn error :(

Any idea on a fix bro? 👍