You are here

commerce_braintree_express_checkout.module in Commerce Braintree 7.3

Provides integration PayPal Express Checkout for Braintree.

File

modules/commerce_braintree_express_checkout/commerce_braintree_express_checkout.module
View source
<?php

/**
 * @file
 * Provides integration PayPal Express Checkout for Braintree.
 */
define('COMMERCE_BRAINTREE_EXPRESS_CHECKOUT_INSTANCE_ID', 'braintree_express_checkout|commerce_payment_braintree_express_checkout');

/**
 * Implements hook_library().
 */
function commerce_braintree_express_checkout_library() {
  $path = drupal_get_path('module', 'commerce_braintree_express_checkout');
  $libraries['braintree.expresscheckout'] = array(
    'title' => 'Braintree Express Checkout',
    'website' => 'https://developer.paypal.com/docs/accept-payments/express-checkout/ec-braintree-sdk/client-side/javascript/v3/',
    'version' => '3.22.2',
    'js' => array(
      'https://js.braintreegateway.com/web/3.22.2/js/client.min.js' => array(
        'type' => 'external',
      ),
      'https://js.braintreegateway.com/web/3.22.2/js/paypal-checkout.min.js' => array(
        'type' => 'external',
      ),
      'https://www.paypalobjects.com/api/checkout.js' => array(
        'type' => 'external',
      ),
      $path . '/js/commerce_braintree_express_checkout.js' => array(),
    ),
  );
  return $libraries;
}

/**
 * Implements hook_commerce_payment_method_info().
 */
function commerce_braintree_express_checkout_commerce_payment_method_info() {
  $payment_methods = array();
  $payment_methods['braintree_express_checkout'] = array(
    'base' => 'commerce_braintree_express_checkout',
    'title' => t('Braintree Express Checkout'),
    'short_title' => t('Braintree Express Checkout'),
    'display_title' => t('PayPal Express Checkout'),
    'description' => t('Integrates with PayPal Express Checkout for secure on-site credit card payment through Braintree.'),
    'callbacks' => array(
      'submit_form_validate' => 'commerce_braintree_js_form_validate',
      'submit_form_submit' => 'commerce_braintree_js_form_submit',
    ),
  );
  return $payment_methods;
}

/**
 * Returns the default settings for Express Checkout.
 */
function commerce_braintree_express_checkout_default_settings() {
  return array(
    'merchant_id' => '',
    'public_key' => '',
    'private_key' => '',
    'merchant_account_id' => '',
    'descriptor_name' => '',
    'environment' => 'sandbox',
    'submit_for_settlement' => TRUE,
    'show_on_cart' => TRUE,
    'update_billing_profiles' => TRUE,
    'update_shipping_profiles' => TRUE,
  );
}

/**
 * Payment method callback: Braintree Express Checkout settings.
 *
 * @see CALLBACK_commerce_payment_method_settings_form()
 */
function commerce_braintree_express_checkout_settings_form($settings = array()) {
  $settings = $settings + commerce_braintree_express_checkout_default_settings();

  // Reuse the transparent redirect settings form.
  $form = commerce_braintree_settings_form($settings);
  $form['show_on_cart'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enable Express Checkout on cart page'),
    '#description' => t('Show PayPal Express Checkout button on the shopping cart page (/cart)'),
    '#default_value' => $settings['show_on_cart'],
  );
  $form['update_billing_profiles'] = array(
    '#type' => 'checkbox',
    '#title' => t('Update billing customer profiles with address information the customer enters at PayPal.'),
    '#default_value' => $settings['update_billing_profiles'],
  );
  if (module_exists('commerce_shipping')) {
    $form['update_shipping_profiles'] = array(
      '#type' => 'checkbox',
      '#title' => t('Update shipping customer profiles with address information the customer enters at PayPal.'),
      '#default_value' => $settings['update_shipping_profiles'],
    );
  }
  return $form;
}

/**
 * Implements hook_form_alter().
 */
function commerce_braintree_express_checkout_form_alter(&$form, &$form_state, $form_id) {
  if (strpos($form_id, 'commerce_checkout_form') === 0 && !empty($form['commerce_payment']) && !empty($form['buttons'])) {
    $order = $form_state['order'];

    // Only alter the form if express checkout is enabled and
    // available to this order.
    if (!_commerce_braintree_express_checkout_enabled($order)) {
      return;
    }
    $ec_payment_method = commerce_payment_method_instance_load(COMMERCE_BRAINTREE_EXPRESS_CHECKOUT_INSTANCE_ID);
    if (!empty($order->data['commerce_braintree_express_checkout']['nonce'])) {
      $nonce = $order->data['commerce_braintree_express_checkout']['nonce'];

      // If the order contains a valid nonce alter the payment form
      // to set Express Checkout as the payment option.
      if (commerce_braintree_express_checkout_validate_nonce($nonce)) {

        // Update the payment method label to show the PayPal account.
        $label = t('Pay with your PayPal account (@mail).', array(
          '@mail' => $order->data['commerce_braintree_express_checkout']['details']['email'],
        ));

        // Remove other payment options since the customer has already
        // selected to use Express Checkout payments.
        $form['commerce_payment']['payment_method']['#options'] = array(
          COMMERCE_BRAINTREE_EXPRESS_CHECKOUT_INSTANCE_ID => $label,
        );
      }
    }

    // Add the Express Checkout button to all payment check pages.
    $form['buttons'] += commerce_braintree_express_checkout_button_form($order, $ec_payment_method, array(
      'form_id' => $form_id,
    ));
  }
}

/**
 * Form callback for Braintree Express Checkout payment method.
 *
 * @see CALLBACK_commerce_payment_method_submit_form().
 */
function commerce_braintree_express_checkout_submit_form($payment_method, $pane_values, $checkout_pane, $order) {
  $nonce = FALSE;
  if (!empty($order->data['commerce_braintree_express_checkout']['nonce'])) {

    // Unset the nonce if it's no longer valid.
    if (commerce_braintree_express_checkout_validate_nonce($order->data['commerce_braintree_express_checkout']['nonce'])) {
      $nonce = $order->data['commerce_braintree_express_checkout']['nonce'];
      $form['cancel'] = array(
        '#type' => 'submit',
        '#value' => t('Use another payment method'),
        '#element_validate' => array(),
        '#submit' => array(
          'commerce_braintree_express_checkout_cancel',
        ),
      );
    }
  }
  $form['nonce'] = array(
    '#type' => 'hidden',
    '#default_value' => $nonce,
  );
  return $form;
}

/**
 * Submit callback to cancel Express Checkout.
 */
function commerce_braintree_express_checkout_cancel($form, &$form_state) {
  if (!empty($form_state['order'])) {
    $order = $form_state['order'];
  }
  else {
    global $user;
    $order = commerce_cart_order_load($user->uid);
  }

  // Remove the Express Checkout data from the order and save it.
  if (!empty($order->data['commerce_braintree_express_checkout'])) {
    unset($order->data['commerce_braintree_express_checkout']);
    commerce_order_save($order);
    drupal_set_message(t('The PayPal account associated with this order will no longer be used.'));
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function commerce_braintree_express_checkout_form_views_form_commerce_cart_form_default_alter(&$form, &$form_state, $form_id) {
  if (!empty($form_state['build_info']['args'][0]->result)) {

    // This hook executes before commerce_line_item adds the order
    // object to form state, so we'll load it the same way.
    $view = $form_state['build_info']['args'][0];
    $order = commerce_order_load($view->argument['order_id']->value[0]);

    // Verify that Express Checkout is available for this order.
    if (_commerce_braintree_express_checkout_enabled($order)) {
      $payment_method = commerce_payment_method_instance_load(COMMERCE_BRAINTREE_EXPRESS_CHECKOUT_INSTANCE_ID);

      // Determine if we should show Express Checkout on /cart.
      if (!empty($payment_method['settings']['show_on_cart'])) {

        // Add the Express Checkout to the cart form.
        $form['actions'] += commerce_braintree_express_checkout_button_form($order, $payment_method, array(
          'form_id' => $form_id,
        ));

        // Append our submit handler to form.
        $form['#submit'][] = 'commerce_braintree_express_checkout_submit';
      }
    }
  }
}

/**
 * Validates that a nonce is still active and has not been consumed.
 *
 * @param $nonce
 *   The Braintree nonce.
 * @return bool
 *   TRUE if the nonce is still valid.
 */
function commerce_braintree_express_checkout_validate_nonce($nonce) {

  // Utilize static caching to prevent extraneous API calls.
  $valid =& drupal_static(__FUNCTION__);
  if (!isset($valid)) {
    $payment_method = commerce_payment_method_instance_load(COMMERCE_BRAINTREE_EXPRESS_CHECKOUT_INSTANCE_ID);
    commerce_braintree_initialize($payment_method);
    $result = Braintree_PaymentMethodNonce::find($nonce);

    // The nonce is valid if a response is returned and it has not been
    // previously consumed.
    $valid = !empty($result) && isset($result->consumed) && $result->consumed == FALSE;
  }
  return $valid;
}

/**
 * Builds the Express Checkout form elements.
 *
 * Form elements are built here are used on the cart and checkout forms.
 *
 * @param $order
 * @param $payment_method
 * @param $context
 * @return array
 */
function commerce_braintree_express_checkout_button_form($order, $payment_method, $context) {
  $form = array();

  // Make sure the payment method is available.
  if (empty($payment_method)) {
    return $form;
  }
  commerce_braintree_initialize($payment_method);
  $form['#attached']['library'][] = array(
    'commerce_braintree_express_checkout',
    'braintree.expresscheckout',
  );
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $amount = $order_wrapper->commerce_order_total
    ->value();

  // Determine the correct submit handler for this form.
  if (!empty($context['form_id'])) {
    if (strpos($context['form_id'], 'commerce_checkout_form') === 0) {
      $submit_selector = '.checkout-continue';
    }
    if ($context['form_id'] == 'views_form_commerce_cart_form_default') {
      $submit_selector = '#edit-checkout';
    }
  }

  // Set up an alterable array of JS settings that will
  // be used in js/commerce_braintree_express_checkout.js.
  $js_settings = array(
    'submitSelector' => !empty($submit_selector) ? $submit_selector : '[name=op]',
    'drupalForm' => !empty($context['form_id']) ? $context['form_id'] : '',
    'environment' => $payment_method['settings']['environment'],
    'orderStatus' => $order->status,
    'buttonId' => 'commerce-braintree-express-checkout-button',
    'nonceSelector' => 'input[name="commerce_payment[payment_details][nonce]"]',
    'payloadInput' => 'commerce_braintree_payload',
    'instanceId' => COMMERCE_BRAINTREE_EXPRESS_CHECKOUT_INSTANCE_ID,
    'buttonStyle' => array(
      'size' => 'small',
      'color' => 'gold',
      'shape' => 'pill',
      'label' => 'checkout',
    ),
    'options' => array(
      'authorization' => Braintree_ClientToken::generate(),
    ),
    'createPaymentOptions' => array(
      'flow' => 'checkout',
      'amount' => commerce_currency_amount_to_decimal($amount['amount'], $amount['currency_code']),
      'currency' => $amount['currency_code'],
      'enableShippingAddress' => TRUE,
    ),
  );

  // If a shipping address exists on the order, pass it to PayPal.
  if (module_exists('commerce_shipping') && !empty($order->commerce_customer_shipping)) {
    try {
      $address = $order_wrapper->commerce_customer_shipping->commerce_customer_address
        ->value();
      $js_settings['createPaymentOptions'] += array(
        'shippingAddressEditable' => FALSE,
        'shippingAddressOverride' => array(
          'recipientName' => $address['name_line'],
          'line1' => $address['thoroughfare'],
          'line2' => $address['premise'],
          'city' => $address['locality'],
          'countryCode' => $address['country'],
          'postalCode' => $address['postal_code'],
          'state' => $address['administrative_area'],
        ),
      );
    } catch (Exception $ex) {
      watchdog('commerce_braintree_express_checkout', 'Unable to access commerce customer shipping address');
    }
  }

  // Allow other modules to alter the JS settings.
  drupal_alter('commerce_braintree_express_checkout_js', $js_settings, $payment_method);

  // Add Express Checkout JS settings object.
  $form['#attached']['js'][] = array(
    'data' => array(
      'commerceBraintreeExpressCheckout' => $js_settings,
    ),
    'type' => 'setting',
  );

  // Create a DOM element for the EC button.
  $form['commerce_braintree_ec_button'] = array(
    '#markup' => '<div id="' . $js_settings['buttonId'] . '"></div>',
    '#weight' => !empty($context['form_id']) && $context['form_id'] == 'views_form_commerce_cart_form_default' ? 10 : -1,
  );
  $form['commerce_braintree_payload'] = array(
    '#type' => 'hidden',
  );
  return $form;
}

/**
 * Form submit handler for Express Checkout payload submission.
 */
function commerce_braintree_express_checkout_submit($form, &$form_state) {
  $order = $form_state['order'];
  $payment_method = commerce_payment_method_instance_load(COMMERCE_BRAINTREE_EXPRESS_CHECKOUT_INSTANCE_ID);

  // If a payload was provided, extract it and grab the nonce.
  if (!empty($form_state['values']['commerce_braintree_payload'])) {
    $payload = json_decode($form_state['values']['commerce_braintree_payload'], TRUE);
    if (empty($payload['nonce'])) {
      watchdog('commerce_braintree_express_checkout', 'Payload submitted without a nonce.', array(), WATCHDOG_ERROR);
      return;
    }

    // Append the payload data to the order object.
    $order->data['commerce_braintree_express_checkout'] = $payload;
    if (empty($order->mail)) {
      $order->mail = $payload['details']['email'];
    }

    // Force the payment method on the order to be Express Checkout.
    $order->data['payment_method'] = COMMERCE_BRAINTREE_EXPRESS_CHECKOUT_INSTANCE_ID;

    // Create a billing information profile for the order with the available info.
    if (!empty($payment_method['settings']['update_billing_profiles'])) {
      commerce_braintree_express_checkout_create_customer_profile($order, 'billing', $payload, TRUE);
    }

    // If the shipping module exists on the site, create a shipping information
    // profile for the order with the available info.
    if (module_exists('commerce_shipping') && !empty($payment_method['settings']['update_shipping_profiles'])) {
      commerce_braintree_express_checkout_create_customer_profile($order, 'shipping', $payload, TRUE);
    }

    // Save the order updates.
    commerce_order_save($order);
  }
}

/**
 * Is Express Checkout enabled or allowed for this order?
 *
 * @param null $order
 *   An optional commerce order object.
 *
 * @return bool
 *   TRUE if the payment method is available.
 */
function _commerce_braintree_express_checkout_enabled($order = NULL) {
  if (!empty($order)) {
    $order->payment_methods = array();
    rules_invoke_all('commerce_payment_methods', $order);
    uasort($order->payment_methods, 'drupal_sort_weight');
    return in_array(COMMERCE_BRAINTREE_EXPRESS_CHECKOUT_INSTANCE_ID, array_keys($order->payment_methods));
  }
  $rule = rules_config_load('commerce_payment_braintree_express_checkout');
  return !empty($rule->active);
}

/**
 * Creates or updates a customer profile for an order based on information
 * obtained from PayPal after an Express Checkout.
 *
 * @param $order
 *   The order that was paid via Express Checkout.
 * @param $profile_type
 *   The type of the customer profile that should be created or updated.
 * @param $payload
 *   The payload array from a Express Checkout payload.
 * @param $skip_save
 *   Boolean indicating whether or not this function should skip saving the
 *   order after setting it to reference the newly created customer profile;
 *   defaults to TRUE, requiring the caller to save the order.
 */
function commerce_braintree_express_checkout_create_customer_profile($order, $profile_type, $payload, $skip_save = TRUE) {

  // First check if the order already references a customer profile of the
  // specified type.
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $field_name = variable_get('commerce_customer_profile_' . $profile_type . '_field', '');

  // If the associated order field has been set and the order currently
  // references a customer profile through it...
  if (!empty($field_name) && !empty($order_wrapper->{$field_name})) {

    // Update the existing customer profile.
    $profile = $order_wrapper->{$field_name}
      ->value();
  }
  elseif (!empty($order->data['profiles']['customer_profile_' . $profile_type])) {

    // Otherwise look for an association stored in the order's data array.
    $profile = commerce_customer_profile_load($order->data['profiles']['customer_profile_' . $profile_type]);
  }

  // Create a new profile if we could not find an existing one.
  if (empty($profile)) {
    $profile = commerce_customer_profile_new($profile_type, $order->uid);
  }

  // Add the order context to the profile to ensure it can be updated without
  // resulting in customer profile duplication.
  $profile->entity_context = array(
    'entity_type' => 'commerce_order',
    'entity_id' => $order->order_id,
  );

  // Prepare an addressfield array to set to the customer profile.
  $field = field_info_field('commerce_customer_address');
  $instance = field_info_instance('commerce_customer_profile', 'commerce_customer_address', $profile_type);
  $address = addressfield_default_values($field, $instance);

  // Handle setting the name fields for billing profiles.
  if ($profile_type == 'billing') {
    $address['first_name'] = $payload['details']['firstName'];
    $address['last_name'] = $payload['details']['lastName'];
    $address['name_line'] = $address['first_name'] . ' ' . $address['last_name'];
  }

  // Handle setting the name and address for shipping profiles.
  if ($profile_type == 'shipping' && !empty($payload['details']['shippingAddress'])) {
    $address_values = $payload['details']['shippingAddress'];

    // The recipient name is returned as one field. Attempt
    // to split the first and last names for address field.
    $names = explode(' ', $address_values['recipientName']);
    $address['first_name'] = array_shift($names);
    $address['last_name'] = implode(' ', $names);
    $address['name_line'] = $address_values['recipientName'];
    $address = array_merge($address, array(
      'country' => !empty($address_values['countryCode']) ? $address_values['countryCode'] : '',
      'administrative_area' => !empty($address_values['state']) ? $address_values['state'] : '',
      'locality' => !empty($address_values['city']) ? $address_values['city'] : '',
      'postal_code' => !empty($address_values['postalCode']) ? $address_values['postalCode'] : '',
      'thoroughfare' => !empty($address_values['line1']) ? $address_values['line1'] : '',
      'premise' => !empty($address_values['line2']) ? $address_values['line2'] : '',
    ));
  }

  // Add the addressfield value to the customer profile.
  $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
  $profile_wrapper->commerce_customer_address = $address;

  // Save the customer profile and update the order to reference it.
  $profile_wrapper
    ->save();
  $order_wrapper->{'commerce_customer_' . $profile_type} = $profile_wrapper;

  // Save the order if specified.
  if (!$skip_save) {
    $order_wrapper
      ->save();
  }
}

Functions

Namesort descending Description
commerce_braintree_express_checkout_button_form Builds the Express Checkout form elements.
commerce_braintree_express_checkout_cancel Submit callback to cancel Express Checkout.
commerce_braintree_express_checkout_commerce_payment_method_info Implements hook_commerce_payment_method_info().
commerce_braintree_express_checkout_create_customer_profile Creates or updates a customer profile for an order based on information obtained from PayPal after an Express Checkout.
commerce_braintree_express_checkout_default_settings Returns the default settings for Express Checkout.
commerce_braintree_express_checkout_form_alter Implements hook_form_alter().
commerce_braintree_express_checkout_form_views_form_commerce_cart_form_default_alter Implements hook_form_FORM_ID_alter().
commerce_braintree_express_checkout_library Implements hook_library().
commerce_braintree_express_checkout_settings_form Payment method callback: Braintree Express Checkout settings.
commerce_braintree_express_checkout_submit Form submit handler for Express Checkout payload submission.
commerce_braintree_express_checkout_submit_form Form callback for Braintree Express Checkout payment method.
commerce_braintree_express_checkout_validate_nonce Validates that a nonce is still active and has not been consumed.
_commerce_braintree_express_checkout_enabled Is Express Checkout enabled or allowed for this order?

Constants

Namesort descending Description
COMMERCE_BRAINTREE_EXPRESS_CHECKOUT_INSTANCE_ID @file Provides integration PayPal Express Checkout for Braintree.