braintree / braintree-web

A suite of tools for integrating Braintree in the browser
https://developer.paypal.com/braintree/docs/start/hello-client/javascript/v3
MIT License
444 stars 134 forks source link

Hosted Fields in Lightning Web Components (Salesforce) #605

Open munchj opened 2 years ago

munchj commented 2 years ago

General information

Issue description

I opened a ticked to braintree (#3469599) but it seems they are private and not visible by the community and this could be useful for someone else.

I am trying to implement braintree hosted fields in a Lightning Web Component (Salesforce), running in a Community (Lightning Web Runtime / Lightning Locker disabled).

A minimal example is live at https://braintree-dev-developer-edition.eu44.force.com/

The issue is that the iframes are inserted outside of the component (c-braintree_payment). Braintree is inserting a slot at the proper location in the DOM (inside the div given as input) instead of putting the iframe inside. The slot is reffering to an other slot which braintree inserted in the parent element, and this slot refers to an other slot inserted in his parent, and so on. Finally at the level of the webruntime-app, braintree is inserting the iframe with the input.

Am I doing something wrong ? I saw that braintree is supporting web components starting at version 3.64.1 https://github.com/braintree/braintree-web/issues/495

What is happening might actually be what is described in the last comment of that post :

The fix was to check if the container being passed existed in the shadow DOM, and if it does, add slot elements into the container and the iframes in divs referencing those slots on the main page. This allows the iframes to communicate with each other even when inside the shadow DOM.

But in the case of Lightning Web Components, it might not be working as intended. Any help would be greatly appreciated !

braintree_payment.html

<template>
  <form data-id="payment-form" onsubmit={handleSubmit}>
    <div>
        <label for="cc-name">Cardholder Name</label>
        <div data-id="cc-name"></div>
    </div>
    <div>
        <label for="cc-number">Credit card number</label>
        <div data-id="cc-number"></div>
    </div>
    <div>
        <label for="cc-expiration">Expiration</label>
        <div data-id="cc-expiration"></div>
    </div>
    <div>
        <label for="cc-cvv">CVV</label>
        <div data-id="cc-cvv"></div>
    </div>
    <button id="pay" type="submit"><label>Pay</label></button>
</form>
</template>

braintree_payment.js

import { LightningElement } from "lwc";

export default class Braintree_payment extends LightningElement {
  paymentForm;
  braintreeClientInstance;
  braintreeHostedFieldsInstance;

  connectedCallback() {
    this.paymentForm = this.template.querySelector(`[data-id="payment-form"`);
    this.clientToken = 'sandbox_g42y39zw_348pk9cgf3bgyw2b';
    this.initializeBraintree();
  }

  initializeBraintree() {
    if (typeof braintree !== "undefined") {
      console.log('initializeBraintree');
      // eslint-disable-next-line no-undef
      braintree.client.create({ authorization: this.clientToken },
        function (clientError, clientInstance) {
          if (clientError) { console.error('client.create', clientError); return; }

          this.braintreeClientInstance = clientInstance;

          // eslint-disable-next-line no-undef
          braintree.hostedFields.create({
            client: this.braintreeClientInstance,
            fields: {
              cardholderName: {
                container: this.template.querySelector(`[data-id="cc-name"`),
                placeholder: 'Name as it appears on your card'
              },
              number: {
                container: this.template.querySelector(`[data-id="cc-number"`),
                placeholder: '4111 1111 1111 1111'
              },
              cvv: {
                container: this.template.querySelector(`[data-id="cc-cvv"`),
                placeholder: '123'
              },
              expirationDate: {
                container: this.template.querySelector(`[data-id="cc-expiration"`),
                placeholder: 'MM / YY'
              }
            }
          },
            function (hostedFieldsError, hostedFieldsInstance) {
              if (hostedFieldsError) { console.error('hostedfields.create', hostedFieldsError); return; }
              this.braintreeHostedFieldsInstance = hostedFieldsInstance;
            }.bind(this));
        }.bind(this));
    }
  }

  handleSubmit(event) {
    event.preventDefault();
    this.braintreeHostedFieldsInstance.tokenize(function (tokenizeErr, tokenizePayload) {
      if (tokenizeErr) {
        console.error('Something bad happened!', tokenizeErr);
        return;
      }
      console.log('Got nonce:', tokenizePayload.nonce);
    }.bind(this));
  }
}
jplukarski commented 2 years ago

@munchj, thanks for bringing this up to us. We are able to see the behaviour in the example you linked. We'll take a look at this and let you know what we find.