# 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="https://2065262705-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTbA6hGFVeEriQvTMtvtQ%2Fuploads%2Fgit-blob-17b66be0197a29075f3f1b2eec0409545adcf4b7%2Fshopify-duplicate.webp?alt=media" alt=""><figcaption></figcaption></figure>

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

<figure><img src="https://2065262705-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTbA6hGFVeEriQvTMtvtQ%2Fuploads%2Fgit-blob-e902ccd8d0d6fdc4ddc0a6283f3a857687721319%2Fimage.png?alt=media" 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="https://2065262705-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTbA6hGFVeEriQvTMtvtQ%2Fuploads%2Fgit-blob-d393271192525f10fdbfaff9bc90f2349821d83f%2Fimage.png?alt=media" 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.
