Routing path to function
/wfmcom/docroot/sites/all/modules /custom/estore/estore.module
function estore_menu() {
$items['shop/%/checkout'] = array(
'access callback' => true,
'page callback' => 'estore_checkoutpage',
'page arguments' => array(1)
);
$items['shop/%'] = array(
'access callback' => true,
'page callback' => 'estore_storepage',
'page arguments' => array(1)
);
return $items;
}
Page callback function to Handlebars theme file
function estore_checkoutpage($store) {
estore_forcessl();
estore_nocache();
estore_start();
global $user;
global $_POST;
$estore = estore_new_estore();
$wfm = estore_new_wfm();
$hbs = estore_new_handlebars();
$cartID = estore_getUserCart($store);
// ...
$html_body = $hbs->render(
'checkout',
array(
'user' => $user_data,
'stored_payments' => $stored_payments,
'cart' => $items,
'usstates' => $usstates,
'ukstates' => $ukstates,
'castates' => $castates,
'countries' => $countries,
'items' => $cart,
'count' => $count,
'store' => $store_info,
'shipping_methods' => $shipping_methods,
'json_data' => json_encode($items),
)
);
Angular.js controller set by ng-controller attribute.
/wfmcom/docroot/sites/all/modules /custom/estore/dev/partials/*.handlebars
{{> header_secure}}
<div ng-controller="checkout">
<form id="checkoutForm"
name="form"
checkout-form
class="ng-class: {attempted_submit: form.attempted_submit}"
action="/shop/{{store.id}}/actions/submit"
method="post">
<div class="contained cart-body" ng-show="step==2">
{{#if user}}
<fieldset>
<h3>Welcome back, {{user.name}}</h3>
<label>Email *</label>
<input
ng-model="email"
required
type="email"
id="email"
name="email"
value="{{user.email}}"
ng-maxlength="100"
>
<div ng-show="form.email.$dirty && form.email.$invalid">
<span ng-show="form.email.$error.email">This is not a valid email.</span>
<span ng-show="form.email.$error.maxlength">This field must be less than 100 characters.</span>
</div>
</fieldset>
{{else}}
<fieldset>
<p>
<label>Guest Email *</label>
<input
ng-model="email"
required
type="email"
id="email"
name="email"
ng-maxlength="100"
>
<div ng-show="form.email.$dirty && form.email.$invalid">
<span ng-show="form.email.$error.email">This is not a valid email.</span>
<span ng-show="form.email.$error.maxlength">This field must be less than 100 characters.</span>
</div>
</p>
</fieldset>
{{/if}}
<!-- PayPage Request Input Fields -->
<fieldset>
<input type="hidden" id="request$paypageId" name="request$paypageId" value="abc123"/>
<input type="hidden" id="request$orderId" name="request$orderId" value="order_123"/>
<input type="hidden" id="request$timeout" name="request$timeout" value="15000"/>
</fieldset>
<!-- PayPage Response Output Fields -->
<fieldset>
<input type="hidden" id="response$paypageRegistrationId" name="response$paypageRegistrationId" readonly="true" value=""/>
<input type="hidden" id="response$litleTxnId" name="response$litleTxnId" readonly="true"/>
<input type="hidden" id="response$firstSix" name="response$firstSix" readonly="true"/>
<input type="hidden" id="response$lastFour" name="response$lastFour" readonly="true"/>
<input type="hidden" id="response$expMonth" name="response$expMonth" readonly="readonly"/>
<input type="hidden" id="response$expYear" name="response$expYear" readonly="true"/>
<input type="hidden" id="response$code" name="response$code" readonly="true"/>
<input type="hidden" id="response$message" name="response$message" readonly="true"/>
</fieldset>
/wfmcom/docroot/sites/all/modules /custom/estore/dev/js/app.js
_estore.controller('checkout', ['$scope','$http', '$sce','Estore','$rootScope','$timeout','$q','DataLayer',
function($scope, $http, $sce, Estore, $rootScope, $timeout, $q, DataLayer){
$scope.reviewOrder = function(form) {
Drupal.settings.WholeFoods = Drupal.settings.WholeFoods || {};
Drupal.settings.WholeFoods.Litle = Drupal.settings.WholeFoods.Litle || {};
var wfmLitle = Drupal.settings.WholeFoods.Litle;
// First validate non-credit card form elements
if ($scope.validateCheckout(form) == false) {
// Form is dirty, modal error should have launched, do not continue with reviewOrder.
console.log('Non credit card form elements are NOT valid according to $scope.validateCheckout(form)');
return;
} else {
console.log('Non credit card form elements are valid according to $scope.validateCheckout(form)');
}
The checkout page loads the litle script (checkout.handlebars):
<script src="https://request-prelive.np-securepaypage-litle.com/LitlePayPage/js/payframe-client.min.js"></script>
We configure and instantiate (app.js :: checkout controller):
jQuery(document).ready(function() {
Drupal.settings.WholeFoods.Litle = Drupal.settings.WholeFoods.Litle || {};
var wfmLitle = Drupal.settings.WholeFoods.Litle;
wfmLitle.payframeClientCallback = function(response) {
$scope.payframeCallback(response);
};
wfmLitle.paypageRegIdConfigure = {
"id" : json_data.id,
"orderId" : json_data.id
};
wfmLitle.litleConfigure = {
"paypageId" : document.getElementById("request$paypageId").value,
"style" : "WFMdefaultStyle", // StyleSheet file name, WFMdefaultStyle or empty
"div" : "payframe",
"callback" : $scope.payframeCallback,
};
if (typeof LitlePayframeClient === 'undefined') {
// TODO: HANDLE LITLE LIBRARY NOT LOADED ERR
} else {
payframeClient = new LitlePayframeClient(wfmLitle.litleConfigure);
}
}
"REVIEW ORDER" onClick event handler calls Angular.js 'checkout' controller reviewOrder() function (checkout.handlebars):
<input type="submit" class="button secure-btn"
onClick="return false;"
ng-click="reviewOrder(form)"
value="REVIEW ORDER" />
Angular.js 'checkout' controller reviewOrder() (app.js):
$scope.reviewOrder = function(form) {
Drupal.settings.WholeFoods = Drupal.settings.WholeFoods || {};
Drupal.settings.WholeFoods.Litle = Drupal.settings.WholeFoods.Litle || {};
var wfmLitle = Drupal.settings.WholeFoods.Litle;
// First validate non-credit card form elements
if ($scope.validateCheckout(form) == false) {
// Form is dirty, modal error should have launched, do not continue with reviewOrder.
console.log('Non credit card form elements are NOT valid according to $scope.validateCheckout(form)');
return;
} else {
console.log('Non credit card form elements are valid according to $scope.validateCheckout(form)');
}
var typeofPayframeClient = typeof payframeClient;
if (typeofPayframeClient == 'object' && jQuery("#vantiv-payframe").length > 0) {
payframeClient.getPaypageRegistrationId(wfmLitle.paypageRegIdConfigure);
return;
} else {
// Payframe not in use, proceed to the next step
$scope.prepForSubmit(form);
}
}
Angular.js 'checkout' controller reviewOrder() (app.js):
$scope.payframeCallback = function(response) {
Drupal.settings.WholeFoods = Drupal.settings.WholeFoods || {};
Drupal.settings.WholeFoods.Litle = Drupal.settings.WholeFoods.Litle || {};
var wfmLitle = Drupal.settings.WholeFoods.Litle;
var successful = false;
var errMsg = '';
document.getElementById('response$code').value = response.response;
document.getElementById('response$responseMessage').value = response.message;
document.getElementById('response$litleTxnId').value = response.litleTxnId;
document.getElementById('response$lastFour').value = response.lastFour;
document.getElementById('response$firstSix').value = response.firstSix;
document.getElementById('response$paypageRegistrationId').value = response.paypageRegistrationId;
if (response.response === '870') {
successful = true;
}
else if (
response.response === '871' ||
response.response === '872' ||
response.response === '873' ||
response.response === '874' ||
response.response === '876'
) {
// Recoverable error caused by user mis-typing their card info.
errMsg = 'Please check and re-enter your credit card number and try again.';
}
else if (
response.response === '881' ||
response.response === '882' ||
response.response === '883'
) {
// Recoverable error caused by user mis-typing their card info.
errMsg = 'Please check and re-enter your card validation number and try again.';
}
else if (response.response === '884') {
// Litle PayPage frame failed to load so payment can't proceed.
// Log the IP, user agent, time, paypageId and style that failed for debugging.
// Hide the frame to remove unsightly browser errs
jQuery('#payframe').hide();
$('#submitButton').attr('disabled','disabled');
}
else if (response.response === '885') {
// CSS failed to load so page will look unsightly but will function
// Continue with order
$('#submitButton').removeAttr('disabled');
// Log IP, user agent, time and style that failed to load for debugging
}
else {
// Non-recoverable or unknown error code.
errMsg = 'We are experiencing technical difficulties. Please reload this page and try again or call us to complete your order.';
}
if (successful == true) {
$scope.prepForSubmit($scope.form);
} else {
DataLayer.validationErrorEvent('billing', 'missing required field(s)');
$scope.openDialog(errMsg,
{onclose: function(){
$scope.focusToCCInput();
}
});
$scope.$apply();
}
}
Angular.js app.js:
/wfmcom/docroot/sites/all/modules/custom/estore/dev/js/app.js
Handlebars themes:
/wfmcom/docroot/sites/all/modules/custom/estore/dev/partials/*.handlebars
CSS:
/wfmcom/docroot/sites/all/modules/custom/estore/dev/css/components/*.scss
PHP Object Classes for APIs, caching and business logic:
/wfmcom/docroot/sites/all/modules/custom/estore/libs/bcapi/*.inc
Prod Files:
/wfmcom/docroot/sites/all/modules/custom/estore/public/*
After editing app.js, handlebars or CSS run grunt before edits are activated.
$ cd /wfmcom/docroot/sites/all/modules/custom/estore/
$ grunt
Grunt prod deployments also minify.
$ cd /wfmcom/docroot/sites/all/modules/custom/estore/
$ grunt prod
Add xdebug breakpoints or error_log() in the function getAPI() for raw requests and responses from the Spinutech or SAGE APIs.
class SAGE {
// ...
public function getAPI($url, $params, $type = 'GET') {
// Debug Spinutech API call here. Look at our request $url and $params values here.
if ($type == 'GET') {
// if we have a SAGE OAuth token at all, use it.
// don't try to generate one here, because we may actually in the process
// of trying to generate one...
if ($token = $this->estore_cache->get('sage_token')) {
$params['access_token'] = $token;
} else {
$params['client_id'] = $this->getParams('client_id');
$params['client_secret'] = $this->getParams('client_secret');
}
$post_data ='';
foreach ($params as $field=>$val) {
$post_data = $post_data . urlencode($field) . "=" . urlencode($val) . "&";
}
$url = $url . '?' . $post_data;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
}
if($type == 'POST'){
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_HTTPHEADER, Array("Content-Type: text/xml"));
}
$result = curl_exec($ch);
// Debug $result here for raw response from Spinutech API
curl_close($ch);
return $result;
}