Skip to main content

Add subscriptions to your theme

In this tutorial, you'll learn the basics of how to support subscriptions in your theme.

Tip

Refer to Subscription UX guidelines to review user experience considerations that might impact your implementation.


  • Add a product form to a template. A product form can be added to any template that can access the product object.

To support subscriptions in your theme, you'll use the following resources:

ContextExample template typesParent object
Product and variant listingsproductvariant
Variants that have been added to a cart or are part of an orderline_item

For more information about how to present each of these objects and their attributes, refer to Subscription UX guidelines.


Anchor to Implementing subscription displaysImplementing subscription displays

To support subscriptions in your theme, you need to implement the following components:


Anchor to The selling plan selector on the product pageThe selling plan selector on the product page

You can add a selling plan selector for products wherever you can access the product form. For example, you might add a selling plan selector to the product template or a section in the template.

Selling plan groups and individual selling plans have a similar structure to products and variants. You can view selling plan groups like products, where there are multiple options that comprise an individual selling plan, similar to variant options that comprise an individual variant.

You can access the available selling plan options through the selling_plan_groups attribute of the product.

Add the following to your product form:

  • For each selling_plan_group, output each of its options inside the product form.
  • To track the ID of the selected selling plan, add an input with an attribute of name="selling_plan". The value should be the ID of the selected selling plan. If there's no selected selling plan, then the value should be empty.
  • Save the product object so that it can be accessed in JavaScript.

The following example is showing an example on how you can show the selling plan group in your product form. Make sure to add the code inside your product form. The example is referring to the file selling-plans-integration.js, this file is covered on the JavaScript section. The following code is doing the following:

  • Assign the product and the current variant to be used inside the integration
  • Loop through every selling plan group, and display each associated selling plan
  • When the product is only sold as a subscription, we do not allow the buyer to buy the product as a one time purchase
  • Add a Subscription badge to be displayed next to the product price when a buyer selects a subscription
Note

This example demonstrates how to integrate selling plans into your theme. We recommend customizing this integration to suit your specific needs. The following code can also be used as a theme app block.

/assets/selling-plans-integration.liquid

{%- assign current_variant = product.selected_or_first_available_variant | default: product.variants.first -%}

{% if product.selling_plan_groups.size > 0 %}
<div className="selling_plan_app_container" data-section-id='{{ section.id }}'>
<script src="{{ 'selling-plans-integration.js' | asset_url }}" defer></script>
<style>.selling_plan_theme_integration--hidden {display: none;}</style>
{% for variant in product.variants %}
{%liquid
assign variantPrice = variant.price | money_with_currency | escape
assign variantComparedAtPrice = variant.compare_at_price | money_with_currency | escape
%}
{% if variant.selling_plan_allocations.size > 0 %}
<section data-variant-id='{{ variant.id }}' className='selling_plan_theme_integration {% if variant.id != current_variant.id %}selling_plan_theme_integration--hidden{% endif %}'>
<fieldset>
<legend>
{{ block.settings.supporting_text_title }}
</legend>
<div>
{% unless product.requires_selling_plan %}
<div>
<label>
<input
aria-label='One-time purchase. Product price {{ variantPrice }}'
type='radio'
name="purchaseOption_{{ section.id }}_{{ variant.id }}"
{% if variant.available == false %}disabled{% endif %}
id='{{ section.id }}_one_time_purchase'
data-radio-type='one_time_purchase'
data-variant-id='{{ variant.id }}'
data-variant-price='{{ variantPrice }}'
data-variant-compare-at-price='{{ variantComparedAtPrice }}'
checked
/>
One-time purchase
</label>
</div>
{% endunless %}
{% assign group_ids = variant.selling_plan_allocations | map: 'selling_plan_group_id' | uniq %}
{% for group_id in group_ids %}
{%liquid
assign group = product | map: 'selling_plan_groups' | where: 'id', group_id | first
assign allocations = variant | map: 'selling_plan_allocations' | where: 'selling_plan_group_id', group_id

if forloop.first
assign first_selling_plan_group = true
else
assign first_selling_plan_group = false
endif
%}
<div>
<div>
<label>{{ group.name }}</label>
</div>
<ul>
{% for allocation in allocations %}

{%liquid
if forloop.first and product.requires_selling_plan and first_selling_plan_group
assign plan_checked = 'checked'
else
assign plan_checked = nil
endif

assign allocationPrice = allocation.price | money_with_currency | escape
assign allocationComparedAtPrice = allocation.compare_at_price | money_with_currency | escape
%}

<li>
<label>
<input
type='radio'
{% if variant.available == false %}disabled{% endif %}
aria-label='{{ allocation.selling_plan.name }}. Product price {{ allocationPrice }}'
name="purchaseOption_{{ section.id }}_{{ variant.id }}"
data-radio-type='selling_plan'
data-selling-plan-id='{{ allocation.selling_plan.id }}'
data-selling-plan-group-id='{{ section.id }}_{{ group_id }}_{{ variant.id }}'
data-selling-plan-adjustment='{{ allocation.selling_plan.price_adjustments.size }}'
data-variant-price='{{ allocationPrice }}'
data-variant-compare-at-price='{{ allocationComparedAtPrice }}'
{{ plan_checked }} />
{{ allocation.selling_plan.name }}
</label>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</fieldset>
</section>
{% endif %}
{% endfor %}
</div>
<input
name='selling_plan'
className='selected-selling-plan-id'
type='hidden' />
{% endif %}

Anchor to JavaScript to update selling plan informationJavaScript to update selling plan information

JavaScript is used to interact with the theme integration. This will make it possible for a buyer to select a product and add the correct subscription to their cart. You can create a separate file named selling-plans-integration.js inside the Assets folder of your theme. The following example illustrates how JavaScript can interact with the selling plan liquid integration:

/assets/selling-plans-integration.js

const hiddenClass = 'selling_plan_theme_integration--hidden';

class SellingPlansWidget {
constructor(sellingPlansWidgetContainer) {
this.enablePerformanceObserver();
this.sellingPlansWidgetContainer = sellingPlansWidgetContainer;
this.appendSellingPlanInputs();
this.updateSellingPlanInputsValues();
this.listenToVariantChange();
this.listenToSellingPlanFormRadioButtonChange();
this.updatePrice();
}

get sectionId() {
return this.sellingPlansWidgetContainer.getAttribute('data-section-id');
}

get shopifySection() {
return document.querySelector(`#shopify-section-${this.sectionId}`);
}

/*
We are careful to target the correct form, as there are instances when we encounter an installment form that we specifically aim to avoid interacting with.
*/
get variantIdInput() {
return (
this.addToCartForms[1]?.querySelector(`input[name="id"]`) ||
this.addToCartForms[1]?.querySelector(`select[name="id"]`) ||
this.addToCartForms[0].querySelector(`input[name="id"]`) ||
this.addToCartForms[0].querySelector(`select[name="id"]`)
);
}

get priceElement() {
return this.shopifySection.querySelector('.price');
}
  • The functions listenToVariantChange() and listenToAddToCartForms() are implemented to track when a product variant is altered or when the product form is updated. The identification of the variant is crucial as it dictates which selling plan box should be displayed. For more information about how to find a variant, refer to our community post.

Anchor to The selling plan display in the cartThe selling plan display in the cart

If a customer selects a selling plan on the product page, then they should see that selection in the cart.

Available selected selling plans are accessible through the selling_plan_allocation attribute of the line_item object. The following is an example:

/customers/order.liquid

{% if line_item.selling_plan_allocation %}
<p className="selling-plan">{{ line_item.selling_plan_allocation.selling_plan.name }}</p>
{% endif %}

Anchor to The selling plan selectorThe selling plan selector

Rather than just display the selected selling plan, you can give customers the option to add a new selling plan, or to remove or edit the current selling plan. To do this, you should implement a selling plan selector that lists out the available selling plans for the line item's variant, and reflects the currently selected selling plan.

You can loop through the selling_plan_allocations attribute of the variant object associated with the line item (line_item.variant) to build out your selector options. You can compare the selected selling plan ID with the ID of the selling plan at the current index of the loop to make sure that the selector reflects the currently selected selling plan.

To change the selling plan for a line item, you can use the /{locale}/cart/change.js endpoint of the Cart AJAX API.

The following example outputs a selling plan selector:

<select name="selling-plan" data-line="{{ forloop.index }}" data-quantity="{{ line_item.quantity }}">
<option value="">One-time purchase</option>

{% for selling_plan_allocation in line_item.variant.selling_plan_allocations %}
<option
value="{{ selling_plan_allocation.selling_plan.id }}"
{% if line_item.selling_plan_allocation.selling_plan.id == selling_plan_allocation.selling_plan.id %}selected="selected"{% endif %}
>
{{ selling_plan_allocation.selling_plan.name }}
</option>
{% endfor %}
</select>

The following example illustrates the concept of watching for a change in the selling plan selector and applying those changes through the /cart/change.js endpoint. It isn't completely functional.

const sellingPlanSelectors = document.querySelectorAll('[name="selling-plan"]');

sellingPlanSelectors.forEach(function(element) {
element.addEventListener('change', function(event) {
const data = {
'line': event.target.dataset.line,
'quantity': event.target.dataset.quantity,
'id': event.target.value
}

fetch('/cart/change.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => {
// Refresh page, or re-render cart
console.log(response);
})
.catch((error) => {
console.error('Error:', error);
});
});
});

Anchor to The checkout charge display in the cartThe checkout charge display in the cart

Because pre-order and TBYB can change how much a customer has to pay up front, you should show them how much they'll be charged at checkout. You can calculate this amount using selling_plan.checkout_charge object.

You can access a line item's checkout charge through its selling_plan_allocation.

The following table outlines the types of checkout charges:

Checkout charge typeDescription
percentageA percent value representing the percentage amount of the full price that must be paid up front.
priceThe price to be paid up front, in cents.
Note

You can't configure checkout charges for subscriptions. Because of this, subscriptions always have a value_type of percentage and value of 100.

The following example outputs the appropriate line item price depending on whether the line item has a selling plan allocation, and what kind of selling plan it is.

{% if item.selling_plan_allocation %}
{%- assign checkout_charge = item.selling_plan_allocation.selling_plan.checkout_charge -%}

{% if checkout_charge.value_type == 'percentage' %}
{{ item.original_price | times: checkout_charge.value | divided_by: 100 | money }}
{% else %}
{{ checkout_charge.value | money }}
{% endif %}
{% else %}
{{ item.original_price | money }}
{% endif %}

Anchor to Customer order selling plan displayCustomer order selling plan display

When a customer selects a selling plan, they should see the name of that selection on the customer order page.

The selected selling plan, if there is one, is accessible through the selling_plan_allocation attribute of the line_item object. The following is an example:

/customers/order.liquid

{% if line_item.selling_plan_allocation %}
<p className="selling-plan">{{ line_item.selling_plan_allocation.selling_plan.name }}</p>
{% endif %}

Was this page helpful?