commerce_stripe.module in Commerce Stripe 7
Same filename and directory in other branches
This module provides Stripe (http://stripe.com/) payment gateway integration to Commerce. Commerce Stripe offers a PCI-compliant way to process payments straight from you Commerce shop.
File
commerce_stripe.moduleView source
<?php
/**
* @file
* This module provides Stripe (http://stripe.com/) payment gateway integration
* to Commerce. Commerce Stripe offers a PCI-compliant way to process payments
* straight from you Commerce shop.
*/
define('STRIPE_DEFAULT_INTEGRATION', 'stripejs');
/**
* Implements hook_menu().
*/
function commerce_stripe_menu() {
$items = array();
// Add a menu item to stripe payment transactions that can be refunded.
$items['admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/commerce-stripe-refund'] = array(
'title' => 'Refund',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'commerce_stripe_refund_form',
3,
5,
),
'access callback' => 'commerce_stripe_return_access',
'access arguments' => array(
3,
5,
),
'type' => MENU_DEFAULT_LOCAL_TASK,
'context' => MENU_CONTEXT_INLINE,
'weight' => 1,
'file' => 'includes/commerce_stripe.admin.inc',
);
return $items;
}
/**
* Implements hook_libraries_info().
*/
function commerce_stripe_libraries_info() {
return array(
'stripe-php' => array(
'name' => 'Stripe API Client Library for PHP',
'vendor url' => 'https://stripe.com/',
'download url' => 'https://github.com/stripe/stripe-php',
'dependencies' => array(),
'version arguments' => array(
'file' => 'VERSION',
'pattern' => '/(\\d+\\.\\d+\\.\\d+)/',
),
'files' => array(
'php' => array(
'lib/Stripe.php',
),
),
),
);
}
/**
* Implements hook_commerce_payment_method_info().
*/
function commerce_stripe_commerce_payment_method_info() {
$payment_methods = array();
// The payment method info depends on the stripe integration method (stripe.js
// or Stripe Checkout) the administrator has selected. We need to load
// the rules action to determine which integration method is selected.
// Note that this code is intentionally duplicated to avoid the infinite
// recursion that would occur if we called _commerce_stripe_load_settings().
// @todo: Explore using a static variable to avoid that recursion.
$stripe_integration = STRIPE_DEFAULT_INTEGRATION;
$cardonfile = FALSE;
$payment_method_rule = rules_config_load('commerce_payment_commerce_stripe');
if ($payment_method_rule && $payment_method_rule->active) {
foreach ($payment_method_rule
->actions() as $action) {
// Skip any actions that are not simple rules actions. (i.e. loops)
if (!$action instanceof RulesAction) {
continue;
}
if (!empty($action->settings['payment_method']['method_id']) && $action->settings['payment_method']['method_id'] == 'commerce_stripe') {
// Default to Stripe.js if no integration_type has been chosen.
if (!empty($action->settings['payment_method']['settings']['integration_type'])) {
$stripe_integration = $action->settings['payment_method']['settings']['integration_type'];
}
$cardonfile = !empty($action->settings['payment_method']['settings']['cardonfile']) ? TRUE : FALSE;
break;
}
}
}
$payment_methods['commerce_stripe'] = array(
'title' => t('Stripe'),
'short_title' => t('Stripe'),
'display_title' => t('Credit card'),
'description' => t('Stripe payment gateway'),
'active' => FALSE,
'terminal' => FALSE,
'offsite' => FALSE,
);
// Set the cardonfile settings. We check that the administrator has enabled
// cardonfile functionality for commerce_stripe; if not, we do not add the
// cardonfile callbacks that would otherwise be called.
if ($cardonfile) {
// Provide general card management functionality outside of the checkout
// process.
// TODO: Write update hook to clear entity cache.
$payment_methods['commerce_stripe']['cardonfile'] = array(
'charge callback' => 'commerce_stripe_cardonfile_charge',
'delete callback' => 'commerce_stripe_cardonfile_delete',
'create form callback' => 'commerce_stripe_cardonfile_create_form',
'create callback' => 'commerce_stripe_cardonfile_create',
'update callback' => 'commerce_stripe_cardonfile_update',
);
}
return $payment_methods;
}
/**
* Access callback for processing returns.
*/
function commerce_stripe_return_access($order, $transaction) {
// Don't allow refunds on non-stripe transactions.
if ($transaction->payment_method != 'commerce_stripe') {
return FALSE;
}
// Don't allow refunds on fully refunded transactions.
if (!empty($transaction->data['stripe']['amount_refunded'])) {
if ($transaction->data['stripe']['amount_refunded'] >= $transaction->amount) {
return FALSE;
}
}
// Only allow refunds on original charge transactions.
if (!empty($transaction->data['stripe']['stripe_refund'])) {
return FALSE;
}
return commerce_payment_transaction_access('update', $transaction);
}
/**
* Payment method settings form.
*
* @param $settings
* Default settings provided from rules
*
* @return array
* Settings form array
*/
function commerce_stripe_settings_form($settings) {
$form = array();
$form['stripe_currency'] = array(
'#type' => 'select',
'#title' => t('Currency'),
'#options' => array(
'CAD' => t('CAD'),
'EUR' => t('EUR'),
'GBP' => t('GBP'),
'USD' => t('USD'),
'AUD' => t('AUD'),
'CHF' => t('CHF'),
),
'#description' => t('Select the currency that you are using.'),
'#default_value' => !empty($settings['stripe_currency']) ? $settings['stripe_currency'] : 'USD',
);
$form['secret_key'] = array(
'#type' => 'textfield',
'#title' => t('Secret Key'),
'#description' => t('Secret API Key. Get your key from https://stripe.com/'),
'#default_value' => !empty($settings['secret_key']) ? $settings['secret_key'] : '',
'#required' => TRUE,
);
$form['public_key'] = array(
'#type' => 'textfield',
'#title' => t('Publishable Key'),
'#description' => t('Publishable API Key. Get your key from https://stripe.com/'),
'#default_value' => !empty($settings['public_key']) ? $settings['public_key'] : '',
'#required' => TRUE,
);
$form['display_title'] = array(
'#type' => 'textfield',
'#title' => t('Payment method display title'),
'#description' => t('Payment method display title'),
'#default_value' => !empty($settings['display_title']) ? $settings['display_title'] : t('Stripe'),
);
$form['receipt_email'] = array(
'#type' => 'checkbox',
'#title' => t('Email receipts'),
'#description' => t('When selected, customers will receive email receipts from Stripe.'),
'#default_value' => isset($settings['receipt_email']) ? $settings['receipt_email'] : 0,
);
$form['integration_type'] = array(
'#type' => 'select',
'#title' => t('Integration type'),
'#description' => t('Choose Stripe integration method: Stripe.js makes it easy to collect credit card (and other similarly sensitive) details without having the information touch your server. Checkout is an embeddable iframe for desktop, tablet, and mobile devices.'),
'#options' => array(
'stripejs' => t('stripe.js'),
'checkout' => t('checkout'),
),
'#default_value' => !empty($settings['integration_type']) ? $settings['integration_type'] : STRIPE_DEFAULT_INTEGRATION,
);
// Stripe Checkout specific settings.
// @see: https://stripe.com/docs/checkout#integration-custom
$form['checkout_settings'] = array(
'#type' => 'fieldset',
'#title' => t('These settings are specific to "checkout" integration type.'),
'#states' => array(
'visible' => array(
':input[name$="[integration_type]"]' => array(
'value' => 'checkout',
),
),
),
);
// Highly recommended checkout options:
// Name
$form['checkout_settings']['name'] = array(
'#type' => 'textfield',
'#title' => t('Name'),
'#description' => t('The name of your company or website.'),
'#default_value' => isset($settings['checkout_settings']['name']) ? $settings['checkout_settings']['name'] : variable_get('site_name', ''),
);
// Description
$form['checkout_settings']['description'] = array(
'#type' => 'textfield',
'#title' => t('Description'),
'#description' => t('A description of the product or service being purchased.'),
'#default_value' => isset($settings['checkout_settings']['description']) ? $settings['checkout_settings']['description'] : '',
);
// Image
$form['checkout_settings']['image'] = array(
'#type' => 'managed_file',
'#title' => t('Image'),
'#progress_message' => t('Please wait...'),
'#progress_indicator' => 'bar',
'#description' => t('Click "Browse..." to select an image to upload.'),
'#required' => FALSE,
'#upload_location' => 'public://commerce_stripe/checkout_images/',
'#default_value' => isset($settings['checkout_settings']['image']) ? $settings['checkout_settings']['image']['fid'] : '',
'#element_validate' => array(
'commerce_stripe_settings_form_image_validate',
),
);
// Optional checkout options:
// panelLabel
$form['checkout_settings']['panelLabel'] = array(
'#type' => 'textfield',
'#title' => t('Payment button label'),
'#description' => t('The label of the payment button in the Checkout form (e.g. “Subscribe”, “Pay {{amount}}”, etc.). If you include {{amount}}, it will be replaced by the provided amount. Otherwise, the amount will be appended to the end of your label.'),
'#default_value' => isset($settings['checkout_settings']['panelLabel']) ? $settings['checkout_settings']['panelLabel'] : "Pay {{amount}}",
);
// zipCode
$form['checkout_settings']['zipCode'] = array(
'#type' => 'checkbox',
'#title' => t('ZIP code verification'),
'#description' => t('Specify whether Checkout should validate the billing ZIP code.'),
'#default_value' => isset($settings['checkout_settings']['zipCode']) ? $settings['checkout_settings']['zipCode'] : 0,
);
// allowRememberMe
$form['checkout_settings']['allowRememberMe'] = array(
'#type' => 'checkbox',
'#title' => t('Show "Remember Me" option'),
'#description' => t('Specify whether Checkout should allow the user to store their credit card for faster checkout.'),
'#default_value' => isset($settings['checkout_settings']['allowRememberMe']) ? $settings['checkout_settings']['allowRememberMe'] : 0,
);
// bitcoin
$form['checkout_settings']['bitcoin'] = array(
'#type' => 'checkbox',
'#title' => t('Accept Bitcoin'),
'#description' => t('When checked, Stripe Checkout will accept Bitcoin as payment.') . l(t('Must be enabled in your Stripe account.'), 'https://dashboard.stripe.com/account/bitcoin/enable'),
'#default_value' => isset($settings['checkout_settings']['bitcoin']) ? $settings['checkout_settings']['bitcoin'] : 0,
);
// billingAddress (Undocumented option)
$form['checkout_settings']['billingAddress'] = array(
'#type' => 'checkbox',
'#title' => t('Billing address'),
'#description' => t('Specify whether to enable billing address collection in Checkout.'),
'#default_value' => isset($settings['checkout_settings']['billingAddress']) ? $settings['checkout_settings']['billingAddress'] : 0,
);
// shippingAddress (Undocumented option)
$form['checkout_settings']['shippingAddress'] = array(
'#type' => 'checkbox',
'#title' => t('Shipping address'),
'#description' => t('Specify whether to enable shipping address collection in Checkout.'),
'#default_value' => isset($settings['checkout_settings']['shippingAddress']) ? $settings['checkout_settings']['shippingAddress'] : 0,
);
if (module_exists('commerce_cardonfile')) {
$form['cardonfile'] = array(
'#type' => 'checkbox',
'#title' => t('Enable Card on File functionality.'),
'#default_value' => isset($settings['cardonfile']) ? $settings['cardonfile'] : 0,
);
}
else {
$form['cardonfile'] = array(
'#type' => 'markup',
'#markup' => t('To enable Card on File funcitionality download and install the Card on File module.'),
);
}
return $form;
}
/**
* Custom validation callback for image field of stripe settings form.
* Uploaded image file is temporary by default, it'll be removed during the file garbage
* collection process. Change its status to FILE_STATUS_PERMANENT.
*
* @param array $element
* @param array $form_state
* @param array $form
*/
function commerce_stripe_settings_form_image_validate($element, &$form_state, $form) {
if (isset($element['fid']['#value'])) {
$file = file_load($element['fid']['#value']);
if (is_object($file) && $file->status !== FILE_STATUS_PERMANENT) {
$file->status = FILE_STATUS_PERMANENT;
file_save($file);
}
}
}
function _commerce_stripe_credit_card_form() {
module_load_include('inc', 'commerce_payment', 'includes/commerce_payment.credit_card');
$credit_card_fields = array(
'owner' => '',
'number' => '',
'exp_month' => '',
'exp_year' => '',
'code' => '',
);
$form = commerce_payment_credit_card_form($credit_card_fields);
// Add a css class so that we can easily identify Stripe related input fields
// Do not require the fields
//
// Remove "name" attributes from Stripe related input elements to
// prevent card data to be sent to Drupal server
// (see https://stripe.com/docs/tutorials/forms)
foreach (array_keys($credit_card_fields) as $key) {
$credit_card_field =& $form['credit_card'][$key];
$credit_card_field['#attributes']['class'][] = 'stripe';
$credit_card_field['#required'] = FALSE;
$credit_card_field['#post_render'][] = '_commerce_stripe_credit_card_field_remove_name';
}
return $form;
}
/**
* Payment method callback: checkout form.
*/
function commerce_stripe_submit_form($payment_method, $pane_values, $checkout_pane, $order) {
global $user;
$integration_type = !empty($payment_method['settings']['integration_type']) ? $payment_method['settings']['integration_type'] : STRIPE_DEFAULT_INTEGRATION;
$order_wrapper = entity_metadata_wrapper('commerce_order', $order);
$field = field_info_field('commerce_customer_address');
$instance = field_info_instance('commerce_customer_profile', 'commerce_customer_address', 'billing');
$available_countries = NULL;
if (isset($form_state['input']['country'])) {
$available_countries = array(
$form_state['input']['country'] => NULL,
);
}
// Attempt to load the billing address from the order data.
$billing_address = addressfield_default_values($field, $instance, array(
$available_countries,
));
if (!empty($order->commerce_customer_billing)) {
if (!empty($order_wrapper->commerce_customer_billing->commerce_customer_address)) {
$billing_address = $order_wrapper->commerce_customer_billing->commerce_customer_address
->value();
}
}
// Pass the billing address values to javacript so they can be included in
// the token request to Stripe.
$address = array(
'address_line1' => !empty($billing_address['thoroughfare']) ? $billing_address['thoroughfare'] : '',
'address_line2' => !empty($billing_address['premise']) ? $billing_address['premise'] : '',
'address_city' => !empty($billing_address['locality']) ? $billing_address['locality'] : '',
'address_state' => !empty($billing_address['administrative_area']) ? $billing_address['administrative_area'] : '',
'address_zip' => !empty($billing_address['postal_code']) ? $billing_address['postal_code'] : '',
'address_country' => !empty($billing_address['country']) ? $billing_address['country'] : '',
);
drupal_add_js(array(
'commerce_stripe_address' => $address,
), array(
'type' => 'setting',
));
// Differentiate form elements based on stripe integration type.
if ($integration_type == 'stripejs') {
// @todo: Use _commerce_stripe_form_add_credit_card_form().
$form = _commerce_stripe_credit_card_form();
// Include the stripe.js from stripe.com.
drupal_add_js('https://js.stripe.com/v2/', 'external');
}
elseif ($integration_type == 'checkout') {
$form = array();
// Add pay button.
$order_wrapper = entity_metadata_wrapper('commerce_order', $order);
$order_total = $order_wrapper->commerce_order_total
->value();
// Add Checkout settings.
$checkout_settings = $payment_method['settings']['checkout_settings'];
// @todo: Use _commerce_stripe_form_configure_stripe_checkout().
// Convert JS settings to Booleans before adding them to the page.
$checkout_settings = array_map(function ($value) {
if ($value === 0 or $value === 1) {
$value = (bool) $value;
}
return $value;
}, $checkout_settings);
// Get the image file if one is set.
if (isset($checkout_settings['image']['fid'])) {
$image = file_load($checkout_settings['image']['fid']);
if (is_object($image)) {
$checkout_settings['image'] = file_create_url($image->uri);
}
else {
// Empty image setting will cause a broken image to be displayed in checkout
// iframe.
unset($checkout_settings['image']);
}
}
$checkout_settings += array(
'currency' => $payment_method['settings']['stripe_currency'],
'email' => $order->mail,
'amount' => $order_total['amount'],
);
drupal_add_js(array(
'stripe' => array(
'checkout' => $checkout_settings,
),
), 'setting');
// Add external checkout.js library.
drupal_add_js('https://checkout.stripe.com/checkout.js', 'external');
}
// @todo: Use _commerce_stripe_form_configure_stripe_common().
// Add stripe token field. This field is a container for token received from
// Stripe API.
$form['stripe_token'] = array(
'#type' => 'hidden',
'#attributes' => array(
'id' => 'stripe_token',
),
'#default_value' => !empty($pane_values['stripe_token']) ? $pane_values['stripe_token'] : '',
);
// Set our key to settings array.
drupal_add_js(array(
'stripe' => array(
'publicKey' => $payment_method['settings']['public_key'],
'integration_type' => $integration_type,
),
), 'setting');
// Load commerce_stripe.js.
$form['#attached']['js'] = array(
drupal_get_path('module', 'commerce_stripe') . '/commerce_stripe.js',
);
// To display validation errors.
$form['errors'] = array(
'#type' => 'markup',
'#markup' => '<div class="payment-errors"></div>',
);
return $form;
}
function _commerce_stripe_credit_card_field_remove_name($content, $element) {
$name_pattern = '/\\sname\\s*=\\s*[\'"]?' . preg_quote($element['#name']) . '[\'"]?/';
return preg_replace($name_pattern, '', $content);
}
/**
* Payment method callback: checkout form submission.
*/
function commerce_stripe_submit_form_submit($payment_method, $pane_form, $pane_values, $order, $charge) {
// If instructed to do so, try using the specified card on file.
if (module_exists('commerce_cardonfile') && !empty($payment_method['settings']['cardonfile']) && !empty($pane_values['cardonfile']) && $pane_values['cardonfile'] !== 'new') {
$card_data = commerce_cardonfile_load($pane_values['cardonfile']);
if (empty($card_data) || $card_data->status == 0) {
drupal_set_message(t('The requested card on file is no longer valid.'), 'error');
return FALSE;
}
return commerce_stripe_cardonfile_charge($payment_method, $card_data, $order, $charge);
}
// The card is new. Either charge and forget, or charge and save.
if (!commerce_stripe_load_library()) {
drupal_set_message(t('Error making the payment. Please contact shop admin to proceed.'), 'error');
return FALSE;
}
// Begin assembling charge parameters.
Stripe::setApiKey($payment_method['settings']['secret_key']);
// Build a default description and offer modules the possibility to alter it.
$description = t('Order Number: @order_number', array(
'@order_number' => $order->order_number,
));
$currency_code = $payment_method['settings']['stripe_currency'];
if (isset($charge['currency_code'])) {
$currency_code = $charge['currency_code'];
}
$c = array(
'amount' => $charge['amount'],
'currency' => $currency_code,
'card' => $pane_values['stripe_token'],
'description' => $description,
);
// Specify that we want to send a receipt email if we are configured to do so.
if (!empty($payment_method['settings']['receipt_email'])) {
$c['receipt_email'] = $order->mail;
}
// The metadata could be added via the alter below but for compatibility
// reasons it may stay.
commerce_stripe_add_metadata($c, $order);
// Let modules alter the charge object to add attributes.
drupal_alter('commerce_stripe_order_charge', $c, $order);
// To later store the card with all required fields, carry out necessary steps before making the charge request.
if (module_exists('commerce_cardonfile') && !empty($payment_method['settings']['cardonfile']) && !empty($pane_values['credit_card']['cardonfile_store']) && $pane_values['credit_card']['cardonfile_store']) {
$card = _commerce_stripe_create_card($pane_values['stripe_token'], $order->uid, $payment_method);
// If the card is not declined or otherwise is error-free, we can save it.
if ($card && !empty($card->id)) {
$stripe_card_id = $card->id;
$stripe_customer_id = $card->customer;
$c['card'] = $stripe_card_id;
$c['customer'] = $stripe_customer_id;
$save_card = TRUE;
}
}
$transaction = commerce_payment_transaction_new('commerce_stripe', $order->order_id);
$transaction->instance_id = $payment_method['instance_id'];
$transaction->amount = $charge['amount'];
$transaction->currency_code = $currency_code;
/*
* save the transaction as pending. this will cause an exception to be thrown
* if the transaction cannot be saved. this prevents the scenario where it
* can go all the way through the try/catch below with success in stripe but
* failing to ever save the transaction. saving the transaction here also acts as
* an early catch to prevent the stripe charge from going through if the Drupal
* side will be unable to save it for some reason.
*/
$transaction->status = COMMERCE_PAYMENT_STATUS_PENDING;
if (!_commerce_stripe_commerce_payment_transaction_save($transaction)) {
return FALSE;
}
try {
// Stripe does not appreciate $0 transfers.
if ($charge['amount'] > 0) {
$response = Stripe_Charge::create($c);
$transaction->remote_id = $response->id;
$transaction->payload[REQUEST_TIME] = $response
->__toJSON();
$transaction->message = t('Payment completed successfully.');
$transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
_commerce_stripe_commerce_payment_transaction_save($transaction);
if (property_exists($response, 'card')) {
$card_response = $response->card;
}
else {
$card_response = $response->source;
}
}
else {
$card_response = $card;
}
} catch (Exception $e) {
drupal_set_message(t('We received the following error processing your card. Please enter your information again or try a different card.'), 'error');
drupal_set_message(check_plain($e
->getMessage()), 'error');
watchdog('commerce_stripe', 'Following error received when processing card @stripe_error.', array(
'@stripe_error' => $e
->getMessage(),
), WATCHDOG_NOTICE);
$transaction->remote_id = $e
->getHttpStatus();
$transaction->payload[REQUEST_TIME] = $e->jsonBody;
$transaction->message = t('Card processing error: @stripe_error', array(
'@stripe_error' => $e
->getMessage(),
));
$transaction->status = COMMERCE_PAYMENT_STATUS_FAILURE;
_commerce_stripe_commerce_payment_transaction_save($transaction);
return FALSE;
}
// If so instructed by the customer, save the card.
if (!empty($save_card)) {
_commerce_stripe_save_cardonfile($card_response, $order->uid, $payment_method, $pane_values['cardonfile_instance_default']);
}
}
/**
* attempt to save the transaction and set messages if unsuccessful
*/
function _commerce_stripe_commerce_payment_transaction_save($transaction) {
if (!commerce_payment_transaction_save($transaction)) {
drupal_set_message(t('Our site is currently unable to process your card. Please contact the site administrator to complete your transaction'), 'error');
watchdog('commerce_stripe', 'commerce_payment_transaction_save returned false in saving a stripe transaction for order_id @order_id.', array(
'@order_id' => $transaction->order_id,
), WATCHDOG_ERROR);
return FALSE;
}
else {
return TRUE;
}
}
function _commerce_stripe_create_card($stripe_token, $uid, $payment_method) {
if (!commerce_stripe_load_library()) {
return FALSE;
}
Stripe::setApiKey($payment_method['settings']['secret_key']);
// If there is no existing customer id, use the Stripe form token to create one.
$stripe_customer_id = commerce_stripe_customer_id($uid, $payment_method['instance_id']);
if (!$stripe_customer_id) {
$account = user_load($uid);
try {
$customer = Stripe_Customer::create(array(
'card' => $stripe_token,
'email' => $account->mail,
));
if (property_exists($customer, 'cards')) {
foreach ($customer->cards->data as $card) {
if ($card->id == $customer->default_card) {
return $card;
}
}
}
else {
foreach ($customer->sources->data as $card) {
if ($card->object == 'card' && $card->id == $customer->default_source) {
return $card;
}
}
}
} catch (Exception $e) {
drupal_set_message(t('We received the following error processing your card: %error. Please enter your information again or try a different card.', array(
'%error' => $e
->getMessage(),
)), 'error');
watchdog('commerce_stripe', 'Following error received when creating Stripe customer: @stripe_error.', array(
'@stripe_error' => $e
->getMessage(),
), WATCHDOG_NOTICE);
return FALSE;
}
}
else {
try {
$customer = Stripe_Customer::retrieve($stripe_customer_id);
if (property_exists($customer, 'cards')) {
$card = $customer->cards
->create(array(
'card' => $stripe_token,
));
}
else {
$card = $customer->sources
->create(array(
'card' => $stripe_token,
));
}
return $card;
} catch (Exception $e) {
drupal_set_message(t('We received the following error processing your card: %error. Please enter your information again or try a different card.', array(
'%error' => $e
->getMessage(),
)), 'error');
watchdog('commerce_stripe', 'Following error received when adding a card to customer: @stripe_error.', array(
'@stripe_error' => $e
->getMessage(),
), WATCHDOG_NOTICE);
return FALSE;
}
}
}
function _commerce_stripe_save_cardonfile($card, $uid, $payment_method, $set_default) {
// Store the Stripe customer and card ids in the remote id field of {commerce_cardonfile} table
$remote_id = (string) $card->customer . '|' . (string) $card->id;
// Populate and save the card
$card_data = commerce_cardonfile_new();
$card_data->uid = $uid;
$card_data->payment_method = $payment_method['method_id'];
$card_data->instance_id = $payment_method['instance_id'];
$card_data->remote_id = $remote_id;
$card_data->card_type = $card->type;
$card_data->card_name = $card->name;
$card_data->card_number = $card->last4;
$card_data->card_exp_month = $card->exp_month;
$card_data->card_exp_year = $card->exp_year;
$card_data->status = 1;
$card_data->instance_default = 0;
if (property_exists($card, 'type')) {
$card_data->card_type = $card->type;
}
else {
$card_data->card_type = $card->brand;
}
commerce_cardonfile_save($card_data);
if ($set_default) {
commerce_cardonfile_set_default_card($card_data->card_id);
}
watchdog('commerce_stripe', 'Stripe Customer Profile @profile_id created and saved to user @uid.', array(
'@profile_id' => (string) $card->customer,
'@uid' => $uid,
));
}
/**
* Implements hook_commerce_payment_method_info_alter().
*
* Displays a warning if Stripe private and public keys are not set and the
* user has permission to administer payment methods.
*/
function commerce_stripe_commerce_payment_method_info_alter(&$payment_methods) {
if (isset($payment_methods['commerce_stripe'])) {
// Just return if they don't have permission to see these errors.
if (!user_access('administer payment methods')) {
return;
}
$found_errors = FALSE;
$settings = _commerce_stripe_load_settings();
// If secret_key or public_key is not set.
if (empty($settings['secret_key']) || empty($settings['public_key'])) {
$found_errors = TRUE;
drupal_set_message(t('Stripe secret and public key are required in order to use Stripe payment method. See README.txt for instructions.'), 'warning');
}
// If integration_type is not set.
if (empty($settings['integration_type'])) {
$found_errors = TRUE;
drupal_set_message(t('The Stripe payment method "Integration type" is not set. Stripe.js will be used by default.'), 'warning');
}
// If they need to configure anything, be nice and give them the link.
if ($found_errors) {
$link = l(t('configured here'), 'admin/commerce/config/payment-methods');
drupal_set_message(t('Settings required for the Stripe payment method can be !link.', array(
'!link' => $link,
)), 'warning');
}
}
}
function _commerce_stripe_load_settings($name = NULL) {
static $settings = array();
if (!empty($settings)) {
return $settings;
}
if (commerce_payment_method_load('commerce_stripe') && rules_config_load('commerce_payment_commerce_stripe')) {
$commerce_stripe_payment_method = commerce_payment_method_instance_load('commerce_stripe|commerce_payment_commerce_stripe');
}
if (isset($name) && rules_config_load('commerce_payment_commerce_stripe')) {
$commerce_stripe_payment_method = commerce_payment_method_instance_load('commerce_stripe|commerce_payment_commerce_stripe');
}
if (isset($commerce_stripe_payment_method)) {
$settings = $commerce_stripe_payment_method['settings'];
}
return $settings;
}
function _commerce_stripe_load_setting($name, $default_value = NULL) {
$settings = _commerce_stripe_load_settings($name);
return isset($settings[$name]) ? $settings[$name] : $default_value;
}
/**
* Card on file callback: background charge payment
* TODO: implement proper return codes per commerce payment
*/
function commerce_stripe_cardonfile_charge($payment_method, $card_data, $order, $charge = NULL) {
if (!commerce_stripe_load_library()) {
return FALSE;
}
// Fetch the customer id and card id from $card_data->remote_id
list($customer_id, $card_id) = explode('|', $card_data->remote_id);
$currency_code = $payment_method['settings']['stripe_currency'];
if (isset($charge['currency_code'])) {
$currency_code = $charge['currency_code'];
}
// Assemble charge parameters.
Stripe::setApiKey($payment_method['settings']['secret_key']);
// Build a default description and offer modules the possibility to alter it.
$description = t('Order Number: @order_number', array(
'@order_number' => $order->order_number,
));
$c = array(
'amount' => $charge['amount'],
'currency' => $currency_code,
'customer' => $customer_id,
'card' => $card_id,
'description' => $description,
);
commerce_stripe_add_metadata($c, $order);
// Let modules alter the charge object to add attributes.
drupal_alter('commerce_stripe_order_charge', $c, $order);
$transaction = commerce_payment_transaction_new('commerce_stripe', $order->order_id);
$transaction->instance_id = $payment_method['instance_id'];
$transaction->amount = $charge['amount'];
$transaction->currency_code = $currency_code;
// save as pending to ensure the drupal side of things is working before we attempt the stripe api call
$transaction->status = COMMERCE_PAYMENT_STATUS_PENDING;
if (!_commerce_stripe_commerce_payment_transaction_save($transaction)) {
return FALSE;
}
try {
$lock = __FUNCTION__ . '_' . $order->order_id;
if (lock_acquire($lock) && $charge['amount'] > 0) {
$response = Stripe_Charge::create($c);
$transaction->remote_id = $response->id;
$transaction->payload[REQUEST_TIME] = $response
->__toJSON();
$transaction->message = t('Payment completed successfully.');
$transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
_commerce_stripe_commerce_payment_transaction_save($transaction);
lock_release($lock);
return TRUE;
}
} catch (Exception $e) {
drupal_set_message(t('We received the following error processing your card. Please enter your information again or try a different card.'), 'error');
drupal_set_message(check_plain($e
->getMessage()), 'error');
watchdog('commerce_stripe', 'Following error received when processing card @stripe_error.', array(
'@stripe_error' => $e
->getMessage(),
), WATCHDOG_NOTICE);
$transaction->remote_id = $e
->getHttpStatus();
$transaction->payload[REQUEST_TIME] = $e->jsonBody;
$transaction->message = t('Card processing error: @stripe_error', array(
'@stripe_error' => $e
->getMessage(),
));
$transaction->status = COMMERCE_PAYMENT_STATUS_FAILURE;
_commerce_stripe_commerce_payment_transaction_save($transaction);
lock_release($lock);
return FALSE;
}
}
/**
* Card on file callback: create form
*/
function commerce_stripe_cardonfile_create_form($form, &$form_state, $op, $card_data) {
// Pass along information to the validate and submit handlers.
$form_state['card_data'] = $card_data;
$form_state['op'] = $op;
$stripe_integration = _commerce_stripe_load_setting('integration_type', STRIPE_DEFAULT_INTEGRATION);
if ($stripe_integration === 'stripejs') {
_commerce_stripe_form_add_credit_card_form($form);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Add card'),
);
}
elseif ($stripe_integration === 'checkout') {
// Stub this out so that making this the default card doesn't break.
$form['credit_card'] = array(
'#tree' => TRUE,
);
$order_total = array(
'amount' => 0,
'currency_code' => commerce_default_currency(),
);
$checkout_currency = _commerce_stripe_load_setting('stripe_currency');
// Since there is no associated order, just use the logged-in user's email
// address.
$email = $GLOBALS['user']->mail;
// Add Checkout settings.
$checkout_settings = _commerce_stripe_load_setting('checkout_settings');
// Change the label to Save, since we are not charging any money.
$checkout_settings['panelLabel'] = t('Save card');
_commerce_stripe_form_configure_stripe_checkout($checkout_settings, $checkout_currency, $email, $order_total);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Enter card details'),
'#description' => t('Please enter your details in the pop-up that opens. They are sent directly to the payment processor and do not pass through this website.'),
);
}
$public_key = _commerce_stripe_load_setting('public_key');
$stripe_token = !empty($form_state['values']['stripe_token']) ? $form_state['values']['stripe_token'] : '';
_commerce_stripe_form_configure_stripe_common($form, $stripe_token, $public_key, $stripe_integration);
$payment_method = commerce_payment_method_instance_load($card_data->instance_id);
$form['credit_card']['cardonfile_instance_default'] = array(
'#type' => 'checkbox',
'#title' => t('Use as default card for payments with %method', array(
'%method' => $payment_method['display_title'],
)),
'#default_value' => FALSE,
);
// Create a billing profile object and add the address form.
$profile = commerce_customer_profile_new('billing', $GLOBALS['user']->uid);
$form_state['commerce_customer_profile'] = $profile;
$form['commerce_customer_profile'] = array();
field_attach_form('commerce_customer_profile', $profile, $form['commerce_customer_profile'], $form_state);
$form['commerce_customer_profile']['#weight'] = -1;
// Add a validation callback so that we can call field_attach functions.
$form['#validate'][] = 'commerce_stripe_cardonfile_create_validate';
commerce_stripe_set_addressfield_class_names($form['address']);
$form['errors'] = array(
'#markup' => '<div id="card-errors"></div>',
);
return $form;
}
/**
* @param $checkout_settings
* @param $checkout_currency
* @param $email
* @param $amount
*/
function _commerce_stripe_form_configure_stripe_checkout($checkout_settings, $checkout_currency, $email, $amount) {
// Clean up js settings before adding them to the page.
$checkout_settings = array_filter(array_map(function ($value) {
if ($value === 0 or $value === 1) {
$value = (bool) $value;
}
return $value;
}, $checkout_settings));
// Get the image file if one is set.
if (isset($checkout_settings['image']['fid'])) {
$image = file_load($checkout_settings['image']['fid']);
if (is_object($image)) {
$checkout_settings['image'] = file_create_url($image->uri);
}
else {
// Empty image setting will cause a broken image to be displayed in checkout
// iframe.
unset($checkout_settings['image']);
}
}
$checkout_settings += array(
'currency' => $checkout_currency,
'email' => $email,
'amount' => $amount['amount'],
);
drupal_add_js(array(
'stripe' => array(
'checkout' => $checkout_settings,
),
), 'setting');
// Add external checkout.js library.
drupal_add_js('https://checkout.stripe.com/checkout.js', 'external');
}
/**
* @param $form
* @param $stripe_token
* @param $public_key
* @param $integration_type
*/
function _commerce_stripe_form_configure_stripe_common(&$form, $stripe_token, $public_key, $integration_type) {
// Add stripe token field. This field is a container for token received from
// Stripe API's response handler.
$form['stripe_token'] = array(
'#type' => 'hidden',
'#attributes' => array(
'id' => 'stripe_token',
),
'#default_value' => !empty($stripe_token) ? $stripe_token : '',
);
// Set our key to settings array.
drupal_add_js(array(
'stripe' => array(
'publicKey' => $public_key,
'integration_type' => $integration_type,
),
), 'setting');
// Load commerce_stripe.js.
$form['#attached']['js'] = array(
drupal_get_path('module', 'commerce_stripe') . '/commerce_stripe.js',
);
}
/**
* @param $form
*/
function _commerce_stripe_form_add_credit_card_form(&$form) {
$form += _commerce_stripe_credit_card_form();
// Include the stripe.js from stripe.com.
drupal_add_js('https://js.stripe.com/v2/', 'external');
}
/**
* Validation callback for card on file creation.
*/
function commerce_stripe_cardonfile_create_validate($form, &$form_state) {
$profile = $form_state['commerce_customer_profile'];
field_attach_form_validate('commerce_customer_profile', $profile, $form['commerce_customer_profile'], $form_state);
}
function commerce_stripe_cardonfile_create_form_submit($form, &$form_state) {
$card_data = $form_state['card_data'];
$payment_method = commerce_payment_method_instance_load($card_data->instance_id);
commerce_stripe_cardonfile_create($form, $form_state, $payment_method, $card_data);
$form_state['redirect'] = 'user/' . $card_data->uid . '/cards';
}
/**
* Card on file callback: create
*/
function commerce_stripe_cardonfile_create($form, &$form_state, $payment_method, $card_data) {
$card = _commerce_stripe_create_card($form_state['values']['stripe_token'], $card_data->uid, $payment_method);
if (!$card) {
return;
}
_commerce_stripe_save_cardonfile($card, $card_data->uid, $payment_method, $form_state['values']['credit_card']['cardonfile_instance_default']);
}
/**
* Card on file callback: updates the associated customer payment profile.
*/
function commerce_stripe_cardonfile_update($form, &$form_state, $payment_method, $card_data) {
if (!commerce_stripe_load_library()) {
return FALSE;
}
// Fetch the customer id and card id from $card_data->remote_id
list($customer_id, $card_id) = explode('|', $card_data->remote_id);
Stripe::setApiKey($payment_method['settings']['secret_key']);
try {
$customer = Stripe_Customer::retrieve($customer_id);
if (property_exists($customer, 'cards')) {
$card = $customer->cards
->retrieve($card_id);
}
else {
$card = $customer->sources
->retrieve($card_id);
}
$card->exp_month = $form_state['values']['credit_card']['exp_month'];
$card->exp_year = $form_state['values']['credit_card']['exp_year'];
$card
->save();
return TRUE;
} catch (Exception $e) {
drupal_set_message(t('We received the following error processing your card: %error. Please enter your information again or try a different card.', array(
'%error' => $e
->getMessage(),
)), 'error');
watchdog('commerce_stripe', 'Following error received when updating card @stripe_error.', array(
'@stripe_error' => $e
->getMessage(),
), WATCHDOG_NOTICE);
return FALSE;
}
}
/**
* Card on file callback: deletes the associated customer payment profile.
*/
function commerce_stripe_cardonfile_delete($form, &$form_state, $payment_method, $card_data) {
if (!commerce_stripe_load_library()) {
return FALSE;
}
// Fetch the customer id and card id from $card_data->remote_id
list($customer_id, $card_id) = explode('|', $card_data->remote_id);
Stripe::setApiKey($payment_method['settings']['secret_key']);
try {
$customer = Stripe_Customer::retrieve($customer_id);
if (property_exists($customer, 'card')) {
$customer->cards
->retrieve($card_id)
->delete();
}
else {
$customer->sources
->retrieve($card_id)
->delete();
}
return TRUE;
} catch (Exception $e) {
drupal_set_message(t('We received the following error processing your card: %error. Please enter your information again or try a different card.', array(
'%error' => $e
->getMessage(),
)), 'error');
watchdog('commerce_stripe', 'Following error received when deleting card @stripe_error.', array(
'@stripe_error' => $e
->getMessage(),
), WATCHDOG_NOTICE);
return FALSE;
}
}
/**
* Brings the stripe php client library into scope
*/
function commerce_stripe_load_library() {
$library = libraries_load('stripe-php');
if (!$library || empty($library['loaded'])) {
watchdog('commerce_stripe', 'Failure to load Stripe API PHP Client Library.', array(), WATCHDOG_CRITICAL);
return FALSE;
}
else {
$minimum_version = '1.17.1';
$maximum_version = '1.18.0';
$message = "Commerce Stripe is currently tested with stripe-php library versions @minimum_version through @maximum_version. You are using version @installed_version, and you should @upgrade_or_downgrade.";
//check that it's not lower than the minimum required version
if (version_compare($library['version'], $minimum_version, '<')) {
$variables = array(
'@minimum_version' => $minimum_version,
'@maximum_version' => $maximum_version,
'@installed_version' => $library['version'],
'@upgrade_or_downgrade' => 'upgrade',
);
watchdog('commerce_stripe', $message, $variables, WATCHDOG_WARNING);
return FALSE;
}
elseif (version_compare($library['version'], $maximum_version, '>')) {
$variables = array(
'@minimum_version' => $minimum_version,
'@maximum_version' => $maximum_version,
'@installed_version' => $library['version'],
'@upgrade_or_downgrade' => 'downgrade',
);
watchdog('commerce_stripe', $message, $variables, WATCHDOG_WARNING);
return FALSE;
}
return TRUE;
}
}
/**
* Checks existing cards on file to see if the customer has a Stripe customer id
*
* @param integer $uid
* The customer's Drupal user id
* @param string $instance_id
* The payment method instance id
*
* @return mixed
* The customer id if one was found, otherwise FALSE
*/
function commerce_stripe_customer_id($uid, $instance_id) {
$stored_cards = commerce_cardonfile_load_multiple_by_uid($uid, $instance_id);
if (!empty($stored_cards)) {
$card_data = reset($stored_cards);
list($customer_id, $card_id) = explode('|', $card_data->remote_id);
}
return !empty($customer_id) ? $customer_id : FALSE;
}
/**
* Implements hook_field_widget_WIDGET_TYPE_form_alter() for addressfield.
*
* Set unique classes on billing address fields so that commerce_stripe.js can find them.
*
* @param $element
* @param $form_state
* @param $context
*/
function commerce_stripe_field_widget_addressfield_standard_form_alter(&$element, &$form_state, $context) {
if ($context['field']['field_name'] === 'commerce_customer_address' && $context['instance']['bundle'] === 'billing') {
commerce_stripe_set_addressfield_class_names($element);
}
}
/**
* Sets unique class names on address field form elements so that they can be
* picked up by commerce_stripe.js.
*
* @param $element
* the addressfield form element
*/
function commerce_stripe_set_addressfield_class_names(&$element) {
if (isset($element['street_block']['thoroughfare'])) {
$element['street_block']['thoroughfare']['#attributes']['class'][] = 'commerce-stripe-thoroughfare';
}
if (isset($element['street_block']['premise'])) {
$element['street_block']['premise']['#attributes']['class'][] = 'commerce-stripe-premise';
}
if (isset($element['locality_block']['locality'])) {
$element['locality_block']['locality']['#attributes']['class'][] = 'commerce-stripe-locality';
}
if (isset($element['locality_block']['administrative_area'])) {
$element['locality_block']['administrative_area']['#attributes']['class'][] = 'commerce-stripe-administrative-area';
}
if (isset($element['locality_block']['postal_code'])) {
$element['locality_block']['postal_code']['#attributes']['class'][] = 'commerce-stripe-postal-code';
}
if (isset($element['country'])) {
$element['country']['#attributes']['class'][] = 'commerce-stripe-country';
}
}
/**
* Adds a metadata key to an existing information array.
*
* By default nothing is added here. Third party modules can implement
* hook_commerce_stripe_metadata(), documented in commerce_stripe.api.php, to add
* metadata. Metadata is useful to pass arbitrary information to Stripe, such as the
* order number, information about the items in the cart, etc.
*
* @param &$c
* An associative array, to which [metadata] => array(...) will be added, in case
* modules define information via the hook_commerce_stripe_metadata() hook.
* @param $order
* The commerce order object.
*/
function commerce_stripe_add_metadata(&$c, $order) {
$metadata = module_invoke_all('commerce_stripe_metadata', $order);
if (count($metadata)) {
$c['metadata'] = $metadata;
}
}
/**
* Implement hook_commerce_stripe_metadata and add our defaults.
*/
function commerce_stripe_commerce_stripe_metadata($order) {
return array(
'order_id' => $order->order_id,
'order_number' => $order->order_number,
'uid' => $order->uid,
);
}
Functions
Constants
Name | Description |
---|---|
STRIPE_DEFAULT_INTEGRATION | @file This module provides Stripe (http://stripe.com/) payment gateway integration to Commerce. Commerce Stripe offers a PCI-compliant way to process payments straight from you Commerce shop. |