# Adding selling plan options to your Shopify product pages

## Before you begin

* First, follow the [setup guide](https://help.firmhouse.com/en/articles/9899679-setting-up-shopify-native-checkout-and-subscriptions) to connect your store to Firmhouse

## Let's get started

{% hint style="warning" %}
This article is based on [Dawn theme v15](https://github.com/Shopify/dawn). If you are using another theme or version, the steps might be slightly different.
{% endhint %}

{% hint style="info" %}
To update your theme without affecting the live version, duplicate it, make your changes, and publish the updated version when ready.
{% endhint %}

<figure><img src="/files/L0qKm583cBoFINleP0NF" alt=""><figcaption></figcaption></figure>

### Add the Selling Plan Selector to Your Product Page Template

<figure><img src="/files/aF55XZGqFWBSkxbnofsc" alt=""><figcaption><p>Selling plan selector on product page</p></figcaption></figure>

1. Create `/snippets/selling-plans-integration.liquid`. This snippet defines the selling plan selector.

<details>

<summary><code>/snippets/selling-plans-integration.liquid</code></summary>

```liquid
{% assign firmhouse_app_id = '4631435' %}
{% if product.selling_plan_groups.size > 0 %}
  {% liquid
    assign app_string = 'app--' | append: firmhouse_app_id | append: '--firmhouse'
    assign native_selling_plans = shop.metaobjects[app_string].subscriptions.selling_plan_mapping.value | map: 'native'
    assign current_variant = product.selected_or_first_available_variant | default: product.variants.first
  %}
  <div class="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;
      }
      .selling_plan_app_plans_list {
        list-style: none;
        padding: 0;
        margin: 0;
      }
    </style>
    {% for variant in product.variants %}
      {% liquid
        assign variant_price = variant.price | money_with_currency | escape
        assign variant_compared_at_price = variant.compare_at_price | money_with_currency | escape
      %}
      {% if variant.selling_plan_allocations.size > 0 %}
        <section
          data-variant-id="{{ variant.id }}"
          class="selling_plan_theme_integration {% if variant.id != current_variant.id %}selling_plan_theme_integration--hidden{% endif %}"
        >
          <fieldset>
            <legend>Frequency</legend>
            <div>
              <div>
                <ul class="selling_plan_app_plans_list">
                  {% unless product.requires_selling_plan %}
                    <li>
                      <input
                        id="sp-one-time-purchase"
                        aria-label="One-time purchase. Product price {{ variant_price }}"
                        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="{{ variant_price }}"
                        data-variant-compare-at-price="{{ variant_compared_at_price }}"
                        checked
                      >
                      <label for="sp-one-time-purchase">One-time purchase</label>
                    </li>
                  {% 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
                    %}
                    {% for allocation in allocations %}
                      {% assign selling_plan_id = 'gid://shopify/SellingPlan/' | append: allocation.selling_plan.id %}
                      {% unless native_selling_plans contains selling_plan_id %}
                        {% continue %}
                      {% endunless %}
                      {% liquid
                        if forloop.first and product.requires_selling_plan and first_selling_plan_group
                          assign plan_checked = 'checked'
                        else
                          assign plan_checked = null
                        endif

                        assign allocation_price = allocation.price | money_with_currency | escape
                        assign allocation_compared_at_price = allocation.compare_at_price | money_with_currency | escape
                        assign input_id = 'sp-' | append: variant.id | append: '-' | append: allocation.selling_plan.id
                      %}
                      <li>
                        <input
                          id="{{ input_id }}"
                          type="radio"
                          {% if variant.available == false %}
                            disabled
                          {% endif %}
                          aria-label="{{ allocation.selling_plan.name }}. Product price {{ allocation_price }}"
                          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="{{ allocation_price }}"
                          data-variant-compare-at-price="{{ allocation_compared_at_price }}"
                          {{ plan_checked }}
                        >
                        <label for="{{ input_id }}">
                          {{ allocation.selling_plan.name }}
                        </label>
                      </li>
                    {% endfor %}
                  {% endfor %}
                </ul>
              </div>
            </div>
          </fieldset>
        </section>
      {% endif %}
    {% endfor %}
  </div>
  <input
    name="selling_plan"
    class="selected-selling-plan-id"
    type="hidden"
  >
{% endif %}

```

</details>

2. Create `/assets/selling-plans-integration.js`. This file contains the logic for the selling plan selector.

<details>

<summary><code>/assets/selling-plans-integration.js</code></summary>

```javascript
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');
  }

  get comparedAtPrice() {
    return this.shopifySection.querySelector('.price__sale');
  }

  get visibleSellingPlanForm() {
    return this.shopifySection.querySelector(
      `section[data-variant-id^="${this.variantIdInput.value}"]`,
    );
  }

  get isVariantAvailable() {
    return this.selectedPurchaseOption.getAttributeNames().includes('disabled');
  }

  get sellingPlanInput() {
    return this.shopifySection.querySelector('.selected-selling-plan-id');
  }

  get addToCartForms() {
    return this.shopifySection.querySelectorAll('[action*="/cart/add"]');
  }

  /*
    To enable the addition of a selling plan to a cart, it's necessary to include an input with the name "selling_plan", which will carry the selling ID as its value. When a buyer clicks on 'add to cart', the appropriate selling plan ID is added to their cart.
  */
  appendSellingPlanInputs() {
    this.addToCartForms.forEach((addToCartForm) => {
      addToCartForm.appendChild(this.sellingPlanInput.cloneNode());
    });
  }

  showSellingPlanForm(sellingPlanFormForSelectedVariant) {
    sellingPlanFormForSelectedVariant?.classList?.remove(hiddenClass);
  }

  hideSellingPlanForms(sellingPlanFormsForUnselectedVariants) {
    sellingPlanFormsForUnselectedVariants.forEach((element) => {
      element.classList.add(hiddenClass);
    });
  }

  /*
    Each product variant comes with a selling plan selection box that the buyer can interact with.
    When a buyer chooses a different variant, we ensure that only the relevant selling plan selection box is displayed.
    This guarantees that only the selling plan associated with the selected variant is shown.
  */
  handleSellingPlanFormVisibility() {
    const sellingPlanFormForSelectedVariant = this.shopifySection.querySelector(
      `section[data-variant-id="${this.variantIdInput.value}"]`,
    );
    const sellingPlanFormsForUnselectedVariants =
      this.shopifySection.querySelectorAll(
        `.selling_plan_theme_integration:not([data-variant-id="${this.variantIdInput.value}"])`,
      );
    this.showSellingPlanForm(sellingPlanFormForSelectedVariant);
    this.hideSellingPlanForms(sellingPlanFormsForUnselectedVariants);
  }

  handleVariantChange() {
    this.handleSellingPlanFormVisibility();
    this.updateSellingPlanInputsValues();
    this.listenToSellingPlanFormRadioButtonChange();
  }

  /*
    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.
  */
  listenToVariantChange() {
    this.listenToAddToCartForms();
    if (this.variantIdInput.tagName === 'INPUT') {
      const variantIdObserver = new MutationObserver((mutationList) => {
        mutationList.forEach((mutationRecord) => {
          this.handleVariantChange(mutationRecord.target.value);
        });
      });

      variantIdObserver.observe(this.variantIdInput, {
        attributes: true,
      });
    }
  }

  listenToAddToCartForms() {
    this.addToCartForms.forEach((addToCartForm) => {
      addToCartForm.addEventListener('change', () => {
        this.handleVariantChange();
      });
    });
  }

  get regularPriceElement() {
    return this.shopifySection.querySelector('.price__regular');
  }

  get salePriceElement() {
    return this.shopifySection.querySelector('.price__sale');
  }

  get salePriceValue() {
    return this.salePriceElement.querySelector('.price-item--sale');
  }

  get regularPriceValue() {
    return this.salePriceElement.querySelector('.price-item--regular');
  }

  get sellingPlanAllocationPrice() {
    return document.getElementById(
      `${this.selectedPurchaseOption.dataset.sellingPlanGroupId}_allocation_price`,
    );
  }

  get selectedPurchaseOptionPrice() {
    return this.selectedPurchaseOption.dataset.variantPrice;
  }

  get selectedPurchaseOptionComparedAtPrice() {
    return this.selectedPurchaseOption.dataset.variantCompareAtPrice;
  }

  get price() {
    return this.sellingPlanAllocationPrices.price ?? null;
  }

  /*
    We aim to ascertain whether a compared price exists, which would indicate that the currently selected input has a discount applied to it.
    If a discount is detected, the discounted price is displayed; otherwise, the regular price is shown.
  */
  updatePrice() {
    if (
      !this.selectedPurchaseOptionComparedAtPrice ||
      this.selectedPurchaseOptionComparedAtPrice ===
        this.selectedPurchaseOptionPrice
    ) {
      this.showRegularPrice();
      this.hideSalePrice();
      this.priceElement.classList.remove('price--on-sale');
    } else {
      this.showSalePrice();
      this.hideRegularPrice();
      this.priceElement.classList.add('price--on-sale');
    }
  }

  hideSalePrice() {
    this.salePriceElement.style.display = 'none';
  }

  hideRegularPrice() {
    this.regularPriceElement.style.display = 'none';
  }

  showRegularPrice() {
    this.regularPriceElement.style.display = 'block';
    this.shopifySection.querySelector('.price__sale').style.display = 'none';
  }

  showSalePrice() {
    this.salePriceElement.style.display = 'block';
    this.regularPriceValue.innerHTML =
      this.selectedPurchaseOptionComparedAtPrice;
    this.salePriceValue.innerHTML = this.selectedPurchaseOptionPrice;
  }

  get sellingPlanInputs() {
    return this.shopifySection.querySelectorAll('.selected-selling-plan-id');
  }

  updateSellingPlanInputsValues() {
    this.sellingPlanInputs.forEach((sellingPlanInput) => {
      sellingPlanInput.value = this.sellingPlanInputValue;
    });
  }

  get sellingPlanInputValue() {
    return this.selectedPurchaseOption?.dataset.sellingPlanId ?? null;
  }

  get selectedPurchaseOption() {
    return this.visibleSellingPlanForm?.querySelector(
      'input[type="radio"]:checked',
    );
  }

  set selectedPurchaseOption(selectedPurchaseOption) {
    this._selectedPurchaseOption = selectedPurchaseOption;
  }

  handleRadioButtonChange(selectedPurchaseOption) {
    this.selectedPurchaseOption = selectedPurchaseOption;
    this.updateSellingPlanInputsValues();
    this.updatePrice();
  }

  listenToSellingPlanFormRadioButtonChange() {
    this.visibleSellingPlanForm
      ?.querySelectorAll('input[type="radio"]')
      .forEach((radio) => {
        radio.addEventListener('change', (event) => {
          this.handleRadioButtonChange(event.target);
        });
      });
  }

  enablePerformanceObserver() {
    const performanceObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.initiatorType !== 'fetch') return;

        const url = new URL(entry.name);
        /*
          When a buyer selects a product variant, a fetch request is initiated.
          Upon completion of this fetch request, we update the price to reflect the correct value.
        */
        if (url.search.includes('variant') || url.search.includes('variants')) {
          this.updatePrice();
        }
      });
    });

    performanceObserver.observe({entryTypes: ['resource']});
  }
}

document
  .querySelectorAll('.selling_plan_app_container')
  .forEach((sellingPlansWidgetContainer) => {
    new SellingPlansWidget(sellingPlansWidgetContainer);
  });
```

</details>

3. Add the selling plan selector to `sections/main-product.liquid`, ideally before the quantity selector. Search for 'quantity\_selector' to find the block and insert the snippet at the start

```liquid
{% render 'selling-plans-integration', product: product, section: section %}
```

<figure><img src="/files/VLTDKhNEVkOBlBYlRj7Q" alt=""><figcaption><p><code>sections/main-product.liquid</code></p></figcaption></figure>

Now you should have a working selling plan selector on your product page.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://developer.firmhouse.com/guides/setting-up-shopify-native-checkout/adding-selling-plan-options-to-your-shopify-product-pages.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
