- 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"
}
]
}
- Create a new snippets in the
snippets
folder and name itfree-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 %}
- 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 besections/cart-template.liquid
.
{% include 'free-gift', enable: settings.free_gift_enable %}
- 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>
- 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 includedefault
(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."
}
}
-
Go into your theme editor by clicking on
Customize Theme
. Then click on theGeneral Settings
tab. You now shoud see aFree Gift Offer
option. Click on that and access the settings. -
The items you need to complete to make this work are to check
Enable Offer
(which should be done by default), enter aDiscount Code
to use for this and select aProduct 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 toPage
if you have the option as this customization will not work with drawer or modal cart types. -
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.
-
Install the
Script Editor
app if you don't already have it. Then create a newLine Item
script. Paste the following code inside and thenSave 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
-
Under the
CAMPAIGNS
near the bottom of the script, you will need to adjust theFreeGiftCampaign
to match what you've created. So replacefreegift
for theDiscountCodeValidator
with your discount code. If you want to discount an item based on a product ID, replace the number insideProductIDSelector
with the id of your gift product. Alternatively, you can use aTagSelector
by replacing the entire line withTagSelector.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. -
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.
-
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.