braintree / braintree-web-drop-in

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

Destroying and recreating the dropin #96

Closed NinoSkopac closed 1 year ago

NinoSkopac commented 7 years ago

I think you guys should expose a drop-in API for destroying the thing, cause as it is, we have to do it ourselves and it can be a pain, since simply emptying the container will keep the event handler bound to the purchase button, meaning the requestPaymentMethod will be called multiple times.

This is how I used to do it (which was buggy since it would trigger the requestPaymentMethod multiple times upon re-creating it):

    createDropin : function() {
        var self = this;

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

        braintree.dropin.create({
            authorization: 'xxx', // @TODO change
            selector: '#braintree-dropin',
            paypal: {
                flow: 'checkout',
                amount : this.getMoney(),
                currency: 'EUR'
            }
        }, 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

                        //self.displayResponse(err, 'bg-danger');
                        return;
                    }

                    // Send payload.nonce to your server
                    self.sendPayload(payload);
                });
            });
        });
    },

    destroyDropin : function() {
        $('#braintree-dropin').empty();
    },

Now I do it like this, which works almost perfectly:


    _submitButton : null,
    _submitButtonEventCallback : null,

    createDropin : function() {
        var self = this;

        // Braintree
        self._submitButton = document.querySelector('#braintree-submit-button');

        var dropin = braintree.dropin.create({
            authorization: 'xxx', // @TODO change
            selector: '#braintree-dropin',
            paypal: {
                flow: 'checkout',
                amount : this.getMoney(),
                currency: 'EUR'
            }
        }, function (err, dropinInstance) {
            if (err) {
                // Handle any errors that might've occurred when creating Drop-in
                Rollbar.error("Braintree drop-in creation failed.", err);
                return;
            }

            self._submitButtonEventCallback = function() {
                dropinInstance.requestPaymentMethod(function (err, payload) {
                    if (err) {
                        Rollbar.info("Braintree error in requesting payment method.", err);

                        self.displayResponse(err, 'bg-danger');
                        return;
                    }

                    // Send payload.nonce to your server
                    self.sendPayload(payload);
                });
            };

            self._submitButton.addEventListener('click', self._submitButtonEventCallback);
        });
    },

    destroyDropin : function() {
        var self = this;
        $dropin = $('#braintree-dropin');

        self._submitButton.removeEventListener('click', self._submitButtonEventCallback);
        self._submitButtonEventCallback = null;

        $dropin.empty();
    },

What I'm doing is I'm using removeEventListener to get rid of the current click handler when destroying it.

Is this a good approach? Pls don't laugh at my JS, I'm a PHP guy :D

intelliot commented 7 years ago

Thanks for asking about this!

We have a teardown() function that you should call to destroy Drop-in. Example:

var dropinInstance;

braintree.dropin.create({
    // ...
}, function (err, dropin) {
    // ...
    dropinInstance = dropin;
});

// Destroy Drop-in
dropinInstance.teardown(function(err) {
    if (err) { console.error('An error occurred during teardown:', err); }
});

We'll add documentation of this to our README.

Removing the click event listener is fine. Here's an alternative:

var dropinInstance;

braintree.dropin.create(/* ... */, function(err, dropin) {
    // ...
    dropinInstance = dropin;
});

_submitButton.addEventListener('click', function () {
    dropinInstance.requestPaymentMethod(/* ... */);
});

The click event listener here uses dropinInstance, which you can overwrite at any time.

Does this help?

NinoSkopac commented 7 years ago

Looks good, I'll use your method then.

intelliot commented 7 years ago

Great! Thanks again for pointing out this issue.

aphavichitr commented 7 years ago

I'm using braintree web drop in with Vue.js and I'm having an issue where I call the teardown method on my destroy life cycle hook (when I leave the checkout screen that has the drop in) and the teardown method gets called without producing any errors. However, when I go back to my checkout screen and reinitialize my dropin, I get the following error:

DropinError {name: "DropinError", message: "options.selector or options.container must reference an empty DOM node.", _braintreeWebError: undefined}

(warning) PayPal Checkout Integration Script already loaded on page

Is there something that I'm doing incorrectly?

crookedneighbor commented 7 years ago

@aphavichitr My guess is that Vue is removing the view from the DOM before Drop-in can succesfully remove itself. When you return to the checkout page, Vue re-inserts the view with the non-empty DOM node.

If you provide a demo app on Github that reproduces the issue, I'm happy to see if I can tweak the lifecycle hooks to work correctly with Drop-in, but if you want a quick solution, simply emptying the DOM node before calling create should do it.

var myContainer = document.getElementById('my-container');

myContainer.innerHTML = '';

braintree.dropin.create({
  authorization: myAuth,
  container: myContainer
}, callback);

The checkout.js warning is nothing to be concerned about, but I will see about silencing that warning.

aphavichitr commented 7 years ago

@crookedneighbor Thanks for the solution. I tried it and it didn't work because It turned out to be an issue with the EventBus I was using and not related to the lifecycle hooks or the drop in listeners getting tore down. I had an EventBus in my created lifecycle hook that initialized the braintree dropin after the api response from getting the client token. I didn't remove the EventBus listeners with EventBus.$off in the beforeDestroy lifecycle hook when navigating away from the page, so every time I returned to the Checkout component, it would create a new EventBus listener and therefore keep reinitializing an extra braintree dropin for the number of times I've been on the page without a refresh. I probably should avoid using the EventBus, but I haven't figured out a way to get the client token asynchronously before initializing the drop in, since I'm getting the client token and initializing the drop in in sibling components.

crookedneighbor commented 7 years ago

@aphavichitr Are you vaulting with Drop-in? If not, you could always use a tokenization key, so you don't need to fetch a client token at all.

aphavichitr commented 7 years ago

@crookedneighbor Not vaulting with drop-in. Thanks, I'll take a look at the tokenization key.

ndabAP commented 6 years ago

I have the problem with Vue.js, too. When switching fast between routes, the DOM element with id="braintree gets removed before the initialization can happen.

The teardown function won't help since the creation isn't finished, yet.

ndabAP commented 6 years ago

Repository to reproduce:

https://github.com/ndabAP/braintree-vue-routes-issue

Steps

  1. $ yarn # or npm i
  2. Place a real authorization token at src/views/BraintreeWebDropIn.vue line 8
  3. $ yarn serve # or npm run serve
  4. Go to given URL
  5. Click on Other route before three seconds passed
  6. See error message in developer console
jackellenberger commented 6 years ago

Hi @ndabAP, thanks for your example, it made reproducing the issue super easy. There are a few ways we could go about fixing this, but it seems like the best strategies might require a few breaking changes and therefore a major version bump. There are a few other major version bump changes we want to make soon, so we'll add this one to the list.

There are some less attractive patches that might do the trick, and we'll put some time into tracking those to see if there's something that can be done in the near future. As you said, currently there's no way to cancel the setup, but you should be fine to just ignore that error as a temporary workaround.

ndabAP commented 6 years ago

@jackellenberger I really appreciate your openness and the given insight in your development.

ndabAP commented 5 years ago

Any updates?

jackellenberger commented 5 years ago

Hey @ndabAP, we played around with patching this in the current major version and weren't really happy with any of the solutions, so we're planning on fixing this issue the right way™ in the next major version. We don't have an ETA for that yet, but it is being worked on and should include a solution for this issue. Thanks for your patience!

ndabAP commented 5 years ago

Thanks a lot for your fast feedback.

armandodlvr commented 1 year ago

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