Promise implementation for Apps Script Stripe payments


In a recent Totally unscripted broadcastSteve Webster provided a great introduction to monetizing add-ons, along with some sample code on how to use Stripe to manage payments. 

Using stripe means that you have to deal with things happening both server and client side, as well as with the Stripe API. This means that there's a fair bit of async activities going on, and if you read the content on this site at all you'll notice that I prefer to use Promises to  orchestrate all that. In this post I'll show a way of implementing Stripe from Apps Script using promises. 

Using StripeCheckout. 


Stripe have a simplified entry point to their API - StripeCheckout. You can include it in your project with 
<script src="https://checkout.stripe.com/checkout.js"></script>

The steps involved in using StripeCheckout are as follows
  • Configure. Here you set up things like the icon to use in the payment dialog, what to callback when the payment is complete, and when the payment is closed.
  • Open. This kicks off the payment screen. Here you can set up the amount, and various behavior parameters. 

I have a dedicated namespace for doing all my Stripe related stuff, but the method that configures and gets the payment is here. 
/**
   * get the payment client side
   * @param {string} name 1st line on panel
   * @param {string} description 2nd line
   * @param {number} amount in cents to charge
   * @return {Promise} a promise to the result of the payment
   */
  ns.getPayment = function (name, description , amount) {

    // this is a promise to the payment
    // store the resolution callbacks for later
    var resolvePayment, rejectPayment;
    var payment = new Promise (function (resolve , reject ) {
      resolvePayment = resolve;
      rejectPayment = reject;
    });
     
    // configure returns an object
    // that will execute with the .open method
    try {
      StripeCheckout
      .configure ({
        key:ns.settings.keys.key,
        image:ns.settings.image,
        token:function (tob) {
          // this is fired
          // when a payment is successfully made
          resolvePayment (tob);
        },
        closed:function() {
          // this will be fired after the token resolution
          // so if the promise payload is null you'll know that
          // the payment was abandoned
          resolvePayment (null);
        }
      })
      .open ({
        name: name ,
        description: description,
        amount: amount,
        currency: 'USD',
        panelLabel: "Pay {{amount}}",
        zipCode: false,
        allowRememberMe:false
      });
    }
    catch (err) {
      rejectPayment (err);
    }

    return payment;
  };

Let's walk through that a little. 

getPayment returns a promise which is resolved when
  • the token handler indicating that payment has been made is fired. In which case the promise delivers the token.
  • the closed handler indicating that the payment form has been closed is fired. This resolution delivers null
The closed handler is fired after the token handler, so that way you can know if the resolved promise delivers null, then the payment form has been abandoned.

This is what the payment form looks like

If the user successfully completes a payment, then  this is fired.

        token:function (tob) {
          // this is fired
          // when a payment is successfully made
          resolvePayment (tob);
        },

Using getPayment.

I have a couple of different payment forms in the app from which this code is extracted, so I use this general function
  /** 
    * configure and get a payment
    * @param {number} currentExpire timestamp that current subscription runs out
    * @param {string} type the type of subscription
    * @param {DomElement} button the button that initiated the payment form to come up
    * @param {object} plan the plan description -eg {name:'Gold',amount:500,description:'Dicers Gold'}
    * @return {Promise} to all this happenening
    */
    function pay_ (currentExpire,type,button,plan) {
      
      // avoid double clicking
      button.disabled = true;
      
      // the callback when token is received
      return GasStripeCheckout.getPayment(type , plan.description, plan.amount)
      .then (function (result) {

        // if result is null, then the payment gas bveen abandoned
        if (result) {
          // do whatever is is required when payment is successful
          Process.control.registration.data.plan.expire = extendSubscription_ (currentExpire).getTime();
        
          // update properties on the server 
          // - only want to store minimal customer information, even though its not accesible to me anyway
          return Provoke.run ('Props', 'setPlan',  {
            created:result.created,
            email:result.email,
            id:result.id,
            type:type,
            name:plan.name,
            expire:Process.control.registration.data.plan.expire
          });
        }
      })
      .then(function(data) {
        
        // this will be null if payment was abandoned
        if (data) {
          Process.control.registration.data = data;
          App.toast ('Thank you for your ' + type,  
                     '<div class="mui--text-caption">Payment reference is <br>' + data.plan.id + '</div>');
        }

        showPlan_ ();

      })
      ['catch'] (function (err) {
        // do whatever is is required when payment fails
        App.showNotification ("payment failed", err);
      });
    }

Let's walk through
This is running from the client side, but it obviously has to update properties using server side functions to store the result of the payment. 
  • disable the button that kicked off the payment process to avoid double clicking
  • initiate the payment form
  • when its resolved, it will be resolved either with some data - a payment was made, or null - it was abandoned.
  • use Provoke (see Using promises with apps script) to initiate a server side store of data associated with the payment. Provoke is just a Promise based wrapper for google.script.run
  • when that's done, inform user all is well and update the status of the premium screen to reflect the new status
In other words , a successful payment replaces this

with this


and that's all there is to it. Some of the functions here are of course specific to my app, so I won't reproduce them here, but you should get the general picture. 


For more on the dicers add-on - see Dicers

Comments