diff --git a/commerce_braintree.module b/commerce_braintree.module
index b9f06e3..63602f7 100644
--- a/commerce_braintree.module
+++ b/commerce_braintree.module
@@ -410,10 +410,9 @@ function commerce_braintree_commerce_checkout_pane_info_alter(&$checkout_panes)
 function _commerce_braintree_commerce_payment_checkout_custom_validation($form, &$form_state, $checkout_pane, $order) {
   if (isset($form_state['values']['commerce_payment']['payment_details']['cardonfile'])) {
     commerce_braintree_payment_session_save($order->order_id, $form_state['values']['commerce_payment']['payment_details']['cardonfile']);
-    return TRUE;
   }
-  // Do nothing, but still return TRUE.
-  return TRUE;
+  // Execute the default validation callback for payment panes.
+  return commerce_payment_pane_checkout_form_validate($form, $form_state, $checkout_pane, $order);
 }
 
 /**
diff --git a/modules/commerce_braintree_dropin/commerce_braintree_dropin.info b/modules/commerce_braintree_dropin/commerce_braintree_dropin.info
new file mode 100644
index 0000000..20bac50
--- /dev/null
+++ b/modules/commerce_braintree_dropin/commerce_braintree_dropin.info
@@ -0,0 +1,6 @@
+name = Braintree (Drop-in UI)
+description = Integrates Braintree Drop-in UI for on-site credit card payment.
+package = Commerce (contrib)
+core = 7.x
+dependencies[] = commerce_braintree
+
diff --git a/modules/commerce_braintree_dropin/commerce_braintree_dropin.module b/modules/commerce_braintree_dropin/commerce_braintree_dropin.module
new file mode 100644
index 0000000..46b2a71
--- /dev/null
+++ b/modules/commerce_braintree_dropin/commerce_braintree_dropin.module
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * @file
+ * Provides integration with Braintree Drop-in UI.
+ */
+
+/**
+ * Implements hook_commerce_payment_method_info().
+ */
+function commerce_braintree_dropin_commerce_payment_method_info() {
+  $payment_methods = array();
+
+  $payment_methods['braintree_dropin'] = array(
+    'base' => 'commerce_braintree_dropin',
+    'title' => t('Braintree Drop-in UI'),
+    'short_title' => t('Braintree Drop-in UI'),
+    'display_title' => t('Credit card'),
+    'description' => t('Integrates with Braintree Drop-in for secure on-site credit card payment.'),
+    'terminal' => FALSE,
+    'offsite' => FALSE,
+  );
+
+  return $payment_methods;
+}
+
+/**
+ * Payment method callback: Braintree Drop-in UI settings form.
+ *
+ * @see CALLBACK_commerce_payment_method_settings_form().
+ */
+function commerce_braintree_dropin_settings_form($settings = array()) {
+  // Reuse the transparent redirect settings form.
+  $form = commerce_braintree_tr_settings_form($settings);
+
+  // Remove card on file as an option until that feature is added.
+  unset($form['cardonfile']);
+  return $form;
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function commerce_braintree_dropin_form_alter(&$form, &$form_state, $form_id) {
+  // Use form alter to make sure the external javsacript is always loaded.
+  // Attaching in the payment method submit form isn't consistent.
+  if (strstr($form_id, 'commerce_checkout_form')) {
+    // Make sure the Drop-in javascript api is included.
+    $form['#attached']['js'][] = array(
+      'data' => '//js.braintreegateway.com/v2/braintree.js',
+      'type' => 'external',
+    );
+  }
+}
+
+/**
+ * Form callback for Braintree Drop-in payment method.
+ *
+ * @see CALLBACK_commerce_payment_method_submit_form().
+ */
+function commerce_braintree_dropin_submit_form($payment_method, $pane_values, $checkout_pane, $order) {
+  $form = array();
+
+  // Initialize Braintree and create a token.
+  commerce_braintree_initialize($payment_method);
+  $token = Braintree_ClientToken::generate(array());
+
+  // The custom token is required to generate the Drop-in payment form.
+  $form['#attached']['js'][] = array(
+    'data' => array('commerceBraintreeDropinToken' => $token),
+    'type' => 'setting',
+  );
+
+  // Make sure the Drop-in javascript api is included.
+  $form['#attached']['js'][] = array(
+    'data' => '//js.braintreegateway.com/v2/braintree.js',
+    'type' => 'external',
+  );
+
+  // Attach our own javascript to handle the Drop-in form generation.
+  $form['#attached']['js'][] = drupal_get_path('module', 'commerce_braintree_dropin') . '/js/commerce_braintree_dropin.js';
+
+  // Inlcude a container div for the Drop-in form to attach to.
+  $form['container'] = array(
+    '#markup' => '<div id="commerce-braintree-dropin-container"></div>',
+  );
+
+  return $form;
+}
+
+/**
+ * Validation callback for the Braintree Drop-in payment method.
+ *
+ * @see CALLBACK_commerce_payment_method_submit_form_validate().
+ */
+function commerce_braintree_dropin_submit_form_validate($payment_method, $pane_form, &$pane_values, $order, $form_parents = array()) {
+  // Make sure we have a valid nonce (sale token) returned from Braintree.
+  $nonce = commerce_braintree_dropin_get_nonce();
+  if (empty($nonce)) {
+    form_set_error('continer', t('We were unable to charge your card at this time.'));
+    return FALSE;
+  }
+}
+
+/**
+ * Submit callback for the Braintree Drop-in payment method.
+ *
+ * @see CALLBACK_commerce_payment_method_submit_form_submit().
+ */
+function commerce_braintree_dropin_submit_form_submit($payment_method, $pane_form, $pane_values, $order, $form_parents = array()) {
+  $nonce = commerce_braintree_dropin_get_nonce();
+  commerce_braintree_initialize($payment_method);
+
+  list($amount, $customer_name, $country, $thoroughfare, $locality, $postal_code, $administrative_area, $customer_mail) = _commerce_braintree_get_transaction_informations($order);
+
+  $balance = commerce_payment_order_balance($order);
+  $charge = commerce_braintree_price_amount($balance['amount'], $balance['currency_code']);
+
+  $sale_data = array(
+    'amount' => $charge,
+    'paymentMethodNonce' => $nonce,
+    'customer' => array(
+      'firstName' => $customer_name,
+      'email' => $customer_mail,
+    ),
+    'billing' => array(
+      'countryCodeAlpha2' => $country,
+      'streetAddress' => $thoroughfare,
+      'firstName' => $customer_name,
+      'locality' => $locality,
+      'postalCode' => $postal_code,
+      'region' => $administrative_area,
+    ),
+    'options' => array(
+      'storeInVault' => TRUE,
+      'submitForSettlement' => TRUE,
+    ),
+  );
+
+  // Allow other modules to alter the sale before sending it to Braintree.
+  drupal_alter('commerce_braintree_dropin_sale_data', $sale_data, $order);
+
+  // Execute the API sale method to create a sale object.
+  $response = Braintree_Transaction::sale($sale_data);
+
+  // Process the Braintree response and create a payment transaction.
+  $transaction = commerce_braintree_dropin_process_transaction($order, $payment_method, $balance, $response);
+
+  // Set a form error if the payment transaction failed for any reason.
+  if (empty($transaction->status) || $transaction->status == COMMERCE_PAYMENT_STATUS_FAILURE) {
+    form_set_error(NULL, t('Your payment transaction cannot be processed at this time. @response', array('@response' => $transaction->message)));
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+/**
+ * Save a commerce_payment_transaction object from the drop-in response.
+ */
+function commerce_braintree_dropin_process_transaction($order, $payment_method, $charge, $response) {
+  $attributes = !empty($response->transaction->_attributes) ? $response->transaction->_attributes : array();
+  $transaction = commerce_payment_transaction_new('braintree_dropin', $order->order_id);
+  $transaction->instance_id = $payment_method['instance_id'];
+  $transaction->remote_id = !empty($attributes['id']) ? $attributes['id'] : NULL;
+  $transaction->amount = $charge['amount'];
+  $transaction->currency_code = $charge['currency_code'];
+  $transaction->payload[REQUEST_TIME] = $response;
+  $transaction->status = (!empty($response->success) && $response->success == TRUE) ? COMMERCE_PAYMENT_STATUS_SUCCESS : COMMERCE_PAYMENT_STATUS_FAILURE;
+  $transaction->remote_status = !empty($attributes['status']) ? $attributes['status'] : NULL;
+  $transaction->message = !empty($attributes['processorResponseText']) ? $attributes['processorResponseText'] : NULL;
+  commerce_payment_transaction_save($transaction);
+  return $transaction;
+}
+
+/**
+ * Gets the payment_method_nonce from the post variables if it exists.
+ */
+function commerce_braintree_dropin_get_nonce() {
+  return !empty($_POST['payment_method_nonce']) ? check_plain($_POST['payment_method_nonce']) : NULL;
+}
diff --git a/modules/commerce_braintree_dropin/js/commerce_braintree_dropin.js b/modules/commerce_braintree_dropin/js/commerce_braintree_dropin.js
new file mode 100644
index 0000000..9f4ac9a
--- /dev/null
+++ b/modules/commerce_braintree_dropin/js/commerce_braintree_dropin.js
@@ -0,0 +1,40 @@
+(function($) {
+  Drupal.behaviors.commerceBraintreeDropin = {
+    attach: function(context, settings) {
+      // Init Braintree Drop-in UI so that the form is insterted into the DOM.
+      if ($('#commerce-braintree-dropin-container', context).length > 0 && Drupal.settings.commerceBraintreeDropinToken != undefined && braintree != undefined) {
+        $('#commerce-braintree-dropin-container', context).once('braintreeSetup', function() {
+          braintree.setup(
+          Drupal.settings.commerceBraintreeDropinToken,
+          'dropin', {
+            container: 'commerce-braintree-dropin-container'
+          });
+        });
+      }
+
+      // Form validation and the Braintree JS api prevent submitting the form
+      // multiple times. We need to remove the "undo" the event handler that
+      // commerce_checkout.js has added to the submit button.
+      var submitButton = $('.checkout-continue', context);
+      submitButton.click(function() {
+        setTimeout(function() {
+          // Show the primary submitButton button and hide the duplicate one.
+          submitButton.show();
+          $('.checkout-continue').each(function() {
+            if ($(this).attr('disabled') == true) {
+              $(this).remove();
+            }
+          })
+        }, 1);
+      });
+
+      // Braintree hijacks all submit buttons for this form. Simulate the back
+      // button to make sure back submit still works.
+      $('.checkout-cancel,.checkout-back', context).click(function(e) {
+        e.preventDefault();
+        window.history.back();
+      });
+
+    }
+  }
+})(jQuery);
