Free shipping on initial orders

If you are on Shopify Plus, you can use Shopify Scripts to give customers free shipping when they purchase a subscription from your store. This will apply to the initial order when a customer checks out with a subscription on your Shopify site. You can also set up free shipping for recurring orders from the Promotions page within Ordergroove. Note that free shipping set up in Ordergroove only applies to recurring subscription orders, and not the initial order when a customer purchases a subscription. See this article for more information about subscription shipping and delivery for Shopify.


Set up free shipping

Follow these steps to set up the script for free shipping:

  1. Open the Script Editor in your Shopify admin. If you do not have the Script Editor, you can add it to your store here.
  2. Click "Create script" on the top right to create a new script.
  3. In the popup, select the Shipping rates tab and choose "Blank template", and click "Create script".
  1. Give the script a descriptive title.
  1. Select Code to open up the script code editor.
  1. Paste the following sample code into the code editor. Please note that you may need to make changes to this script so that it works properly on your site.
# SCRIPT BASED ON
# https://help.shopify.com/en/manual/checkout-settings/script-editor/examples/shipping-scripts
# ================================ Customizable Settings ================================
# ================================================================
# Hide Rate(s) for Product/Country
#
# If the cart contains any matching items, and we have a matching
# country, the entered rate(s) are hidden.
#
# - 'product_selector_match_type' determines whether we look for
# products that do or don't match the entered selectors. Can
# be:
# - ':include' to check if the product does match
# - ':exclude' to make sure the product doesn't match
# - 'product_selector_type' determines how eligible products
# will be identified. Can be either:
# - ':tag' to find products by tag
# - ':type' to find products by type
# - ':vendor' to find products by vendor
# - ':product_id' to find products by ID
# - ':variant_id' to find products by variant ID
# - ':subscription' to find subscription products
# - 'product_selectors' is a list of tags or IDs to identify
# associated products
# - 'country_code_match_type' determines whether we look for
# countries that do, or don't, match the entered options, or
# all countries. Can be:
# - ':include' to look for countries that DO match
# - ':exclude' to look for countries that DO NOT match
# - ':all' to look for all countries
# - 'country_codes' is a list of country code abbreviations
# - ie. United States would be `US`
# - 'rate_match_type' determines whether the below strings
# should be an exact or partial match. Can be:
# - ':exact' for an exact match
# - ':partial' for a partial match
# - ':all' for all rates
# - 'rate_names' is a list of strings to identify rates
# - if using ':all' above, this can be set to 'nil'
# ================================================================
HIDE_RATES_FOR_PRODUCT_AND_COUNTRY_PLUS_FREE_SHIPPING = [
{
product_selector_match_type: :include,
product_selector_type: :subscription,
product_selectors: [],
country_code_match_type: :all,
country_codes: [],
rate_match_type: :exact,
rate_names: [""],
discount_type: :percent,
discount_amount: 100,
discount_message: "Free Subscription Shipping"
},
]

# ================================ Script Code (do not edit) ================================
# ================================================================
# ProductSelector
#
# Finds matching products by the entered criteria.
# ================================================================
class ProductSelector
def initialize(match_type, selector_type, selectors)
@match_type = match_type
@comparator = match_type == :include ? 'any?' : 'none?'
@selector_type = selector_type
@selectors = selectors
end

def match?(line_item)
if self.respond_to?(@selector_type)
self.send(@selector_type, line_item)
else
raise RuntimeError.new('Invalid product selector type')
end
end

def tag(line_item)
product_tags = line_item.variant.product.tags.map { |tag| tag.downcase.strip }
@selectors = @selectors.map { |selector| selector.downcase.strip }
(@selectors & product_tags).send(@comparator)
end

def type(line_item)
@selectors = @selectors.map { |selector| selector.downcase.strip }
(@match_type == :include) == @selectors.include?(line_item.variant.product.product_type.downcase.strip)
end

def vendor(line_item)
@selectors = @selectors.map { |selector| selector.downcase.strip }
(@match_type == :include) == @selectors.include?(line_item.variant.product.vendor.downcase.strip)
end

def product_id(line_item)
(@match_type == :include) == @selectors.include?(line_item.variant.product.id)
end

def variant_id(line_item)
(@match_type == :include) == @selectors.include?(line_item.variant.id)
end

def subscription(line_item)
!line_item.selling_plan_id.nil?
end
end

# ================================================================
# CountrySelector
#
# Finds whether the supplied country code matches the entered
# strings.
# ================================================================
class CountrySelector
def initialize(match_type, countries)
@match_type = match_type
@countries = countries.map { |country| country.upcase.strip }
end

def match?(country_code)
if @match_type == :all
true
else
(@match_type == :include) == @countries.any? { |country| country_code.upcase.strip == country }
end
end
end

# ================================================================
# RateNameSelector
#
# Finds whether the supplied rate name matches any of the entered
# names.
# ================================================================
class RateNameSelector
def initialize(match_type, rate_names)
@match_type = match_type
@comparator = match_type == :exact ? '==' : 'include?'
@rate_names = rate_names&.map { |rate_name| rate_name.downcase.strip }
end

def match?(shipping_rate)
if @match_type == :all
true
else
@rate_names.any? { |name| shipping_rate.name.downcase.send(@comparator, name) }
end
end
end

# ================================================================
# HideRatesForProductCountryCampaign
#
# If the cart contains any matching items, and we have a matching
# country, the entered rate(s) are hidden.
# ================================================================
class HideRatesForProductCountryCampaign
def initialize(campaigns)
@campaigns = campaigns
end

def run(cart, shipping_rates)
address = cart.shipping_address

return if address.nil?

@campaigns.each do |campaign|
product_selector = ProductSelector.new(
campaign[:product_selector_match_type],
campaign[:product_selector_type],
campaign[:product_selectors],
)

country_selector = CountrySelector.new(campaign[:country_code_match_type], campaign[:country_codes])
product_match = cart.line_items.any? { |line_item| product_selector.match?(line_item) }
country_match = country_selector.match?(address.country_code)

next unless product_match && country_match

rate_name_selector = RateNameSelector.new(
campaign[:rate_match_type],
campaign[:rate_names],
)

shipping_rates.delete_if do |shipping_rate|
rate_name_selector.match?(shipping_rate)
end

discount_applicator = DiscountApplicator.new(
campaign[:discount_type],
campaign[:discount_amount],
campaign[:discount_message],
)

shipping_rates.each do |shipping_rate|
discount_applicator.apply(shipping_rate)
end
end
end
end

class DiscountApplicator
def initialize(discount_type, discount_amount, discount_message)
@discount_type = discount_type
@discount_message = discount_message

@discount_amount = if discount_type == :percent
discount_amount * 0.01
else
Money.new(cents: 100) * discount_amount
end
end

def apply(shipping_rate)
rate_discount = if @discount_type == :percent
shipping_rate.price * @discount_amount
else
@discount_amount
end

shipping_rate.apply_discount(rate_discount, message: @discount_message)
end
end

CAMPAIGNS = [
HideRatesForProductCountryCampaign.new(HIDE_RATES_FOR_PRODUCT_AND_COUNTRY_PLUS_FREE_SHIPPING),
]

CAMPAIGNS.each do |campaign|
campaign.run(Input.cart, Input.shipping_rates)
end

Output.shipping_rates = Input.shipping_rates
  1. Click "Save and Publish" on the top right to save the script.

With this script in place, you'll see free shipping applied at checkout when there is a subscription item in the cart.