Join us for an upcoming Shopify Partner webinar on February 27, 2024. Discover the latest Checkout Extensibility features, and deep dive on improvements to Shopify Functions and Web Pixels. Register now for either the 10am EST or 2pm EST sessions.

Script editor does not detect subscription products if changed using cart API

osea-malibu
Shopify Partner
4 0 0

In our store we use ReCharge to manage our subscription program. There are 2 ways to activate subscription on a product. You can select subscription from the product page, then add it to your cart:
Screen Shot 2023-02-21 at 9.59.37 AM.png

Or you can add a one-time product to your cart, and select the subscription option from the ajax cart drawer:

Screen Shot 2023-02-21 at 11.45.08 AM.png

We also use the script editor to apply free shipping to subscription products. The issue we're having is that if you add a subscription product from the product page, the script editor detects that it's a subscription product and applies the free shipping. But if you add a one-time product to your cart and change it to subscription there, the script editor does not recognize it as a subscription product and therefor charges shipping. Although the script editor doesn't detect the subscription, the checkout does. The item in the checkout will have the subscription discount and the shipping cadence applied, just the free shipping script doesn't work.

 

I'm wondering if this issue is coming from the cart change API, which is what we use to change the product when someone selects subscription from the ajax cart drawer. Here is the javascript for that:

 

updateCartItem(line, value, name, target) {
  //enable loading state on line item
  this.enableLoading(line);

  //set selling plan value
  let selling_plan = null;
  let properties = null;

  //use default selling plan value when subscription checkbox is checked
  if (name === "subscribe") {
    if (target.checked) {
      selling_plan = target.dataset.default;
      properties = { _is_subscription: true };
    } else {
      properties = { _is_subscription: false };
    }
  //change selling plan value when frequency dropdown is changed
  } else if (name === "selling_plan") {
    selling_plan = value;
  }

  const body = JSON.stringify({
    line,
    sections: this.getSectionsToRender().map((section) => section.section),
    sections_url: window.location.pathname,
    //if change is not on subscription inputs, change the quantity
    ...(!["subscribe", "selling_plan"].includes(name) && { quantity: value }),
    //if change is on the subscription inputs, change the selling plan
    ...(["subscribe", "selling_plan"].includes(name) && { selling_plan }),
    //include subscription property for shipping company
    ...(properties && { properties }),
  });

  //call shopify cart change API
  fetch(`${routes.cart_change_url}`, { ...fetchConfig(), ...{ body } })
    .then((response) => response.text())
    .then((state) => {
      const parsedState = JSON.parse(state);

      //get cart object, we are using cart-drawer
      this.cart = document.querySelector("cart-notification") || document.querySelector("cart-drawer");

      //toggle empty state depending on cart item count
      this.classList.toggle("is-empty", parsedState.item_count === 0);

      const cartDrawerWrapper = document.querySelector("cart-drawer");
      const cartFooter = document.getElementById("main-cart-footer");

      if (cartFooter) cartFooter.classList.toggle("is-empty", parsedState.item_count === 0);
      if (cartDrawerWrapper) cartDrawerWrapper.classList.toggle("is-empty", parsedState.item_count === 0);

      //replace cart elements that when line item changes
      //elements to change: subtotal, line item, free shipping countdown, catch pay
      this.replaceSections(parsedState);
      this.updateLiveRegions(line, parsedState.item_count);
      Catch.refresh();

      //move focus to appropriate element after cart elements are updated
      const lineItem = document.getElementById(`CartItem-${line}`) || document.getElementById(`CartDrawer-Item-${line}`);
      if (lineItem && lineItem.querySelector(`[name="${name}"]`)) {
        cartDrawerWrapper ? trapFocus(cartDrawerWrapper, lineItem.querySelector(`[name="${name}"]`)) : lineItem.querySelector(`[name="${name}"]`).focus();
      } else if (parsedState.item_count === 0 && cartDrawerWrapper) {
        trapFocus(cartDrawerWrapper.querySelector(".cart-empty"), cartDrawerWrapper.querySelector("a"));
      } else if (document.querySelector(".cart-item") && cartDrawerWrapper) {
        trapFocus(cartDrawerWrapper, document.querySelector(".cart-item__name"));
      }

      //disable loading state on line item
      this.disableLoading();
    })
    .catch((error) => {
      console.error(error);
      this.querySelectorAll(".loading-overlay").forEach((overlay) => overlay.classList.add("hidden"));
      const errors = document.getElementById("cart-errors") || document.getElementById("CartDrawer-CartErrors");
      if (errors) {
        errors.textContent = window.cartStrings.error;
      }
      this.disableLoading();
    });
}

 

 

So when someone interacts with any form element on a line item in the ajax cart, we check whether that form element is subscription related (we have a checkbox to enable subscription and a dropdown to choose the shipping interval). If it is subscription related, we assign a value to selling_plan. This value is the id of the selling plan. Here's the liquid for the subscription checkbox and interval dropdown so you can see where the id values come from:

 

{%- if item.product.selling_plan_groups.size > 0 and hide_subscription == false -%}
  <div class="cart-subscibe flex flex-col items-end h-full">
    {%- liquid
      comment
        set default selling plan value to the second interval option (2 months)
        if there is a custom value for the default selling plan set in metafields, change default
      endcomment
      assign selling_plans = item.product.selling_plan_groups[0].selling_plans
      assign default_option = selling_plans[1].id
      if item.product.metafields.custom.subscription_interval != blank
        for plan in selling_plans
          if plan.name contains item.product.metafields.custom.subscription_interval
            assign default_option = plan.id
          endif
        endfor
      endif
    -%}
    <label for="CartSubscribeCheckbox-{{ item.index | plus: 1 }}" class="cursor-pointer flex-1 flex items-center tracking-wide text-sm relative mb-1">
      <input
        id="CartSubscribeCheckbox-{{ item.index | plus: 1 }}"
        type="checkbox"
        class="w-4 h-4 box-content mr-1 input"
        name="subscribe"
        data-index="{{ item.index | plus: 1 }}"
        data-default="{{ default_option }}"
        {% if item.selling_plan_allocation %}checked{% endif %}
      >
      <span id="CartSubscribeCheckbox-label" class="whitespace-nowrap">{% if item.selling_plan_allocation != nil %}Subscribed{% else %}Subscribe & Save 10%{% endif %}</span>
    </label>
    <div class="relative text-seaweed-700 flex-1">
      {%- if item.selling_plan_allocation != nil -%}
        <label for="CartSubscribeSelect-{{ item.index | plus: 1 }}" class="absolute left-3 top-1/2 -translate-y-1/2 text-sm pointer-events-none">Ship every</label>
        <select
          id="CartSubscribeSelect-{{ item.index | plus: 1 }}"
          class="input input-sm pl-[5rem] box-border"
          name="selling_plan"
          data-index="{{ item.index | plus: 1 }}"
        >
          {%- for option in selling_plans -%}
            <option value="{{ option.id }}" {%- if option.id == item.selling_plan_allocation.selling_plan.id -%}selected{%- endif -%}>
              {{- option.name | replace: 'Ship every ', '' | replace: 'Month', 'month' -}}
            </option>
          {%- endfor -%}
        </select>
      {%- endif -%}
    </div>
  </div>
{%- endif -%}

 

 

 So here we get the array of selling plans from `item.product.selling_plan_groups[0].selling_plans`. In our case there is always just one selling plan group, so the selling_plan array is always the first array in the selling_plan_group array. We then set the default id to the second option, which is always 2 months in our case. We also have a metafield on each product that allows us to choose a custom default option, so that first chunk of liquid code is just looking for that metafield and changing the default if it exists. The interval dropdown just has the id as the value for each option in the menu.

 

In the cart API documentation for selling plans, it says that to add a selling plan you just need to include the property selling_plan with a value that is the id of the selling plan in the POST data. We are doing this, and it seems like it's working because the item has all its subscription properties in the checkout.

 

However, the shipping script in the script editor doesn't seem to agree. It's extra confusing because the script editor works fine if you add it as a subscription product from the beginning. The shipping script we use is pretty long, but here is the relevant snippet:

 

def freeShipping()
  Input.shipping_rates.each do |shipping_rate|
    name = shipping_rate.name.downcase
    if(name == 'standard shipping')
      shipping_rate.apply_discount(shipping_rate.price, message: "Free Shipping!")
    end
  end
end

def validateIsSubscription()
  hasSubscription = false
  Input.cart.line_items.each do |line_item|
    if line_item.properties.has_key?("_is_subscription") && line_item.properties["_is_subscription"] == "true"
      hasSubscription = true
    end
  end
  if hasSubscription
    freeShipping()
  end
end 

validateIsSubscription()

 

 

We have to include a hidden property for subscription on the line item for our logistics company (_is_subscription). We piggy back off that value to check if an item is a subscription item, and apply free shipping if it is. You can see in the javascript for the cart change that I am also updating this value on cart change, so it should be accurate and included for the script editor.

 

Any help would be greatly appreciated, I have been unable to figure this problem out for awhile.

Replies 2 (2)

J-ROM
Shopify Staff (Retired)
66 6 8

Hi Osea-Malibu,

 

I've been following the steps you provided (adding the Undaria Algae Body Oil as a one-time purchase to the cart, then checking out Subscribe in the cart drawer, then proceeding to the checkout) and I correctly get Free shipping for the subscription product.

 

Screenshot 2023-02-22 at 10.53.54 AM.pngScreenshot 2023-02-22 at 10.54.06 AM.pngScreenshot 2023-02-22 at 10.54.30 AM.png

Are you still experiencing the problem?

Cheers,
Jerome

To learn more visit the Shopify Help Center or the Community Blog.

osea-malibu
Shopify Partner
4 0 0

Yes it's still an issue. We offer free shipping on orders over $60 so you have to do this with a product that is less than $60 after discounting, this one works: https://oseamalibu.com/products/blemish-balm