Your Checkout. Our iframes

Last year Braintree launched Drop-in UI, a pre-built, iframe-based payments form. The benefit of Drop-in is that it ships with a ready-to-go UI to can get you up and running with a secure checkout in a few lines of code. This integration is great, but there are plenty of merchants whose checkouts require a more tailored experience. To address this, we launched Hosted Fields -- an extension to our custom integration that offers merchants control of the visual experience while extending the same PCI compliance benefits (SAQ A) as Drop-in.

How does it work?

Hosted Fields is a coordinated set of iframes that collect user input to compose a credit card entry. As a merchant, you will define a set of containers (usually <div>s) for each credit card field. You’ll provide Braintree with references to these containers and the necessary iframes will be written into each of them. Each iframe contains an <input> element that handles the appropriate field; there will be one that handles credit card number, another for the security code, and so on. In code, your HTML might look like this (yes, labels work right out of the box):

<form id="my-form" method="post" action="/purchase">
  <label for="card-number-field">Card Number</label>
  <div id="card-number-field"></div>

  <label for="security-code-field">CVV</label>
  <div id="security-code-field"></div>

  <label for="expiration-date-field">Expiration Date</label>
  <div id="expiration-date-field"></div>

  <input type="submit" value="Purchase" />
</form>

Notice the three <div> elements on the page. Typically, these fields would be <input> elements. However, Hosted Fields uses these as containers in which to render individual <iframe> elements. Here's what the JavaScript code to run Hosted Fields looks like:

braintree.setup('YOUR-CLIENT-TOKEN', 'custom', {
  id: 'my-form',
  hostedFields: {
    number: {
      selector: '#card-number-field',
      placeholder: 'Card Number'
    },
    cvv: {
      selector: '#security-code-field',
      placeholder: 'CVV'
    },
    expirationDate: {
      selector: '#expiration-date-field',
      placeholder: 'MM/YYYY'
    }
  }
});

With this small amount of code, you can have all of your fields securely hosted on Braintree and seamlessly accept credit card payments.

But what about the look and feel?

Given that these fields are hosted on Braintree domains, how do we make sure your credit card form fits in with the CSS on the rest of your site?

The trick is that the bulk of the styling happens on the container that lives on your page. That means if your input requires a blue border, you give your container a blue border; Hosted Fields doesn't need to get involved.

We provide functionality for you to style invalid, valid, and focused states. However, there are some styles that can't be applied to the container via CSS alone; these include mostly font-related properties including size, family, and color. Although, we do allow you to send in these styles when initializing Hosted Fields. Here's how that looks:

braintree.setup('YOUR-CLIENT-TOKEN', 'custom', {
  id: 'my-form',
  hostedFields: {
    number: {
      selector: '#card-number-field',
      placeholder: 'Card Number'
    },
    // ...
    styles: {
      'input': {
        'color': '#1A41A4',
        'font-family': 'Helvetica, sans-serif',
        'font-size': '16pt'
      },
      'input.invalid': {
        'color': 'red'
      }
    }
  }
});

The styling API looks very similar to CSS. This was intentional -- we wanted to make this as smooth as possible.

How is this seamless?

A core goal of Hosted Fields is to provide a powerful and secure checkout solution while minimizing our footprint. The smaller the footprint, the less likely we are to distort your intended checkout experience. Given that Hosted Fields will run in a myriad of DOM environments, we made the decision to avoid handling layout. Not just because layout as a third-party integration is hard (it is), but because we knew the better solution would be to build a product that defers the mechanics of layout to your stylesheets where it belongs. We decided to treat the container element as the input itself. This allows you to style this container to match the rest of your UI and leaves layout completely up to you!

Inside of each container, we write an iframe which renders a transparent input element without margin, padding, or borders. As the user is typing into this field, they are none the wiser that a portion of that element is an iframe. With this approach, we only have to account for a subset of CSS properties such as styling text and transitions.

Declarative styles

As a third-party integration, our code should function in such a way that your users will never realize we are there. To achieve predictability and reliability, we take great care never to infer anything on your behalf. We do not magically guess at your styles or scrape your DOM to try to match your look and feel. Browser UI is stateful, thus so is styling. The game of capture the styles is a dangerous one; every time something changes, you end up having to run your checks again. Resize, orientationchange, async DOM node injection...these are all potential causes for change in your layout and styling. This can quickly become unmanageable, and we feel there are better ways.

Input state

The domain of styling is not limited to CSS; it also encompasses representation of state. By default, Hosted Fields will only render static input states. For example, a focus event on the input will not propagate out to the iframe and then its parent container. So, unfortunately, this is not possible with Hosted Fields.

/* Static State */
input[type="text"],
.hosted-fields-frame {
  box-shadow: 0 0 2px 1px #999;
}

/* Focused State */
input[type="text"]:focus,
.hosted-fields-frame:focus {
  box-shadow: 0 0 4px 1px #777;
}

Since we have taken away this behavior, we need to supplement it somehow so that you can provide the appropriate visual response to your user. We provide you with the lowest level data possible regarding user actions. We do this via a single callback function that will fire whenever state has changed within the Hosted Fields ecosystem. Along with this callback, Hosted Fields will toggle a class of braintree-hosted-fields-focus to the corresponding container element so that you can apply the :focus styling you otherwise would have with traditional input fields. So you can do this:

input[type="text"]:focus,
.braintree-hosted-fields-focus {
  box-shadow: 0 0 4px 1px #777;
}

In addition to focus, an input's states can include validity which varies by degree over time. Hosted Fields emits low-level state changes that track these properties as often as is necessary to keep your UI in sync.

Internally, when a user begins to enter a card number, several things happen. We not only check the entry for its validity, but we also determine the card type. This determination is flushed through the fields in a render cycle and any interested parties (in this case, the CVV field may need to change its validation rules) can update should they need to.

Let's assume the user enters “411” into the card number field, the representation of this state will look like:

{
  card: {
    code: {
      name: 'CVV',
      size: 3
    },
    niceType: 'Visa'
    type: 'visa'
  },
  isFocused: true,
  isPotentiallyValid: true,
  isValid: false,
  target: {
    container: div#bt-credit-card.hf-container // DOM Node
    fieldKey: 'number'
  },
  type: 'fieldStateChange'
}

By presenting you with this data, we have provided you with a way to do things you would otherwise be able to do with normal inputs and events. In the above example, we see card.type is visa, so presenting a corresponding card icon becomes trivial. Additionally, since isPotentiallyValid is true, there is no need to render any invalid UI at this point.

There are other insights you can gain from this data as well. The name of the security varies per card brand. For example, with American Express, the field is called "CID," but for Visa, it is "CVV." Using the data from the card object, you can easily update the security code label based on the card type.

var cvvLabel = document.querySelector('label[for="cvv"]');

...
onFieldEvent: function (event) {

  // Only handle events from the 'number' field
  if (event.target.fieldKey !== 'number') { return; }

  cvvLabel.innerHTML = event.card ? event.card.code.name : 'CVV';
}

What happens when an invalid entry is detected? For the input “711” (the makings of an invalid card number), the object looks like this:

{
  card: null,
  isFocused: true,
  isPotentiallyValid: false,
  isValid: false,
  target: {
    container: div#bt-credit-card.hf-container // DOM Node
    fieldKey: 'number'
  },
  type: 'fieldStateChange'
}

Notice that card is null and both isValid and isPotentiallyValid are false. At this point, it would be helpful to notify the user that bad input has been detected. We will add a class of braintree-hosted-fields-invalid to the corresponding container element so you can easily style against this scenario in your CSS.

/* Invalid State */
.braintree-hosted-fields-invalid {
  box-shadow: 0 0 4px 1px tomato;
}

Input styles

Custom styling is core to the Hosted Fields product. But, how do you securely allow the introduction of stylistic directives from a different domain? Aside from the immediate concern of protection against certain classes of XSS attack vectors, we had to consider what this API would ultimately look like.

Syntax: CSS or JavaScript?

We set out to architect a JavaScript-driven style declaration that would allow us the proper amount of control over style injection. We quickly narrowed our focus to two directions. The first would be to have developers create a <script> tag with a custom type attribute; inside of this tag we would expect vanilla CSS which we could then apply inside of our iframes. The second was to simply add configuration to our existing braintree.setup().

CSS (via a <script> tag)

Pros

  • Familiar CSS syntax
  • Less intimidating for designers
  • Most flexible for styling options

Cons

  • Configuration is now required in both in JavaScript and in a <script> tag (typically in HTML or templates)
  • Need to programmatically parse all CSS for sanitization

For this method to work, developers would have to provide something like this in their markup:

<!-- script-style declaration -->
<script type="text/braintree-css">
  .number {
    font-family: monospace;
  }
  /* ... */
</script>

JavaScript

Pros

  • Easier for us to validate input
  • Centralized configuration via braintree.setup

Cons

  • Styles must be written in camelCase since they will be applied as inline styles via JavaScript (fontSize, backgroundColor, etc.)
  • Does not allow for psuedo-classes/elements (:focus, :after)
  • Does not provide the cascade which CSS gives for free
  • Limited to targeting only the input elements

This approach would look something like this:

// JS-style declaration
braintree.setup(/* ... */, {
  // ...
  hostedFields: {
    number: {
      selector: '#number-container',
      style: {
        fontFamily: 'monospace'
      }
    }
  }
});

Sanitizing values

In either case, before blindly accepting values, we have to mitigate injection-vectors. The obvious ones are any use of url(...), expression(...), and @import. There is also the trouble of invalid CSS properties. The former is solved with some regular expressions while the latter is solved with a simple whitelist of allowed CSS properties.

Today this whitelist allows for properties such as font-size and text-decoration while preventing properties like background-image and margin from being applied. The win is that not only is the subset of CSS we have to monitor drastically reduced, but it also aids in the predictability in the rendering of our inputs.

Once we could be sure the styles are being securely read into our system, it’s time to actually apply them in the render cycle.

How to apply injected styles?

The decision to stick to a declarative API in braintree.setup led us to applying styles through DOMElement.style[property]. This has some benefits from the get-go; it automatically HTML-escapes values and provides explicit style values only to intended elements.

This worked very well in basic scenarios, but neglected to account for stateful selectors like :focus and :hover. These are common, secure, and expressive aspects to UI design. We needed something that would allow a more flexible and dynamic use of styles.

Conditional application

Using JavaScript for style injection and assignment in this way really limits what you can do. To accommodate stateful styles, we allowed for an API like this:

number: {
  style: {
    ':focus': {
      color: 'green'
    }
  }
}

But it didn't feel right doing a "kinda-CSS-but-really-not" approach. We also had to leave out, or build implementations for features like mediaqueries. The interface felt awkward and we were relying on JavaScript to re-implement CSS manually.

applyIf

Another approach we spiked out was using applyIf. With this pattern, we opened the door for developers to create their own conditional rules for styling. This would defer things like media queries and browser-specific conditions to the merchant developer.

styles: [
  // base styles
  {
    color: 'green',
    fontFamily: 'Helvetica'
  },

  // setting styles for focused elements
  {
    applyIf: function(inputElement) { return inputElement.isFocused; },
    color: 'limegreen'
  },

  // setting styles for invalid elements
  {
    applyIf: function(inputElement) { return !inputElement.isValid; },
    color: 'red'
  },

  // setting styles for specific card types
  {
    applyIf: function(inputElement) {
      return inputElement.card.type === 'mastercard';
    },
    fontFamily: 'Comic Sans'
  },

  // media queries
  {
    applyIf: function() { return window.innerWidth > 600; },
    fontSize: '16pt'
  },
  {
    applyIf: function() { return window.innerWidth <= 600; },
    fontSize: '14pt'
  },

  // whatever you dang please
  {
    applyIf: function() {
      return navigator.userAgent.indexOf('MSIE') !== -1;
    },
    fontFamily: 'Arial'
  }
]

Unfortunately, this completely abandons any semblance to CSS. The road ahead quickly began to darken. Surely adding some styles to some inputs can't be that difficult...then we had an epiphany.

Insert rules with insertRule()

After several interesting approaches, we arrived at insertRule. For a number of reasons, this was initially not on our radar, but it turned out to be just what we needed.

var styleEl = document.createElement('style');
document.head.appendChild(styleEl);
styleEl.sheet.insertRule('input:focus { color: lightgray; }');
styleEl.sheet.insertRule('@media any { input { font-family: monospace; } }');

Those of you who have followed to this point are now probably performing facepalms.

This not only allows us to accept the full spectrum of CSS, including pseudo-elements and classes, but also to support media queries. Our styles were no longer shackled to limitations of inline styles and developers could use CSS in a familiar manner. We had arrived at the API that we have today:

braintree.setup(/* ... */, {
  hostedFields: {
    styles: {
      'input': {
        'color': 'gray'
      },
      'input:focus': {
        'color': 'darkgray';
      },
      '.number': {
        'font-family': 'monospace'
      },
      '@media any': {
        'input': { /*...*/ }
      }
    }
  }
});

You can specify styles in a nearly-CSS format while using all that CSS has to offer. The exception is that we only allow a whitelisted set of properties to apply. This is for the security of the users' credit card information.

We set out to build Hosted Fields following a guiding set of principles: focus on the API, minimize our footprint, preserve developer control, and, above all, maintain PCI compliance. The product we’re delivering reflects these foundations.

A bit of postscript

The approach and backstory shared in this post serve as a glimpse into the thought process and development style at Braintree. In future posts we hope to dive deeper into not only our tech, but the "ethos" of our JavaScript. We are extremely proud of what we have built and we hope you are as excited for it as we are.

***
Kyle DeTella Kyle is a developer at Braintree in Chicago. More posts by this author

You Might Also Like