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; }