Skip to content

Instantly share code, notes, and snippets.

@dturton
Forked from jgodson/gift-modal.md
Created May 12, 2018 13:55
Show Gist options
  • Save dturton/a4ffafffca5ba841e6fb2b05cec6de9d to your computer and use it in GitHub Desktop.
Save dturton/a4ffafffca5ba841e6fb2b05cec6de9d to your computer and use it in GitHub Desktop.
Free Gift Modal for Shopify Themes

How to install

  1. Copy the following code and paste it at the end of config/settings_schema.json, just after the last }. Then save the file.
,
  {
    "name": "Free Gift Offer",
    "settings": [
      {
        "type": "header",
        "content": "Offer a free gift with discount code on cart page"
      },
      {
        "type": "paragraph",
        "content": "When a customer enters the discount code on the cart page, a modal will open up offering them the selected product. Wording can be customized in the Langauage Editor. The settings below should match the discount you've created in the Admin. This requires the companion script to apply the actual discount and changes to the discount will need to be reflected there."
      },
      {
        "type": "checkbox",
        "id": "free_gift_enable",
        "label": "Enable offer",
        "default": true
      },
      {
        "type": "checkbox",
        "id": "free_gift_registered_only",
        "label": "Logged in customers only",
        "info": "The modal will not show unless the customer is logged in.",
        "default": true
      },
      {
        "type": "text",
        "id": "free_gift_minimum_cart_amount",
        "label": "Minimum purchase (eg: 50 = $50)",
        "info": "Minimum purchase before free gift is offered.",
        "default": "0"
      },
      {
        "type": "text",
        "id": "free_gift_code",
        "label": "Discount code"
      },
      {
        "type": "paragraph",
        "content": "You must place this HTML snippet somewhere in your templates\/cart.liquid or sections\/cart-template.liquid file"
      },
      {
        "type": "paragraph",
        "content": "<div class='discount-wrapper'><\/div>"
      },
      {
        "type": "product",
        "id": "free_gift_product",
        "label": "Product to display",
        "info": "Only the first variant will be used."
      },
      {
        "type": "radio",
        "id": "free_gift_added_action",
        "options": [
          {
            "value": "checkout",
            "label": "Go to checkout"
          },
          {
            "value": "refresh",
            "label": "Stay on cart page"
          }
        ],
        "label": "Free gift accept action",
        "default": "checkout",
        "info": "This dictates what happens after the customer adds the free gift to their cart."
      },
      {
        "type": "header",
        "content": "Modal options"
      },
      {
        "type": "checkbox",
        "id": "free_gift_show_price",
        "label": "Show original item price",
        "default": true
      },
      {
        "type": "checkbox",
        "id": "free_gift_modal_has_border",
        "label": "Enable border",
        "default": true
      },
      {
        "type": "image_picker",
        "id": "free_gift_modal_background_image",
        "label": "Background image",
        "info": "If an image is present, it will be used as the background. If not, the background color will be used."
      },
      {
        "type": "color",
        "id": "free_gift_modal_background_color",
        "label": "Background color",
        "default": "#FFFFFF"
      },
      {
        "type": "range",
        "id": "free_gift_modal_background_opacity",
        "label": "Background opacity",
        "min": 50,
        "max": 100,
        "step": 5,
        "unit": "%",
        "default": 100
      },
      {
        "type": "color",
        "id": "free_gift_modal_border_color",
        "label": "Border color"
      },
      {
        "type": "checkbox",
        "id": "free_gift_button_animation",
        "label": "Button hover animation",
        "default": false
      },
      {
        "type": "color",
        "id": "free_gift_accept_button_color",
        "label": "Accept button color",
        "default": "#a3e86a"
      },
      {
        "type": "color",
        "id": "free_gift_decline_button_color",
        "label": "Decline button color",
        "default": "#f48f86"
      },
      {
        "type": "header",
        "content": "Discount field options"
      },
      {
        "type": "checkbox",
        "id": "free_gift_show_label",
        "label": "Show label",
        "default": true
      },
      {
        "type": "color",
        "id": "free_gift_discount_accept_color",
        "label": "Discount applied color",
        "info": "Color of message shown when discount code is applied",
        "default": "#46b164"
      },
      {
        "type": "color",
        "id": "free_gift_discount_error_color",
        "label": "Discount error color",
        "info": "Color of message shown when discount could not be applied",
        "default": "#b14646"
      }
    ]
  }
  1. Create a new snippets in the snippets folder and name it free-gift.liquid. Copy the following code into that file and save it
<!-- snippets/free-gift.liquid -->
{% if enable and settings.free_gift_product and settings.free_gift_code != blank %}
  {% comment %} Checks to ensure all the requirements are met {% endcomment %}
  {% assign allow_free_gift = false %}
  {% if settings.free_gift_registered_only == false or settings.free_gift_registered_only and customer %}
    {% assign cart_required_amount = settings.free_gift_minimum_cart_amount | times: 100 %}
    {% if cart.total_price >= cart_required_amount %}
      {% assign has_gift_in_cart = false %}
      {% for item in cart.items %}
        {% if item.product.handle == settings.free_gift_product %}
          {% assign has_gift_in_cart = true %}
        {% endif %}
      {% endfor %}
      {% if has_gift_in_cart == false %}
        {% assign allow_free_gift = true %}
      {% endif %}
    {% endif %}
  {% endif %}


    {% comment %} Styles for HTML elements {% endcomment %}
      <style>
      /* Modal Styles */
        .gift-modal {
          position: fixed;
          display: none;
          left: 50% !important;
          top: 50% !important;
          width: 95%;
          height: 95%;
          max-width: 650px;
          max-height: 650px;
          transform: translateX(-50%) translateY(-50%);
          -webkit-transform: translateX(-50%) translateY(-50%);
          -ms-transform: translateX(-50%) translateY(-50%);
          {% if settings.free_gift_modal_background_image %}
            background: url("{{ settings.free_gift_modal_background_image | img_url: '650x650' }}");
            background-size: cover;
          {% else %}
            background: {{ settings.free_gift_modal_background_color }};
          {% endif %}
          z-index: 1000000000;
          {% if settings.free_gift_modal_has_border %}border: 1px solid {{ settings.free_gift_modal_border_color }};{% endif %}
          border-radius: 3px;
          opacity: 0;
          -webkit-transition: opacity 300ms ease-in-out;
          -o-transition: opacity 300ms ease-in-out;
          transition: opacity 300ms ease-in-out;
        }

        .gift-modal.showing {
          display: block;
        }

        .gift-modal.shown {
          opacity: {{ settings.free_gift_modal_background_opacity | times: 0.01 }};
        }

        .gift-modal__content {
          width: calc(100% - 40px);
          height: calc(100% - 40px);
          padding: 20px;
          margin: auto;
          text-align: center;
        }

        .gift-modal__title {
          font-size: 120%;
          font-weight: bold;
          text-align: center;
        }

        .gift-modal__body {
          overflow-y: auto;
          height: calc(100% - 40px);
        }

        .free-gift__price:before {
          content: "{{ 'free_gifts.gift_modal.price_seperator' | t }}";
        }

        .gift-modal__footer {
          position: absolute;
          right: 10px;
          bottom: 10px;
        }

        .gift-modal__button {
          padding: 10px;
          border-radius: 4px;
          float: right;
          -webkit-transition: -webkit-transform ease 300ms;
          transition: -webkit-transform ease 300ms;
          -o-transition: transform ease 300ms;
          transition: transform ease 300ms;
          outline: none;
          color: black;
        }

        {% if settings.free_gift_button_animation %}
          .gift-modal__button:hover {
            -webkit-transform: scale(1.2);
            -ms-transform: scale(1.2);
            transform: scale(1.2);
          }
        {% endif %}

        .gift-modal__button__accept {
          margin-left: 20px;
          background-color: {{ settings.free_gift_accept_button_color }};
        }

        .gift-modal__button__decline {
          background-color: {{ settings.free_gift_decline_button_color }};
        }

        @media screen and (min-width: 750px) {
          .gift-modal {
            width: 80%;
            height: 80%;
          }
        }

        @media screen and (min-width: 1024px) {
          .gift-modal {
            width: 50%;
            height: 60%;
          }
        }

      /* Discount Form Styles */
        /* Only show the discount form when it's inside the wrapper. Preventing any weirdness if the HTML snippet is missing */
        .discount-form {
          display: none;
        }

        .discount-wrapper .discount-form {
          display: block;
        }

        .discount-wrapper {
          margin: 10px 0 10px 20%;
          max-width: 80%;
        }
        
        {% unless settings.free_gift_show_label %}
          .discount-form label {
            color: transparent;
            cursor: default;
            height: 0;
            width: 0;
            margin: 0;
          }
        {% endunless %}

        .discount-form__status {
          visibility: hidden;
          max-height: 0px;
          margin: 5px 0;
          -webkit-transition: max-height 800ms ease-out, visibility 0ms 250ms;
          -o-transition: max-height 800ms ease-out, visibility 0ms 250ms;
          transition: max-height 800ms ease-out, visibility 0ms 250ms;
        }

        .discount-form__status.applied {
          visibility: visible;
          max-height: 40px;
        }

        .discount-form__status.accepted {
          color: {{ settings.free_gift_discount_accept_color }};
        }

        .discount-form__status.error {
          color: {{ settings.free_gift_discount_error_color }};
        }

        @media screen and (min-width: 501px) {
          .discount-wrapper {
            margin: 10px 0 10px 60%;
            max-width: 40%;
          }
        }
      </style>

    {% comment %} HTML for gift modal {% endcomment %}
      <div class="gift-modal">
        <div class="gift-modal__content">
          <div class="gift-modal__title">
            <p>{{ 'free_gifts.gift_modal.title' | t }}</p>
          </div>
          <div class="gift-modal__body">
            {% assign gift_product = all_products[settings.free_gift_product] %}
            <div class="free-gift__container">
              <div class="free-gift__image">
                <img src="{{ gift_product.images.first | product_img_url: '200x' }}" alt="{{ gift_product.images.first.alt }}" width="200px" height="200px" />
              </div>
              <div class="free-gift__info">
                <h3>
                  <span>{{ gift_product.title }}</span>
                  {% if settings.free_gift_show_price %}<span class="free-gift__price"> <del>{{ gift_product.price | money_with_currency }}</del></span>{% endif %}
                </h3>
                <h6>{{ 'free_gifts.gift_modal.disclaimer' | t }}</h6>
              </div>
            </div>
          </div>
          <div class="gift-modal__footer">
            <button class="btn gift-modal__button gift-modal__button__accept"  value="accept"  type="button">
              {{ 'free_gifts.gift_modal.accept_button' | t }}
            </button>
            <button class="btn gift-modal__button gift-modal__button__decline" value="decline"  type="button">
              {{ 'free_gifts.gift_modal.decline_button' | t }}
            </button>
          </div>
        </div>
      </div>

    {% comment %} 
      HTML for discount form
      Need to place the following snippet where you would like it to appear in your cart template -> <div class='discount-wrapper'></div>
      This will usually be in templates/cart.liquid for non-sectioned themes or sections/cart-template.liquid for sectioned themes.
    {% endcomment %}
      <form class="discount-form" autocomplete="off">
        <label for="discount-form__code">{{ 'free_gifts.discount_field.textbox_label' | t }}</label>
        <input type="text" name="discount-code" placeholder="{{ 'free_gifts.discount_field.textbox_placeholder' | t }}"/>
        <button class="btn discount-form__button">{{ 'free_gifts.discount_field.apply_button' | t }}</button>
        <p class="discount-form__status"></p>
      </form>

    {% comment %} JavaScript {% endcomment %}
    <script>
      (function() {
        // Exit if the wrapper isn't found
        var wrapper = document.querySelector('.discount-wrapper');
        if (!wrapper || !getCheckoutForm()) {
          return;
        }

        // Attach elements to where they should be on the page
        document.body.appendChild(document.querySelector('.gift-modal'));
        wrapper.appendChild(document.querySelector('.discount-form'));

        var discountCookie = 'gift_discount_code';
        var modalDismissedCookie = 'gift_modal_dismissed';
        // Set cookie expiry to a fraction of a day 0.042 = ~1 hour
        var cookieExpiry = 0.042;
        var discountForm = document.querySelector('.discount-form');

        // Populate discount form if one was already entered previously
        var previousValue = getCookie(discountCookie);
        if (previousValue) {
          discountForm.querySelector('input[name="discount-code"]').value = previousValue;
          applyDiscount(previousValue);
        }

        // Capture discount form when submitted
        discountForm.addEventListener('submit', function(event) {
          event.preventDefault();
          var discountCode = this.querySelector('input[name="discount-code"]').value;
          if (discountCode.trim() !== '') {
            applyDiscount(discountCode);
          } else {
            // If discount is cleared we remove the discount and allow the modal to show again
            removeDiscount();
            deleteCoookie(modalDismissedCookie);
          }
        });

        function applyDiscount(discountCode) {
          var messageDiv = document.querySelector('.discount-form__status');
          var isEligle = discountCode.toLowerCase() === "{{ settings.free_gift_code }}".toLowerCase();
          var form = getCheckoutForm();
          if (form) {
            // If there is already a discount on the form action, remove it
            removeDiscount();

            messageDiv.innerText = "{{ 'free_gifts.discount_field.apply_message' | t }}";
            addClass(messageDiv, 'accepted');

            // Add the discount to the form action
            form.action += form.action.indexOf('?') > -1 ? '&discount=' + discountCode : '?discount=' + discountCode;
            
            // Always go to /checkout instead of /cart
            form.action = form.action.replace('cart', 'checkout');
            setCookie(discountCookie, discountCode, cookieExpiry);
          } else {
            addClass(messageDiv, 'error');
            messageDiv.innerText = "{{ 'free_gifts.discount_field.error_message' | t }}";
          }
          addClass(messageDiv, 'applied');

          {% comment %} Show modal only if product is in available for purchase {% endcomment %}
          {% if gift_product.available and allow_free_gift %}
            if (isEligle && !getCookie(modalDismissedCookie)) {
              toggleModal();
            }
          {% endif %}
        }

        function removeDiscount() {
          deleteCoookie(discountCookie);
          removeClass(document.querySelector('.discount-form__status'), 'applied');
          var form = getCheckoutForm();

          if (form.action.indexOf('discount=') > -1) {
            var temp = form.action.split('discount=')[0];
            temp = temp.substring(0, temp.length - 1);
            form.action = temp;
          }
        }

        function toggleModal() {
          var modal = document.querySelector('.gift-modal');
          toggleClass(modal, 'showing');
          setTimeout(function() {
            toggleClass(modal, 'shown');
          }, 1);
        }

        function handleModalButtonClick(event) {
          var target = event.target;
          // Ignore it unless it was a click on one of the buttons
          if (target.className.indexOf('gift-modal__button') < 0) {
            return;
          }

          // Handle each button
          if (target.value === 'accept') {
            acceptButtonClick(target);
          } else {
            declineButtonClick();
          }

          function acceptButtonClick(buttonElement) {
            // Add item to cart
            buttonElement.innerText = "{{ 'free_gifts.gift_modal.adding_to_cart_text' | t }}";
            var data = "?id=" + {{ gift_product.first_available_variant.id | json }} + "&quantity=1";
            var ajax = new XMLHttpRequest();
            ajax.onreadystatechange = function() {
              if (this.readyState === 4) {
                if (this.status === 200) {
                  // Continue with desired action
                  {% if settings.free_gift_added_action == 'checkout' %}
                    getCheckoutForm().submit();
                  {% else %}
                    window.location.reload();
                  {% endif %}
                } else {
                  buttonElement.innerText = "{{ 'free_gifts.gift_modal.accept_button' | t }}";
                  alert("There was a problem adding the free gift to your cart. Please try again.");
                }
              }
            };
            ajax.open("POST", 'cart/add.js' + data, true);
            ajax.send();
          }

          function declineButtonClick() {
            // Set cookie so we don't show modal again when page reloads
            setCookie(modalDismissedCookie, true, cookieExpiry);
            toggleModal();
          }
        }

        // Listen for button clicks on the modal and complete actions
        document.querySelector('.gift-modal').addEventListener('click', handleModalButtonClick);

        // Ensure modal content is always in the center
        function centerModalContent() {
          var modalDiv = document.querySelector('.gift-modal');
          var titleDiv = document.querySelector('.gift-modal__title');
          var footerDiv = document.querySelector('.gift-modal__footer');
          var bodyDiv = document.querySelector('.gift-modal__body');
          var containerDiv = document.querySelector('.free-gift__container');
          // Use the top padding + bottom padding from the .gift-modal__content
          var padding = 40;
          var topMargin = (modalDiv.clientHeight / 2 + titleDiv.clientHeight + footerDiv.clientHeight)  - containerDiv.clientHeight - padding;

          if (topMargin < 0) {
            topMargin = 0;
          }

          bodyDiv.style.marginTop = topMargin + 'px';
        }

        centerModalContent();
        window.addEventListener('resize', centerModalContent);

        // General Helper Functions
        function getCheckoutForm() {
          var form = document.querySelector('form[action*="cart"]');
          if (!form) {
            form = document.querySelector('form[action*="checkout"]');
          }
          return form;
        }

        // Helper functions to add/remove classes to elements
        function addClass(element, className) {
          if (element.className.indexOf(className) < 0) {
            element.className += ' ' + className;
          }
        }

        function removeClass(element, className) {
          var classes = element.className;
          element.className = classes.replace(className, '').trim();
        }

        function toggleClass(element, className) {
          if (element.className.indexOf(className) > -1) {
            removeClass(element, className);
          } else {
            addClass(element, className);
          }
        }

        // Cookie helper functions
        function setCookie(cname, cvalue, exdays) {
          var d = new Date();
          d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
          var expires = "expires="+d.toUTCString();
          document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
        }

        function deleteCoookie(cname) {
          setCookie(cname, '', -1);
        }

        function getCookie(cname) {
          var name = cname + "=";
          var ca = document.cookie.split(';');
          for(var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == ' ') {
                c = c.substring(1);
            }
            if (c.indexOf(name) == 0) {
                return c.substring(name.length, c.length);
            }
          }
          return "";
        }
      })();
    </script>
{% endif %}
  1. Paste the next snippet at the end of your cart template. If you have an unsectioned theme, this will probably be templates/cart.liquid, for a sectioned theme this will likely be sections/cart-template.liquid.
{% include 'free-gift', enable: settings.free_gift_enable %}
  1. In the same file, paste this snippet of code where you would like the discount field to appear in your cart template. This may require some trial and error to get it where you want it.
<div class='discount-wrapper'></div>
  1. Finally, you will need the language translations for the new items you created. Paste this snippet into your default language file, found in the locales folder, just before the last }. The file name will include default (eg: en.default.json for English). Feel free to customize these to your liking.
,
  "free_gifts": {
    "gift_modal": {
      "title": "You Qualify for this Free Gift!",
      "price_seperator": "-",
      "disclaimer": "Discount will be applied during Checkout",
      "accept_button": "Add to cart",
      "decline_button": "No thanks",
      "adding_to_cart_text": "Adding..."
    },
    "discount_field": {
      "textbox_label": "Apply a Discount",
      "textbox_placeholder": "Enter discount code...",
      "apply_button": "Apply",
      "apply_message": "Discount code will be validated during checkout!",
      "error_message": "There was a problem. Proceed to checkout and try again."
    }
  }
  1. Go into your theme editor by clicking on Customize Theme. Then click on the General Settings tab. You now shoud see a Free Gift Offer option. Click on that and access the settings.

  2. The items you need to complete to make this work are to check Enable Offer (which should be done by default), enter a Discount Code to use for this and select a Product to display. When a customer enters a discount code in the newly created field, a modal will pop up offering them the chance to add it to their cart. You should also make sure that your cart type is set to Page if you have the option as this customization will not work with drawer or modal cart types.

  3. Now we need to create the script to actually discount the gift item. You will need to grab the product ID of the item you wish to discount (or tag a product with a tag of your choosing), also remember the discount code you created previously.

  4. Install the Script Editor app if you don't already have it. Then create a new Line Item script. Paste the following code inside and then Save Draft

# Returns a match when a product id matches given ids
class ProductIDSelector

  def initialize(product_ids)
    @product_ids = Array(product_ids)
  end

  def match?(line_item)
    @product_ids.include?(line_item.variant.product.id)
  end
end

# Returns a match when product tags match the given tags
class TagSelector
  
  def initialize(tags)
    @tags = Array(tags).map{ |tag| tag.downcase }
  end
  
  def match?(line_item)
    (@tags & line_item.variant.product.tags).length > 0
  end
  
end

class DiscountCodeValidator
  
  def initialize(discount_codes)
    @discount_codes = Array(discount_codes).map{ |code| code.downcase }
  end
  
  def match?(cart)
    return false unless cart.discount_code
    @discount_codes.include?(cart.discount_code.code.downcase)
  end
  
end

class PercentageDiscount
# Applies a given percentage discount to an item with the given message

  def initialize(percent, message)
    @percent = Decimal.new(percent) / 100.0
    @message = message
  end

  def apply(line_item)
    # Calculate the discount for this line item
    line_discount = line_item.line_price * @percent

    # Calculated the discounted line price
    new_line_price = line_item.line_price - line_discount

    # Apply the new line price to this line item with a given message
    # describing the discount, which may be displayed in cart pages and
    # confirmation emails to describe the applied discount.
    line_item.change_line_price(new_line_price, message: @message)
  end
  
end


class FreeGiftCampaign

  def initialize(cart_qualifiers, gift_qualifiers, discount)
    @cart_qualifiers = cart_qualifiers
    @gift_qualifiers = gift_qualifiers
    @discount = discount
  end
  
  def run(cart)
    return unless @cart_qualifiers.match?(cart)
    gift_items = cart.line_items.select{ |item| @gift_qualifiers.match?(item) }
    return unless gift_items.length > 0
    
    # Sort gift items to the least expensive is free
    gift_items.sort_by { |item| item.variant.price }
  
    if gift_items.first.quantity > 1
      discounted_item = gift_items.first.split(take: 1)
      @discount.apply(discounted_item)
      cart.line_items << discounted_item
    else
      @discount.apply(gift_items.first)
    end
    
  end

end

CAMPAGINS = [
  FreeGiftCampaign.new(
    DiscountCodeValidator.new('freegift'), # Discount code(s) to qualify
    ProductIDSelector.new(9213784014), # Discountable Items,
    PercentageDiscount.new(100, "Free Gift") # Discount % and Message to show
  )
].freeze


CAMPAGINS.each do |campaign|
  campaign.run(Input.cart)
end

Output.cart = Input.cart
  1. Under the CAMPAIGNS near the bottom of the script, you will need to adjust the FreeGiftCampaign to match what you've created. So replace freegift for the DiscountCodeValidator with your discount code. If you want to discount an item based on a product ID, replace the number inside ProductIDSelector with the id of your gift product. Alternatively, you can use a TagSelector by replacing the entire line with TagSelector.new('tag_name'). Finally, on the last line, you can adjust the discount percentage and message that is shown (note that discount messages are not currently shown in checkout). Feel free to test this to make sure it's working properly.

  2. Before publishing this theme, you should set up a discount code in your admin with the same settings as you just configured in your theme. (Customer is logged in, discount code, minimum order value). You should also ensure you published the script we created previously.

  3. That's it, you're done! The modal will no longer show once the gift product you selected in the theme editor is out of stock.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment