Customer Subscription Management

Customers in LemonStand can manage their subscriptions directly from the store frontend. This includes actions like changing the subscription's Billing Plan, the subscription's products, pausing the subscription, and others.

Subscription CMS Actions

For an overview on how CMS actions work, please refer to this entry.

There are two CMS actions that give registered customers the ability to view and change their subscriptions. These are listed below:

  • subscriptions:subscriptions - If a customer is logged in, and has subscriptions, this action will serve a collection of subscriptions (defined by the variable subscriptions) for the customer.
  • subscriptions:subscription/:subscription_id - If a customer is logged in, and has a valid subscription, as specified by the supplied parameter, this action will serve the subscription (defined by the variable subscription).

Customer Subscriptions Page

We'll follow these steps for adding a Subscriptions page:

  1. Click on the "Add" button within the Store Design > Pages page.
  2. Under "Edit Page" input the following per field:
    1. Name - Subscriptions
    2. Template - Inner (or whichever internal template you have set up)
    3. URL- /subscriptions
    4. Code - subscriptions
  3. Under "Code Blocks", input the following per field:
    1. Code - setup
    2. Content - {% global active_page = 'subscriptions' %}
  4. Under "Action", select subscriptions:subscriptions.

For the HTML content to render in this view, we'll use the following:

{% if not subscriptions or not subscriptions.count %}
  <p>Subscriptions not found</p>
{% else %}
  <table class="product-list full-width">
    <tr>
      <th>#</th>
      <th>Date</th>
      <th>Status</th>
    </tr>
    {% for subscription in subscriptions %} 
      {% set url = root_url('/subscription/' ~ subscription.id) %} 
      <tr>
        <td><a href="{{ url }}">{{ subscription.id }}</a></a></td>
        <td><a href="{{ url }}">{{ subscription.created_at.format('M jS, Y') }}</a></td>
        <td><a href="{{ url }}">{{ subscription.status.name }}</a></td>
      </tr>
    {% endfor %}
  </table>
{% endif %}

This will generate links to our "/subscription/:subscriptionId" action, which expect a valid subscription ID for the logged-in customer in order to load the page. Under 'Action', select 'subscriptions:subscription'.

Customer Subscription Page

We'll follow these steps for adding a Subscription page:

  1. Click on the "Add" button within the Store Design > Pages page.
  2. Under "Edit Page" input the following per field:
    1. Name - Edit Subscription
    2. Template - Inner (or whichever internal template you have set up)
    3. URL - /subscription/:subscriptionId
    4. Code - subscription
  3. Under "Action", select subscriptions:subscription.

For the HTML content we'll use a partial:

{% if subscription %}
    <div id="subscription-content">
        {{ partial('shop-subscription-content') }}
    </div>
{% else %} 
    <p>Subscription not found</p>
{% endif %}

By placing this partial in a div element, we can reload the contents of that div when we want to make updates, showing the new subscription data. This is a faster approach to refreshing the page.

List of Subscription Order History

Next, we create the partial called shop-subscription-content. We'll start with the following HTML content:

{% if not subscription.orders or not subscription.orders.count %} 
    <p class="flash info">Orders not found</p>
{% else %} 
    <h3>Order History</h3>
    <table>
        <tr>
            <th>#</th>
            <th>Date</th>
            <th>Status</th>
            <th>Total</th>
        </tr>
        {% for order in subscription.orders if not order.is_quote %} 
            {% set url = root_url('/order/'~order.id) %} 
            <tr>
                <td><a href="{{ url }}">{{ order.number }}</a></a></td>
                <td><a href="{{ url }}">{{ order.created_at.format('M jS, Y') }}</a></td>
                <td><a href="{{ url }}">{{ order.orderStatus.name }}</a></td>
                <td><a href="{{ url }}"><i>{{ order.total|currency }}</i></a></td>
            </tr>
        {% endfor %} 
    </table>
{% endif %}

To display the order history of a given subscription, we can loop through the subscriptions orders available under the {{ subscription.orders }} variable. We populate the rows of a table with various order variables.

Changing the Billing Plan

To change the billing plan we can display a dropdown of billing plans available, which allows the customer to choose the plan they want. 

We'll edit the same partial as above, namely "shop-subscription-content":

{{ flash() }}
{{ open_form({
    'data-ajax-handler': 'subscriptions:onUpdateBillingPlan',
    'data-ajax-redirect': root_url('/subscription/' ~ subscription.id)
}) }} 
    <select id="billing_plan" name="billing_plan">
        {% for plan in billingPlans %}
            <option value="{{ plan.id }}" {{ option_state(plan.id, subscription.billingPlan.id) }}>
                {{ plan.name }}
            </option>
        {% endfor %}
    </select> 
    <input type="submit" class="button" value="Update Billing Plan"/>
    <input type="hidden" name="subscription_id" value="{{ subscription.id }}"/>
{{ close_form() }}

The important pieces of this example are here:

  • {{ flash() }} - The flash variable will populate with a message once a request completes, in this case: "Billing plan successfully updated.".
  • 'data-ajax-handler': 'subscriptions:onUpdateBillingPlan' - This handler makes our request listen for the billing_plan parameter.
  • data-ajax-redirect - Defines the url we want to redirect to.
  • billing_plan - The input with name "billing_plan" should send a value equal to the billing plan ID that the subscription is changing to.
  • {{ billingPlans }} - An array of billing plans available globally.
  • subscription_id - The current subscription's ID, required for the  "onUpdateBillingPlan" request handler.

Changing the Subscription Status

  • subscriptions:onUpdateSubscriptionStatus - If a customer is logged in and has a valid subscription as specified by the supplied parameter, this action allows the customer to be able to change their subscription status. Statuses currently supported are "active", "cancelled", "paused", or "trial".

Below is a code snippet that will render a form that posts to the action defined above. It includes a drop-down list of valid statuses to choose from:

{{ flash() }}
{{ open_form({
    'data-ajax-handler': 'subscriptions:onUpdateSubscriptionStatus',
    'class': 'custom', 'data-ajax-redirect': root_url('/subscription/' ~ subscription.id)
}) }}
    <select id="subscription_status" name="subscription_status">
        {% for status in subscriptionStatuses 
            if (status.code != 'past-due') 
            and (status.code != 'trial') 
        %}
            <option value="{{ status.code }}" {{ option_state(status.code, subscription.status.code) }}>
                {{ status.name }}
            </option>
        {% endfor %}
    </select>
    <input type="submit" class="button" value="Update Status"/>
    <input type="hidden" name="subscription_id" value="{{ subscription.id }}"/>
{{ close_form() }}

The important pieces of this example are here:

  • {{ flash() }} - The flash variable will populate with a message once a request completes, in this case: "Subscription status successfully updated.".
  • 'data-ajax-handler': 'subscriptions:onUpdateSubscriptionStatus' - This handler makes our request listen for the subscription_status parameter.
  • data-ajax-redirect - Defines the url we want to redirect to.
  • subscription_status - The input with name "subscription_status" should send a value equal to the subscription status code we are changing to.
  • subscription_id - The current subscription's ID, required for the  "onUpdateBillingPlan" request handler.

Adding Products to the Subscription

The subscription page supports the following two fields for adding products:

  1.  productId 
  2.  productIds[] 

Passing these field with a value equal to a product ID will add that product to the subscription. General use:

Single product add:

productId: {{ product.id }}
 
<!-- Product options -->
quantity: 2
options[{{ option.id }}]: {{ option.id }}
variantId: {{ variant.id }}

Multiple product add:

productIds[<array-key>]: {{ product.id }}
 
<!-- Product options -->
quantities[<array-key>]: 2
options[<array-key>][{{ option.id }}]: {{ option.id }}
variantIds[<array-key>]: {{ variant.id }}

Example Request:

data-ajax-handler="subscriptions:onAddSubscriptionProducts"

productIds[lemon-shirt] = 1
quantities[lemon-shirt] = 1
options[lemon-shirt][1] = 1
 
productIds[sweater] = 2
quantities[sweater] = 2
variantIds[lemon-shirt] = 1

Removing Products from the Subscription

The subscription page supports the following two fields for deleting products:

  1.  deleteProduct 
  2.  deleteProducts[] 

Passing these field with a value equal to a subscription product ID will remove that product from the subscription. General use:

Single product delete:

deleteProduct: {{ subscriptionProduct.id }}

Multiple product delete:

deleteProducts[<array-key>]: {{ subscriptionProduct.id }}

Note here that the "deleteProducts" is an array. The array key isn't required to be any specific value and usually makes sense to set as the subscriptionProduct's ID.

Example Request:

data-ajax-handler="subscriptions:onDeleteSubscriptionProducts"

deleteProduct[1] = 1
deleteProduct[2] = 2

Changing Subscription Products

The subscription page supports the following field for changing products:

  1.  subscriptionProduct[] 

Passing this field with a value equal to a subscription product ID will change that product in the subscription.

General use:

subscriptionProduct[<subscriptionProductId>][<attributeName>] = <value>

The attributeName's available are listed:

  • variantId
  • quantity
  • options[]

Example Request:

data-ajax-handler="subscriptions:onUpdateSubscriptionProducts"

subscriptionProduct[1][quantity] = 2
subscriptionProduct[1][variantId] = {{ variant.id }}
subscriptionProduct[1][options][{{ option.id }}] = {{ option.id }}

Add a Coupon to the Subscription

  • subscriptions:onAddSubscriptionCoupon - If a customer is logged in, and has a valid subscription as specified by the supplied parameter, this action allows the customer to be able to change the coupon code associated with their subscription.
  • Sending an empty or null value will unset the subscription's coupon code.
  • Valid parameters are either coupon or coupon_code.

Example - A code snippet that will render a form that posts to the action defined above:

{{ flash() }}
{{ open_form({
    'data-ajax-handler': 'subscriptions:onAddSubscriptionCoupon',
    'data-ajax-redirect': root_url('/subscription/' ~ subscription.id)
}) }}
    <input type="text" name="coupon_code" value="{{ subscription.coupon_code }}">
    <input type="submit" value="Add Coupon">
    <input type="hidden" name="subscription_id" value="{{ subscription.id }}"/>
{{ close_form() }}

Example - Setting a Coupon Code via AJAX

<script type="text/javascript">
    $.ajax({
        type: 'POST',
        url: window.location.pathname,
        headers: {
            'X-Event-Handler': 'subscriptions:onAddSubscriptionCoupon'
        },
        data: {
          "coupon_code":"test_code",
          "subscription_id":1
        }
    }).done(function(res, status, xhr) {
         // Do...
    }).fail(function(res, status, xhr) {
         // Do..
    });
</script>

Example - Unsetting a Coupon Code via AJAX

<script type="text/javascript">
    $.ajax({
        type: 'POST',
        url: window.location.pathname,
        headers: {
            'X-Event-Handler': 'subscriptions:onAddSubscriptionCoupon'
        },
        data: {
          "coupon_code":"",
          "subscription_id":1
        }
    }).done(function(res, status, xhr) {
         // Do...
    }).fail(function(res, status, xhr) {
         // Do..
    });
</script>

Update Next Renewal Date

The subscription page supports a field for changing the subscription's next renewal date:

  • next_invoice_date - Date in format: MM/DD/YYYY

Requires:

  • subscription_id 
  • Ajax handler: subscriptions:subscription

Example:

{% if subscription.canReschedule() %}
  {{ open_form({
    'data-ajax-handler': 'subscriptions:subscription',
    'data-ajax-redirect' : root_url('/subscription/' ~ subscription.id)
  }) }}
    <input type="hidden" name="subscription_id" value="{{ subscription.id }}"/>
    <input type="text" value="04/06/2018" name="next_invoice_date"/>
    <input type="submit" class="button" value="Save Subscription"/>
  {{ close_form() }}
{% endfor %}

Manually Generate an Extra Paid Order

LemonStand supports a handler for customers creating an extra paid order at any time. 

Requires:

  • Ajax Handler: subscriptions:onGenerateOrder
  • subscription_id 

Example:

{% if subscription.canGenerateOrder() %}
  {{ open_form() }}
    <h3>Create an extra paid order:</h3>
    <input type="hidden" name="subscription_id" value="{{ subscription.id }}"/>
    <a href="#" class="button"
      data-ajax-handler="subscriptions:onGenerateOrder"
      data-ajax-redirect="{{ root_url('/subscription/' ~ subscription.id) }}"
    >Go</a>
  {{ close_form() }}
{% endif %}

Skip Upcoming Billing Cycles

The subscription page supports a field for customers skipping a specific number of billing cycles. The logic is the same as changing the next invoice date, for example:

  • Renewal monthly, next date is April 25th.
  • Skip 3 cycles applied.
  • Next invoice date is set to July 25th (skips April 25, May 25, June 25).
  • Subscription will show the next invoice date as July 25th in the backend/frontend, instead of original date April 25th.

Requires:

  • Ajax Handler: subscriptions:onSkipBillingCycles
  • skip_billing_cycles - Format: Integer
  • subscription_id 

Example:

{% if subscription.canReschedule() %}

  {{ open_form() }}
    <input type="hidden" name="subscription_id" value="{{ subscription.id }}"/>
    <input type="number" name="skip_billing_cycles" value="3"/>
    <a href="#" class="button"
      data-ajax-handler="subscriptions:onSkipBillingCycles"
      data-ajax-redirect="{{ root_url('/subscription/' ~ subscription.id) }}"
    >Skip</a>
  {{ close_form() }}

{% endif %}


Customer Edit Subscription Products Example

We can build a subscription product editor form that allows us to add, remove, and change subscription products - all without reloading the page. 

The pieces included are all covered in the above documentation. Notice this requires some JavaScript code at the bottom, to dynamically change the form inputs depending on what the user clicks.

Example:

{{ flash() }}
 
<h2>Update Billing Plan</h2>
 
{{ open_form({
    'data-ajax-handler': 'subscriptions:onUpdateBillingPlan',
    'data-ajax-redirect': root_url('/subscription/' ~ subscription.id)
}) }} 
    <select id="billing_plan" name="billing_plan">
        {% for plan in billingPlans %}
            <option value="{{ plan.id }}" {{ option_state(plan.id, subscription.billingPlan.id) }}>
                {{ plan.name }}
            </option>
        {% endfor %}
    </select> 
    <input type="submit" class="button" value="Update Billing Plan"/>
    <input type="hidden" name="subscription_id" value="{{ subscription.id }}"/>
{{ close_form() }} 
 
<h2>Order History</h2>
 
{% if not subscription.orders or not subscription.orders.count %} 
    <p class="flash info">Orders not found</p>
{% else %} 
    <table>
        <tr>
            <th>#</th>
            <th>Date</th>
            <th>Status</th>
            <th>Total</th>
        </tr>
        {% for order in subscription.orders if not order.is_quote %} 
            {% set url = root_url('/order/'~order.id) %} 
            <tr>
                <td><a href="{{ url }}">{{ order.number }}</a></a></td>
                <td><a href="{{ url }}">{{ order.created_at.format('M jS, Y') }}</a></td>
                <td><a href="{{ url }}">{{ order.orderStatus.name }}</a></td>
                <td><a href="{{ url }}"><i>{{ order.total|currency }}</i></a></td>
            </tr>
        {% endfor %} 
    </table>
{% endif %} 
<h2>Add Coupon Discount</h2>
{{ open_form({
    'data-ajax-handler': 'subscriptions:onAddSubscriptionCoupon',
    'data-ajax-redirect': root_url('/subscription/' ~ subscription.id)
}) }}
    <input type="text" name="coupon_code" value="{{ subscription.coupon_code }}">
    <input type="submit" name="Add Coupon">
    <input type="hidden" name="subscription_id" value="{{ subscription.id }}"/>
{{ close_form() }}
 
<h2>Update Subscription Status</h2>
 
{{ open_form({
    'data-ajax-handler': 'subscriptions:onUpdateSubscriptionStatus',
    'class': 'custom', 'data-ajax-redirect': root_url('/subscription/' ~ subscription.id)
}) }}
    <select id="subscription_status" name="subscription_status">
        {% for status in subscriptionStatuses 
            if (status.code != 'past-due') 
            and (status.code != 'trial') 
        %}
            <option value="{{ status.code }}" {{ option_state(status.code, subscription.status.code) }}>
                {{ status.name }}
            </option>
        {% endfor %}
    </select>
    <input type="submit" class="button" value="Update Status"/>
    <input type="hidden" name="subscription_id" value="{{ subscription.id }}"/>
{{ close_form() }}
 
<h2>Change Subscription Products</h2>
 
{{ open_form({
    'data-ajax-handler': 'subscriptions:subscription',
    'data-ajax-redirect' : root_url('/subscription/' ~ subscription.id)
}) }}
    <input type="hidden" name="subscription_id" value="{{ subscription.id }}"/>
    <table id="subscription-products">
        <tr>
            <th>Name</th>
            <th>Quantity</th>
            <th>Price</th>
            <th></th> <!-- remove row -->
        </tr>
        {% for subscriptionProduct in subscription.products %} 
            {% set url = root_url('/product/'~subscriptionProduct.product.url_name) %}
            {% set product = subscriptionProduct.product %}
            <tr>
                <td>
                    <a href="{{ url }}"><img src="{{ subscriptionProduct.product.images.first.thumbnail(80, 80)|default('http://placehold.it/80x80') }}"/></a>
                    <h3><a href="{{ url }}">{{ subscriptionProduct.product.name }}</a></h3>
                    {% if product.options.count %}
                        {% for index, option in product.options %}
                            <label for="subscriptionProduct[{{ subscriptionProduct.id }}][options][{{ option.id }}]">{{ option.name }}</label>
                            <select name="subscriptionProduct[{{ subscriptionProduct.id }}][options][{{ option.id }}]">
                              {% for id, value in option.values %}
                                <option value='{{ id }}' {{ option_state(value, subscriptionProduct.options[option.id].value) }}>{{ value }}</option>
                              {% endfor %}
                            </select>
                        {% endfor %}
                    {% endif %}
                </td>
                <td><input type="text" name="subscriptionProduct[{{ subscriptionProduct.id }}][quantity]" value="{{ subscriptionProduct.quantity }}"/></td>
                <td><a href="{{ url }}"><i>{{ subscriptionProduct.price|currency }}</i></a></td>
                <td class="remove">
                    <input disabled type="hidden" name="deleteProducts[{{ subscriptionProduct.id }}]" value="{{ subscriptionProduct.id }}"/>
                    <a class="js-delete" href="javascript:void(0)">Remove</a>
                </td>
            </tr>
        {% endfor %}
    </table>
    <input type="submit" class="button" value="Save Subscription"/>
{{ close_form() }}
 
<h2>Add Products</h2>
 
<table>
    <tr>
        <th>Name</th>
        <th>Quantity</th>
        <th>Price</th>
        <th></th>
    </tr>
    {% for product in products %} 
        {% set url = root_url('/product/'~subscriptionProduct.product.url_name) %} 
        <tr>
            <input type="hidden" name="productIds[{{ product.sku }}]" value="{{ product.id }}"/>
            <td>
                <a href="{{ url }}"><img src="{{ product.images.first.thumbnail(80, 80)|default('http://placehold.it/80x80') }}"/></a>
                <h3><a href="{{ url }}">{{ product.name }}</a></h3>
                {% if product.options.count %}
                    {% for index, option in product.options %}
                        <label for="options[{{ product.sku }}][{{ option.id }}]">{{ option.name }}</label>
                        <select name="options[{{ product.sku }}][{{ option.id }}]">
                          {% for id, value in option.values %}
                            <option value='{{ id }}'>{{ value }}</option>
                          {% endfor %}
                        </select>
                    {% endfor %}
                {% endif %}
            </td>
            <td><input type="text" name="quantities[{{ product.sku }}]" value="1"/></td>
            <td><a href="{{ url }}"><i>{{ product.price|currency }}</i></a></td>
            <td style="display: none;" class="remove">
                <a class="js-delete" href="javascript:void(0)">Remove</a>
            </td>
            <td>
                <a class="js-add button" href="javascript:void(0)">Add to subscription</a>
            </td>
        </tr>
    {% endfor %}
</table>
 
<script type="text/javascript">
    $(document).on('click', '.js-delete', function () {
        // Add delete ID to form
        $(this).prev('input').clone().appendTo('#subscription-products').prop('disabled', false);
        $(this).closest('tr').remove();
    });
    $(document).on('click', '.js-add', function () {
        // Add product row including ID input to form
        $rowClone = $(this).closest('tr').clone();
        $rowClone.appendTo('#subscription-products')
            .find('td.remove').toggle() // show delete button
            .next().toggle();           // hide add button
        /**
         * jQuery select clone() bug workaround - copies select dropdown value to cloned select
         */
        var selects = $(this).closest('tr').find("select");
        $(selects).each(function(i) {
            var select = this;
            $rowClone.find("select").eq(i).val($(select).val());
        });
    });
</script>