Implementing Subscriptions

Please see this article for information on implementing Subscriptions in a theme.

Prerequisites

Subscribe on the Product Page

Subscribing on the product page requires only a few extra pieces on top of the standard product page. Our first step is to display our new Subscription Billing Plans.

Navigate to your theme's product page. For most themes the url looks like this:

<your-store>.com/product/<product-url-name>

Displaying billing plans in a dropdown

The product page supports the product_billing_plan field. We can use it like this:

product_billing_plan: {{ plan.id }}

The value for this parameter should equal a billing plan's ID. 

To see your product's enabled billing plans, use the twig variable available under your product:

{{ product.subscriptionPlans }}

Since this is an array of plans, we should loop through them to properly display their attributes:

{% for plan in product.subscriptionPlans %}
    {{ plan.name }}
    {{ plan.id }}
{% endfor %}

To allow our customers to choose a plan in a dropdown, we create a <select> tag that will list out the available billing plan options. Putting it together we get our finished billing plan dropdown:

<label for="product-billing-plan">Subscription plan</label>
<select id="product-billing-plan" name="product_billing_plan"> 
    {% for plan in product.subscriptionPlans %}
        <option value="{{ plan.id }}">{{ plan.name }}</option>
    {% endfor %}
</select>

Notice the product_billing_plan parameter used for the <select> tag's name attribute. This defines the plan that will be attached to the product once it's in our cart.

Adding subscription products to the cart

The minimal product page button for adding products to your cart looks like this:

<input type="hidden" name="productId" value="{{ product.id }}"/>
<a href="#" class="button"
    data-ajax-handler="shop:onAddToCart" 
    data-ajax-update="#mini-cart=shop-minicart, #product-page=shop-product"
>Add to Cart</a>

Here, the productId field defines which product is being added. We can use it like this:

productId: {{ product.id }}

To extend this for Subscriptions, we add the Billing Plan dropdown together with the standard Add to Cart button:

<input type="hidden" name="productId" value="{{ product.id }}"/>
<label for="quantity">Quantity</label>
<input type="text" value="{{ quantity|default("1") }}" id="quantity" name="quantity"/>
<label for="product-billing-plan">Subscription plan</label>
<select id="product-billing-plan" name="product_billing_plan"> 
    {% for plan in product.subscriptionPlans %}
        <option value="{{ plan.id }}">{{ plan.name }}</option>
    {% endfor %}
</select>
<a href="#" class="button" 
    data-ajax-handler="shop:onAddToCart" 
    data-ajax-update="#mini-cart=shop-minicart, #product-page=shop-product"
>Subscribe</a>

Notice the Ajax data attributes specifying two things:

  1. The shop:onAddToCart handler, so clicking the button registers an "Add to Cart" event.
  2. The partials we want to reload into our HTML elements:
    - Element of Id #mini-cart reloads the shop-minicart partial contents
    - Element of Id #product-page reloads the shop-product partial 

The quantity field was also added, which lets customers choose how many products to add to their cart.

Subscribe on the Cart Page

Subscribing on the cart page requires only a few extra pieces on top of the standard cart page.

Navigate to your theme's cart page. For most themes the url looks like this:

<your-store>.com/cart

Assign a Billing Plan to a Cart Item

The cart page supports the item_subscription_plan[] field. Passing this field with a value equal to a billing plan ID will assign the plan to the cart item. Since it's an array, we need to use the cart item's key as the array key value. Using it should look like this

item_subscription_plan[<item-key>]: {{ plan.id }}

Cart items can be accessed through the variable {{ items }} . All of our themes have cart pages built-in, that loop through the cart items like this:

{% for item in items %}
    <!-- Display items in a list -->
    {{ item.quantity }}
    <!-- ... -->
{% endfor %}

In this example we'll be able to assign a plan to each of these items individually. This way products can be purchased on a subscription plan at the same time (and in the same checkout) as a regular non-subscription product.

We'll need to create a billing plan dropdown for our customers to choose between different subscription plans. To loop through our subscription plans we'll use the global variable {{ billingPlans }}like this:

{% for plan in billingPlans %}
  <option value="{{ plan.id }}">{{ plan.name }}</option>
{% endfor %}

We can place this into a <select> tag to create a dropdown of subscription plan options. We'll also place it within the cart item loop to finish it off:

{% for item in items %}
    <!-- Display items in a list -->
    <select id="billing_plan" 
        name="item_subscription_plan[{{ item.key }}]" 
        data-ajax-handler="shop:cart" 
        data-ajax-update="#cart-content=shop-cart-content, #mini-cart=shop-minicart"
    > 
        <option value="">-- No subscription --</option>
        {% for plan in billingPlans %}
            <option value="{{ plan.id }}" {{ option_state(plan.id, item.x_billing_plan_id) }}>{{ plan.name }}</option>
        {% endfor %}
    </select>
{% endfor %}

Notice the Ajax data attributes specifying two things:

  1. The shop:cart handler, so changing the dropdown registers a "Cart" event.
  2. The partials we want to reload into our HTML elements:
    - Element of Id #mini-cart reloads the shop-minicart partial contents
    - Element of Id #cart-content reloads the shop-cart-content partial contents

Assign a Billing Plan to all Items in the Cart

The cart page supports the product_billing_plan field. We can pass this field with a value equal to the billing plan ID to assign that plan to all items in the cart, like this:

product_billing_plan: {{ plan.id }}

Placing this into a dropdown with the " shop:cart" ajax="" handler:<="" p="">

<label for="billing_plan">Billing plan</label>
<select id="billing_plan" 
    name="product_billing_plan" 
    data-ajax-handler="shop:cart"
    data-ajax-update="#cart-content=shop-cart-content, #mini-cart=shop-minicart"
> 
    <option value="">-- No subscription --</option>
    {% for plan in billingPlans %}
      <option value="{{ plan.id }}" {{ option_state(plan.id, cart_billing_plan) }}>{{ plan.name }}</option>
    {% endfor %}
</select>

We can place this anywhere in our cart page.

Assign a Billing Plan to the Cart

The cart page supports the billing_plan field. 

We can pass this field with a value equal to the billing plan ID to assign that plan to the cart, like this:

billing_plan: {{ plan.id }}

This field changes the billing plan of your entire cart. This does not apply plans to your cart items individually, so using this field means your customers cannot purchase one-time products and subscription products at the same time. 

Use the same dropdown as in the above example under Assign a Billing Plan to all Items in the Cart.

Assign a Cart Billing Plan from the Product Page

The product page supports the billing_plan field. 

We can pass this field with a value equal to the billing plan ID to assign that plan to the cart, like this:

billing_plan: {{ plan.id }}

This field changes the billing plan of your entire cart. This does not apply plans to your cart items individually, so using this field means your customers cannot purchase one-time products and subscription products at the same time. 

For an example of this, follow the steps under Adding Subscription Products to Cart but replace the product_billing_plan field with billing_plan .

Subscribe from a List of Products

Having predefined subscriptions is a common way to sell products such as subscription boxes, where customers choose a single subscription option from a few different variants.

To add this to a theme we start by navigating to or creating our Products page.

The product list looks like this:

<ul>
  {% for product in products %}
    {% set is_on_sale = product.onSale %}
    {% set page_url = '/product/' ~ product.url_name %}
    <li>
      {{ open_form() }}
      <div class="product">
        <div>
          <a href="{{ page_url }}"><img src="{{ product.images.first.thumbnail(365, 365)|default('http://placehold.it/365x365') }}" width="365" height="365" alt="{{ product.images.first.description }}" title="{{ product.images.first.title }}"/></a>
        </div>
        <div class="short-description">
            <h3>{{ product.name }}</h3>
                <em>
                  {% if is_on_sale %}{{ product.fullPrice|currency }}{% endif %}
                  {{ product.price|currency }}
                </em>
            
            <p>
              <a class="button" href="#" data-ajax-handler="shop:onAddToCart">Subscribe</a>
            </p>
        </div>
        {% if is_on_sale %}SALE{% endif %}
      </div>
      <input type="hidden" name="productId" value="{{ product.id }}">
      <input type="hidden" name="billing_plan" value="30">
      <input type="hidden" name="subscribe" value="1">
      <input type="hidden" name="subscribeRedirect" value="/cart">
      <input type="hidden" name="noFlash" value="1">
      {{ close_form() }}
    </li>
  {% else %}
    <li class="empty">No products found</li>
  {% endfor %}
</ul>

The important inputs are the billing_plan , subscribe , and subscribeRedirect fields. These define consecutively the billing plan ID to subscribe to, a flag that we are subscribing, and page to redirect to.

Displaying Trial Incentives

Read about Billing Plan Incentives under the Billing Plan documentation. You can find "Trial Incentives" under each individual Subscription Billing Plan in your store backend.

There are two twig variables we can display, relating to items and totals in the cart:

1. {{ item.is_trial_product }}

  • Cart Item identifier against trial products.
  • Returns a true/false value if the cart item is a trial product.

2. {{ totals.first_order_discount }}

  • Returns the actual discount as a flat number or percentage, e.g.; "10%" or "10".
  • Real discount amount is be calculated and included in the Cart's {{ totals.discountTotal }}

Note that these are limited to the cart page.

Display Subscription Products Not Being Charged Immediately In Mixed Carts

Read about Charging Customers On Subscription Creation under the Billing Plan documentation.

When a cart is mixed and a subscription product is associated with a subscription billing plan that does not charge on creation, we split the subscription product cart items off into their own collection, accessible through the variable upcomingItems, and totals will be accessible through the variable upcomingTotals. These variables are visible in both cart and checkout pages, and if defined, should be rendered alongside items and totals so that storefront customers can see both items that will be paid for in the checkout as well as subscription products that will eventually be charged and shipped.

Example - Rendering upcomingItems alongside items

{% if upcomingItems | length > 0  %}
  {{ 
      partial('cart-contents-items', {
          listTitle: 'Upcoming subscription items',
          items: upcomingItems,
          checkoutMode: checkoutMode
      })
  }}
  
  {{ 
      partial('cart-contents-items', {
          listTitle: 'Items shipping immediately',
          items: items,
          checkoutMode: checkoutMode
      })
  }}
{% else %}
  {{ partial('cart-contents-items', { items: items }) }}
{% endif %}

Example - Rendering upcomingTotals alongside totals

{{
  partial('checkout-totals', {
    totalTitle: 'Total (shipping immediately)',
    totals: totals,
    checkoutMode: checkoutMode
  })
}}
{% if upcomingTotals %}
    {{
      partial('checkout-totals', {
        totalTitle: 'Total (upcoming subscription items)',
        totals: upcomingTotals,
        checkoutMode: checkoutMode,
        hideCouponCode: true
      })
    }}
{% endif %}

Display Upcoming Subscription Orders On Receipt Pages

Read about Charging Customers On Subscription Creation under the Billing Plan documentation.

When a mixed cart checkout is completed, and a subscription product in the checkout is associated with a subscription billing plan that does not charge on creation, we:

  • create two orders
  • one for items immediately purchased
  • one for the upcoming subscription order (to be charged at a later date)
  • associate the immediately purchased order with the upcoming order.

Orders that have immediately been paid for act as parents to an upcoming order.

Rendering on the Receipt Page

The following methods/accessors have been added to order objects to allow for easy theming in the receipt page:

  • order.hasUpcomingOrders() - returns true if an order is associated with an upcoming order.
  • order.upcomingOrders - provides access to upcoming orders associated with a placed/paid order.

Example - Rendering upcomingOrders in a receipt page.

{% if invoice.order.hasUpcomingOrders() %}
  {% for upcomingOrder in invoice.order.upcomingOrders %}
        {{ partial('order-header', { title: 'Upcoming Order Receipt', order: upcomingOrder }) }}
        {{ partial('cart-contents', { order: upcomingOrder, checkoutMode: true, hideCouponCode: true }) }}
        {{ partial('order-totals', { order: upcomingOrder }) }}
  {% endfor %}
{% endif %}

Save Checkout Addresses To Subscriptions

Subscriptions renew against a customer's default billing and shipping address unless you specify the save_subscription_checkout_address=1 flag when attempting a payment. The subscription saves the addresses as part of the checkout and will continue to re-bill against these custom addresses as the subscription renews in the future. You will need to update your payment form to pass this flag to enable custom subscription addresses.

Example - Saving Subscription Addresses On Payment Form

{{
    paymentForm({
          options: {
            number: {
              label: 'Card Number',
              placeholder: ' ',
              style: "font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif; font-size: 16px; color: #0a0a0a; width: 100%;"
            },
            cvv: {
              label: 'CVV',
              placeholder: ' ',
              style: "font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif; font-size: 16px; color: #0a0a0a; width: 100%;"
            },
            is_default: {
              label:'Make my default card'
            },
            expiry: {
              label:'Expiry (MM/YYYY)',
              placeholder: ''
            },
            full_name: {
              label:'Cardholder Name',
              placeholder: ''
            },
            save_card: {
              label: 'Save Card',
              enabled: saved_card_enabled
            }
          }
    }, paymentMethod, {
        'data-ajax-extra-fields':'save_subscription_checkout_address=1'
    })
}}