The Problem

After a few initial discussions with my cofounder, we decided that a multi-pronged attack was needed to entice new users to our app.

First, we needed a A/B landing pages, one page with a free option, two pages without a free option.

Second, we needed to offer coupons so that we could track certain ads and other promotionals. The gotcha is that some of the coupons were 100% off.

Third, we needed to be able to get money. Using the Stripe Checkout seemed like the simplest solution.

Fourth, we wanted to do client-side validation. We felt that billing a user, using Stripe checkout, without creating an account, due to invalid form data, was a poor user experience.

Jeah

This is how we set up client-side validation, Stripe Payments, and coupons at VersesByPhone.com

For this app we use: * jQuery, bValidator plug-in * Stripe Checkout * Padrino * HTML, bootstrap

Include the Javascript & CSS

1 <script src="/javascripts/jquery.js"></script>
2 <script src="/javascripts/jquery.bvalidator-yc.js"></script>
3 <script src="/javascripts/verses.custom.js"></script>
4 <script src="https://checkout.stripe.com/v2/checkout.js"></script>
5 <link rel="stylesheet" href="/stylesheets/bvalidator.css" media="all" />
6 ... [ bootstrap includes ]

This is basic: 1. Include the jQuery library 2. Include the bValidator jQuery plug-in 3. Include the Stripe Checkout javascript for express payment 4. Have a bit of style for our client side validation 5. I happen to use bootstrap as well, because my design skills are strugglin'

Create a form, Part 1

 1 <% form_for :account, url(:accounts, :create), {id: "account_form" } do |f| %>
 2 <div class="input">
 3   <%= f.label :name, caption: "First Name" %>
 4   <%= f.text_field :name, :class => :input, :"data-bvalidator" => "required"  %>
 5   <%= f.error_message_on :name, id: "account_name_error" %>
 6 </div>
 7 
 8 <div class="input">
 9   <%= f.label :email, caption: "Email" %>
10   <%= f.text_field :email, :class => :input, :"data-bvalidator" => "required,email"  %>
11   <%= f.error_message_on :email, id: "account_email_error"  %>
12 </div>
13 
14 
15 <%
16 #  NOTE: Assume:
17 #  @subscription_types = {
18 #    id: 0, label: "Free",
19 #    id: 1, label: "Weekly ($5.99)",
20 #    id: 2, label: "Yearly $(34.99)",
21 %>
22 -->
23 <div class="input">
24   <%= f.label :subscription_type_id, caption: "Subscription:" %>
25   <%= select_tag(:subscription_type_id,
26     collection: @subscription_types, fields: [:label, :id],
27     selected: params[:subscription_type_id],
28     class: "select",
29     id: :subscription_type_id,
30   ) %>
31   <%= f.error_message_on :subscription_type_id %>
32 </div>
33 
34 <%= partials :"accounts/_coupon_code", locals: { f: f } %>
35 <%= partials :"accounts/_payment", locals: { f: f } %>

Here we have set up a trivial form. - We ask the user for her first name and email, and subscription (from a select box). - First name and email are required during validation, using the data-bvalidator attributes. - Furthermore, the email should be a valid email address. - We include the coupon partial - We include the payment partial

Create a form, Part 2

accounts/_coupon_code.erb:

 1 <div class="input">
 2   <%= f.label :coupon_code, caption: "Coupon Code:" %>
 3   <%= text_field_tag :coupon_code, value: params[:coupon_code],
 4     id: :coupon_code, :class => :input %>
 5 
 6   <a href="#" id="apply_coupon" class="btn btn-large info_button">Apply Coupon</a>
 7 </div>
 8 
 9 <div class="input" id="total_payment_div" style="display:none">
10   <%= f.label :total, caption: "Total Payment", style:"color: #F00" %>
11   <span id="total_payment"></span>
12 </div>

This code: - Displays a text box for entering a coupon code - Exposes a button to “apply coupon” - Creates a hidden div for displaying total payment

Create a form, Part 3

accounts/_payment.erb:

<div class="input" id="save_and_cancel"
  style="display: <%= show ? :block : :none %>">
  <button class="btn btn-primary btn-large submit_button" id="save_button">
    Create My Account
  </button>
</div>

<div class="inptfilds" id="stripe_payment"
  style="display: <%= show ? :block : :none %>">
    <button class="btn btn-primary btn-large submit_button" id="stripe_button">
      Create My Account
  </button>
</div>

Th above code creates two buttons: - One for the free subscriptions (100% coupon, per se) - One for paid subscriptions

The jQuery code

/javascripts/verses.custom.js

 1 // apply_coupon button clicked
 2 $('#apply_coupon').click(function(){
 3 $.post('/apply_coupon?',
 4   $('#account_form').serialize(),
 5   function(data) {
 6     res = $.parseJSON(data);
 7 
 8     $('#total_payment').html(res.str);
 9     $('#total_payment_div').show();
10 
11     if (res.free) {
12       $('#stripe_payment').hide();
13       $('#save_and_cancel').show();
14     } else {
15       $('#stripe_payment').show();
16       $('#save_and_cancel').hide();
17     }
18   });
19   return false;
20 
21 // stripe_button clicked:
22 // - validate form
23 // - if valid, show Stripe Payment form
24 $('#stripe_button').click(function(){
25   if (!($('#account_form').data('bValidator').validate())) { return false; }
26 
27   var token = function(res) {
28     var $input = $('<input type=hidden name=stripeToken />').val(res.id);
29     $('#account_form').append($input).submit();
30   };
31 
32   StripeCheckout.open({
33     key:         '<%= settings.publishable_key %>',
34     image:       '/images/logo.png',
35     name:        'versesByPhone.com',
36     description: 'Subscription to versesByPhone.com',
37     panelLabel:  'Purchase',
38     token:       token,
39   });
40 
41   return false;
42 });
43 
44 // save_button is clicked:
45 // - validate form
46 // - if valid, show Stripe Payment form
47 $('#save_button').click(function(){
48   if (!($('#account_form').data('bValidator').validate())) { return false; }
49   $('#account_form').submit();
50   return false;
51 });

The code above is a bit hairy. So I will explain block by code block.

When the apply_coupon button is clicked: - We serialize the form, and send it to the route “/apply_coupon” - The ajax call is returns a JSON object { str: “[price with coupon applied]”, free: [true|false] } - The string from the json is placed into the hidden div #total_payment. This div is displayed to the user - If the subscription is free – w/ or w/o the coupon – the stripe-payment button is hidden and the save button is exposed - If the subscription is not free, the stripe button is exposed, the save button is hidden

When the stripe_button is clicked: - Validate the form text boxes against the bValidator rules found in the input tags attributes - If the form is appears to be valid, create the stripeToken hidden input field, and raise the Stripe Checkout form - If the Stripe checkout form is filled with valid input and submitted, it will set the stripeToken field, and submit the form

When the save_button is clicked: - Validate the form text boxes against the bValidator rules found in the input tags attributes - If the form is appears to be valid, submit the form

The Route Handler

 1 MyApp.controllers :base do
 2 
 3   [...]
 4 
 5   post :apply_coupon, map: "/apply_coupon" do
 6     unless (options = params[:subscription_type_id])
 7       return { str: 'No Data', free: false }
 8     end
 9 
10     s = Subscription.new(options)
11 
12     res = [s.price_frequency, s.coupon_status].compact.join(" ")
13 
14     {str: res, free: s.free? }.to_json
15   end
16 
17   [...]
18 end

This above code does the following: - Looks for a subscription_type_id passed from the select box - If subscription_type_id doesn’t exist, it explicitly returns with a some info for the user. - If subscription_type_id exists, it builds a Subscription object from the params - The Subscription object makes calls to build a string to display in the total_payment div, as well as whether the subscription is free after the coupon is or isn’t applied - The response is returned as a json hash

The Model

/models/subscription.rb

 1 class Subscription
 2 
 3   many_to_one :subscription_type
 4   attr_reader :coupon_status
 5 
 6   def price
 7     @price ||= begin
 8       if subscription_type
 9         subscription_type.price
10       else
11         raise "Subscription Type Error for Subscription ##{id}, [type id: #{subscription_type_id.inspect}]"
12       end
13     end
14   end
15 
16   def stripe_coupon
17     @stripe_coupon ||= Stripe::Coupon.retrieve(coupon)
18   rescue Stripe::InvalidRequestError
19     @coupon_status = "(error with coupon '#{coupon}')"
20     nil
21   end
22 
23   def adjusted_price
24     return price if stripe_coupon.nil?
25 
26     @coupon_status = "(coupon applied)"
27     if (amt = stripe_coupon["amount_off"])
28       price - amt
29     elsif (pct = stripe_coupon["percent_off"])
30       price - (price * pct.fdiv(100))
31     end
32   end
33 
34   def price_frequency
35     dec_price =  "%0.02f" % adjusted_price.fdiv(100)
36 
37     "$#{dec_price}/#{subscription_type.freq}"
38   end
39 
40   def free?
41     @free ||= subscription_type.free? || adjusted_price.zero?
42   end
43 end

This is where all of the fun happens. We will go through this block by block.

Assume the model: SubscriptionType: price (USD, in cents) freq (monthly, weekly)

Subscription#price We attempt to grab a base price to which a coupon might be applied. It looks for the subscription_type. If the subscription_typ is nil, an RunTimeError is raised.

Subscription#stripe_coupn We attempt to grab the Stripe coupon by name. If successful, this call will return a hash that will contain the keys “amount_off”, “percent_off”, one of which will be nil, the other, set. If the coupon doesn't exist, an exception will be thrown, which we rescue. We set the coupon status accordingly, to let the user know that their coupon was bogus. Bogus!

Subscription#adjusted_price No we are cookin' with gasoline! If the coupon doesn't exist, return the default price. If the coupon does exists, set the status to “coupon applied.” If the coupon contains a set amount off, we subtract this amount from the base price. We the coupon is a percent off, we subscription the percentage of the price from the base.

Subscription#price_frequency We generate a string as a label that will allow the user to know the amount he will be charged, and the frequency that we will attempt to charge them. This is based off the adjusted price and the frequency of the subscription type he have chosen.

Subscription#free? This returns true for the following conditions: - The Subscription Type is free, such as our “2 Week Trial” - The coupon, when applied, adjusts the price to $0.00, such as our 100% off promo

Final Thoughts

I think thats about it. This is how we set up client-side validation, Stripe Payments, and coupons at VersesByPhone.com I am not going to proof read this blog posting until months from now. Its taken me about 2 hours to right this thing, and I am tired. I forgot why I stopped blogging, its because its tiring. I didn't include all of the code, so if you are having trouble, feel free to comment and I will get back to you. There are plenty of places to refactor this code. If you have suggestions I am all ears.



blog comments powered by Disqus