LP1774892 stripe elements
authorJason Etheridge <jason@EquinoxInitiative.org>
Fri, 12 Jun 2020 13:57:03 +0000 (09:57 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Wed, 5 Aug 2020 21:40:11 +0000 (17:40 -0400)
This commmit changes the OPAC to use https://js.stripe.com/v3/ instead of
https://js.stripe.com/v2/ for processing payments through Stripe.

Additionally, it disables the "internal" credit card form in the staff client
when Stripe is the payment processor (or if the processor is not set at all),
as that does not currently work.

It also does not replace Business::Stripe's use of the "Charges API" with the
newer "Payment Intents" API on the backend, but credit card details are still
not sent to the Evergreen server.

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
Signed-off-by: John Amundson <jamundson@cwmars.org>
Signed-off-by: Dawn Dale <ddale@georgialibraries.org>
Signed-off-by: Jason Stephenson <jason@sigio.com>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>

Open-ILS/src/templates/opac/css/style.css.tt2
Open-ILS/src/templates/opac/myopac/main_payment_form.tt2
Open-ILS/src/templates/opac/parts/stripe.tt2
Open-ILS/src/templates/staff/circ/patron/t_cc_payment_dialog.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/bills.js

index 72c092f..6fe12ea 100644 (file)
@@ -1367,6 +1367,17 @@ div.result_table_utils_cont {
    text-align:center;
 }
 
+/* Stripe's payment form */
+#payment-form
+{
+    background-color: [% css_colors.accent_ultralight %];
+    width: 50%;
+}
+#card-element
+{
+    padding: 10px;
+}
+
 /* text to state what is seen on cc statement */
 .cc_disclaimer
 {
index 455af6a..6c40229 100644 (file)
         "process credit card payments without it.  Please change your " _
         "browser settings and try again.") %]
 </noscript>
+<script type="text/javascript">
+function build_stripe_form() {
+    var elements = stripe.elements();
+
+    var style = {
+        base: {
+            color: '#32325d',
+            fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
+            fontSmoothing: 'antialiased',
+            fontSize: '16px',
+            '::placeholder': {
+                color: '#aab7c4'
+            }
+        },
+        invalid: {
+            color: '#fa755a',
+            iconColor: '#fa755a'
+        }
+    };
+
+    var card = elements.create('card', {style: style});
+    card.mount('#card-element');
+
+    // real-time validation
+    card.on('change', function(event) {
+      var displayError = document.getElementById('card-errors');
+      if (event.error) {
+        displayError.textContent = event.error.message;
+      } else {
+        displayError.textContent = '';
+      }
+    });
+
+    // let's try some auto-focus
+    card.on('ready', function(event) {
+        try { card.focus(); } catch(E) { console.log('failed to focus card element',E); }
+    });
+
+    var form = document.getElementById('payment-form');
+    form.addEventListener('submit', function(event) {
+      event.preventDefault();
+
+      stripe.createToken(card).then(function(result) {
+        if (result.error) {
+          // Inform the user if there was an error.
+          var errorElement = document.getElementById('card-errors');
+          errorElement.textContent = result.error.message;
+        } else {
+          // Send the token to your server.
+          stripeTokenHandler(result.token);
+        }
+      });
+    });
+
+    function stripeTokenHandler(token) {
+      var form = document.getElementById('payment-form');
+      var hiddenInput = document.createElement('input');
+      hiddenInput.setAttribute('type', 'hidden');
+      hiddenInput.setAttribute('name', 'stripe_token');
+      hiddenInput.setAttribute('value', token.id);
+      form.appendChild(hiddenInput);
+
+      form.submit();
+    }
+}
+[% IF ctx.want_jquery %]
+    // jquery won't actually load until after this execution thread
+    setTimeout(function() { $(document).ready(build_stripe_form) }, 0);
+[% ELSE %]
+    // but jquery is preferable to doing this
+    setTimeout(build_stripe_form,0);
 [% END %]
-<div id="pay_fines_now"[% IF ctx.use_stripe %] class="hide_me"[% END %]>
+</script>
+<form action="[% ctx.opac_root %]/myopac/main_pay_init" method="post" id="payment-form">
+  <div class="form-row">
+    <label for="card-element">
+      <h1>Credit Card Information</h1>
+    </label>
+    <div id="card-element">
+      <!-- A Stripe Element will be inserted here. -->
+    </div>
+
+    <!-- Used to display form errors. -->
+    <div id="card-errors" role="alert"></div>
+  </div>
+
+  <button class="opac-button">Submit Payment</button>
+</form>
+[% ELSE %]
+<div id="pay_fines_now">
     [% IF last_chance %]
     
     <p><big>[% l("Are you sure you are ready to charge ") %]
         <a href="[% mkurl(ctx.opac_root _ '/myopac/main#selected_fines', {}, 1) %]" class="opac-button">[% l('Cancel') %]</a>
     [% ELSE %]
     
-    <form method="post" id="payment_form" action='#payment'
-    [% IF ctx.use_stripe %]
-    onsubmit="return stripe_onsubmit();"
-    [% END %]
-    >
+    <form method="post" id="payment_form" action='#payment'>
         <input type="hidden" name="last_chance" value="1" />
         [% FOR xact IN CGI.param('xact') %]
         <input type="hidden" name="xact" value="[% xact | html %]" />
         [% FOR xact IN CGI.param('xact_misc') %]
         <input type="hidden" name="xact_misc" value="[% xact | html %]" />
         [% END %]
-        [% IF ctx.use_stripe %]
-        <input type="hidden" name="stripe_token" id="stripe_token" />
-        [% END %]
 
          <table id="billing_info_table">
           <tbody>
                     <td><label for="payment-credit-card">[% l('Credit Card #') %]</label></td>
                     
                      <!-- Make type tel, which prompts for numbers in mobile -->
-                    <td><input type="tel" pattern="[0-9]*" maxlength="16" id="payment-credit-card" required 
-                    [% IF ctx.use_stripe %]
-                    data-stripe="number"
-                    [% ELSE %]
-                    name="number"
-                    [% END %]
-                    /></td>
+                    <td><input type="tel" pattern="[0-9]*" maxlength="16" id="payment-credit-card" required name="number"/></td>
                 </tr>
                 <tr>
                     <td><label for="payment-security-code">[% l('Security Code') %]</label></td>
                     <td>
                          <!-- Make type tel, which prompts for numbers in mobile -->
-                        <input type="tel" pattern="[0-9]*" size="4" maxlength="5" id="payment-security-code"
-                        [% IF ctx.use_stripe %]
-                        data-stripe="cvc"
-                        [% ELSE %]
-                        name="cvv2"
-                        [% END %]
-                        /></td>
+                        <input type="tel" pattern="[0-9]*" size="4" maxlength="5" id="payment-security-code" name="cvv2"/></td>
                 </tr>
                 <tr>
                     <td><label for="payment-expire-month">[% l('Expiration Month') %]</label></td>
                     <td>
-                        <select id="payment-expire-month" required
-                        [% IF ctx.use_stripe %]
-                        data-stripe="exp_month"
-                        [% ELSE %]
-                        name="expire_month"
-                        [% END %]
-                        >
+                        <select id="payment-expire-month" required name="expire_month">
                             <option value="-1"></option>
                             <option value="01">[% l("January (1)") %]</option>
                             <option value="02">[% l("February (2)") %]</option>
                 <tr>
                     <td><label for="payment-expire-year">[% l('Expiration Year') %]</label></td>
                     <td>
-                        <select id="payment-expire-year"
-                        [%- IF ctx.use_stripe %]
-                        data-stripe="exp_year"
-                        [% ELSE %]
-                        name="expire_year"
-                        [% END -%]
-                        >
+                        <select id="payment-expire-year" name="expire_year">
                         [% year = date.format(date.now, '%Y');
                         y = year;
                         WHILE y < year + 10; # show ten years starting now %]
 </div>
 [% END %]
 [% END %]
+[% END %]
index ea4ca04..29fea86 100644 (file)
@@ -1,35 +1,9 @@
 [%- PROCESS "opac/parts/header.tt2" %]
 [% IF ctx.use_stripe %]
-        <script type="text/javascript" src="https://js.stripe.com/v2/"></script> <!-- use an ou setting for this url? -->
+        <script type="text/javascript" src="https://js.stripe.com/v3/"></script> <!-- use an ou setting for this url? -->
         <script type="text/javascript">
         // This script is only displayed when logged in, so ctx.user.home_ou is always available
-        Stripe.setPublishableKey('[% ctx.get_org_setting(ctx.user.home_ou.id, 'credit.processor.stripe.pubkey') %]');
+        var stripe = Stripe('[% ctx.get_org_setting(ctx.user.home_ou.id, 'credit.processor.stripe.pubkey') %]');
 
-        function stripe_onsubmit() {
-            var form = document.getElementById("payment_form");
-            var button = document.getElementById("payment_submit");
-
-            button.disabled = true;
-
-            Stripe.card.createToken(form, stripe_callback);
-
-            return false;
-        }
-
-        function stripe_callback(status, response) {
-            var form = document.getElementById("payment_form");
-            var button = document.getElementById("payment_submit");
-            var stripe_token = document.getElementById("stripe_token");
-
-            if(response.error) {
-                alert(response.error.message);
-                button.disabled = false;
-                return;
-            }
-
-            stripe_token.value = response.id; // response.id is the token id, though there are more fields available if needed.
-            form.setAttribute("onsubmit","");
-            form.submit();
-        }
         </script>
 [%- END %]
index 1551f56..4511b51 100644 (file)
@@ -13,7 +13,7 @@
         <div class="col-md-4"><label>[% l('Process Where?') %]</label></div>
         <div class="col-md-8">
           <select class="form-control" ng-model="context.cc.where_process">
-            <option value='1'>
+            <option value='1' ng-disabled="context.cc.disable_internal">
               [% l('Process payment through Evergreen') %]</option>
             <option value='0'>
               [% l('Record externally processed payment') %]</option>
index 539fe6c..f9a6d66 100644 (file)
@@ -16,7 +16,8 @@ function($q , egCore , egWorkLog , patronSvc) {
             'ui.circ.billing.uncheck_bills_and_unfocus_payment_box',
             'ui.circ.billing.amount_warn', 'ui.circ.billing.amount_limit',
             'circ.staff_client.do_not_auto_attempt_print',
-            'circ.disable_patron_credit'
+            'circ.disable_patron_credit',
+            'credit.processor.default'
         ]).then(function(s) {return service.settings = s});
     }
 
@@ -543,6 +544,13 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
         if (s['circ.disable_patron_credit']) {
             $scope.disablePatronCredit = true;
         }
+        if (!s['credit.processor.default']) {
+            // If we don't have a CC processor, we should disable the "internal" CC form
+            $scope.disableCreditCardForm = true;
+        } else {
+            // Stripe isn't supported in the staff client currently, so disable here too
+            $scope.disableCreditCardForm = (s['credit.processor.default'] == 'Stripe');
+        }
     });
 
     $scope.gridControls.allItemsRetrieved = function() {
@@ -691,6 +699,8 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
         if ($scope.payment_type != 'credit_card_payment') 
             return $q.when();
 
+        var disableCreditCardForm = $scope.disableCreditCardForm;
+
         return $uibModal.open({
             templateUrl : './circ/patron/t_cc_payment_dialog',
             backdrop: 'static',
@@ -700,7 +710,8 @@ function($scope , $q , $routeParams , egCore , egConfirmDialog , $location,
 
                     $scope.context = {
                         cc : {
-                            where_process : '1', // internal=1 ; external=0
+                            where_process : disableCreditCardForm ? '0' : '1', // internal=1 ; external=0
+                            disable_internal : disableCreditCardForm,
                             type : 'VISA', // external only
                             billing_first : patronSvc.current.first_given_name(),
                             billing_last : patronSvc.current.family_name()