Skip to content

Instantly share code, notes, and snippets.

@elghorfi
Last active December 27, 2024 07:25
Show Gist options
  • Save elghorfi/ce7e5b1080aae37d0a415643f33bc79e to your computer and use it in GitHub Desktop.
Save elghorfi/ce7e5b1080aae37d0a415643f33bc79e to your computer and use it in GitHub Desktop.
{%comment%}
#############################################
# Mohamed El-Ghorfi Discount Code on Cart #
# [UPDATED] #
#############################################
# Paypal Me: https://paypal.me/elghorfimed #
# Buy Me A Coffee: #
# https://www.buymeacoffee.com/elghorfi #
#############################################
# [email protected] #
#############################################
{%endcomment%}
<style>
.cart-sidebar-discount {
display: flex;
flex-direction: column;
width:300px;
margin: 20px auto;
}
.cart-sidebar-discount input {
margin-top:20px;
background: #eee;
border: 1px solid #eee;
height:50px;
outline: none;
font-size: 18px;
letter-spacing: .75px;
text-align: center;
}
#apply-discount-btn {
background-color: #000;
color:#fff;
border: 0;
height: 60px;
width: 100%;
font-size: 18px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .75px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
span.applied-discount-code-value>small {
background: #eee;
padding: 0px 10px;
color: #000;
font-weight: bold;
border-radius: 20px;
}
.loader {
border: 5px solid #f3f3f3;
border-top: 4px solid #000;
border-radius: 50%;
width: 25px;
height: 25px;
animation: spin .5s linear infinite;
}
#discount-code-error {
background: #ff00004f;
color: #e22120;
padding: 5px;
border-radius: 4px;
font-size: 13px;
line-height: 1;
}
.applied-discount-code-wrapper {
display: none;
background: #ddd;
padding: 3px 6px;
border-radius: 25px;
}
.applied-discount-code-value {
font-size: 13px;
text-transform: uppercase;
}
#discount-code-error:empty {
display: none;
}
.applied-discount-code-value:empty+button {
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<div class="cart-sidebar-discount">
<span id="applied-discount-code">
Discount Code:
<span class="applied-discount-code-wrapper">
<span class="applied-discount-code-value"></span>
<button id="clear-discount-btn">X</button>
</span>
</span>
<small id="discount-code-error"></small>
<input type="text" id="discount-code-input" autocomplete="on" value="">
<button id="apply-discount-btn">APPLY</button>
</div>
<script>
document.addEventListener("DOMContentLoaded", function(event) {
let clearBtn = document.querySelector("#clear-discount-btn");
let applyBtn = document.querySelector("#apply-discount-btn");
let discountCodeError = document.querySelector("#discount-code-error");
let discountCodeWrapper = document.querySelector("#applied-discount-code .applied-discount-code-wrapper");
let discountCodeValue = document.querySelector("#applied-discount-code .applied-discount-code-value");
let discountCodeInput = document.querySelector("#discount-code-input");
let totalCartSelector = document.querySelector(".cart__subtotal .money"); // Total Cart Selector to update the total amount.
let authorization_token;
let checkoutContainer = document.createElement('div');
document.body.appendChild(checkoutContainer);
if (localStorage.discountCode) applyDiscount( JSON.parse(localStorage.discountCode).code);
if(applyBtn)
applyBtn.addEventListener("click", function(e){
e.preventDefault()
applyDiscount(discountCodeInput.value);
});
if(clearBtn)
clearBtn.addEventListener("click", function(e){
e.preventDefault()
clearDiscount();
});
function clearDiscount() {
discountCodeValue.innerHTML = "";
discountCodeError.innerHTML = "";
clearLocalStorage();
fetch("/discount/CLEAR");
}
function clearLocalStorage() {
if(discountCodeWrapper) discountCodeWrapper.style.display = "none";
if(totalCartSelector) totalCartSelector.innerHTML = JSON.parse(localStorage.discountCode).totalCart;
localStorage.removeItem("discountCode");
}
function applyDiscount(code) {
if(applyBtn) {
applyBtn.innerHTML = "APPLYING <div class='loader'></div>";
applyBtn.style.pointerEvents = "none";
}
fetch("/payments/config", {"method": "GET"})
.then(function(response) { return response.json() })
.then(function(data) {
const checkout_json_url = '/wallets/checkouts/';
authorization_token = btoa(data.paymentInstruments.accessToken)
fetch('/cart.js', {}).then(function(res){return res.json();})
.then(function(data){
let body = {"checkout": { "country": Shopify.country,"discount_code": code,"line_items": data.items, 'presentment_currency': Shopify.currency.active } }
fetch(checkout_json_url, {
"headers": {
"accept": "*/*", "cache-control": "no-cache",
"authorization": "Basic " + authorization_token,
"content-type": "application/json, text/javascript",
"pragma": "no-cache", "sec-fetch-dest": "empty",
"sec-fetch-mode": "cors", "sec-fetch-site": "same-origin"
},
"referrerPolicy": "strict-origin-when-cross-origin",
"method": "POST", "mode": "cors", "credentials": "include",
"body": JSON.stringify(body)
})
.then(function(response) { return response.json() })
.then(function(data) {
console.log(data.checkout);
if(data.checkout && data.checkout.applied_discounts.length > 0){
let discountApplyUrl = "/discount/"+code+"?v="+Date.now()+"&redirect=/checkout/";
fetch(discountApplyUrl, {}).then(function(response) { return response.text(); })
if(discountCodeWrapper) discountCodeWrapper.style.display = "inline";
if(discountCodeError) discountCodeError.innerHTML = "";
if(discountCodeValue) discountCodeValue.innerHTML = data.checkout.applied_discounts[0].title + " (" + data.checkout.applied_discounts[0].amount + ' ' + Shopify.currency.active + ")";
let localStorageValue = {
'code': code.trim(),
'totalCart': data.checkout.total_line_items_price
};
localStorage.setItem("discountCode", JSON.stringify(localStorageValue));
if(totalCartSelector) totalCartSelector.innerHTML = "<s>" + data.checkout.total_line_items_price + "</s>" + data.checkout.total_price;
}else{
if(discountCodeValue) discountCodeValue.innerHTML = "";
clearLocalStorage();
if(discountCodeError) discountCodeError.innerHTML = "Please Enter Valid Coupon Code."
}
}).finally(function(params) {
if(applyBtn){
applyBtn.innerHTML = "APPLY";
applyBtn.style.pointerEvents = "all";
}
});
});
});
}
});
</script>
@ThomasB-Sunology
Copy link

@Raysbaelo Actually I found this blog article. It's not always CLEAR we need to pass a non-existing discount code :

https://community.shopify.com/c/technical-q-a/how-to-fully-remove-a-discount-code-from-a-cookie-with/m-p/2514785/highlight/true#M154983

@Raysbaelo
Copy link

@ThomasB-Sunology Cool, that's an interesting way to delete a discount code, that means if my code has been made for multiple discount codes it is not viable anymore, I guess.. ( depends on the code structure ). Does anyone have a functional code for a combination of discount codes without weird behavior when applying/deleting a code?

@geranuda
Copy link

geranuda commented Aug 24, 2024

This is great! Thank you so much for the help, I managed to make this with Claude and it is fully functional for multiple codes:

document.addEventListener("DOMContentLoaded", function(event) { 
  let clearBtn = document.querySelector("#clear-discount-btn");
  let applyBtn = document.querySelector("#apply-discount-btn");
  let discountCodeError = document.querySelector("#discount-code-error");
  let discountCodeWrapper = document.querySelector("#applied-discount-code .applied-discount-code-wrapper");
  let discountCodeValue = document.querySelector("#applied-discount-code .applied-discount-code-value");
  let discountCodeInput = document.querySelector("#discount-code-input");
  let totalCartSelector = document.querySelector(".cart__subtotal .money");
  
  let appliedDiscounts = JSON.parse(localStorage.getItem("appliedDiscounts") || "[]");

  if (typeof customerHasLowestPriceTag !== 'undefined' && customerHasLowestPriceTag) {
    applyAutomaticDiscounts(['custom-code-1', 'custom-code-2', 'custom-code-3', ... more codes]);
  }

  if (clearBtn)
    clearBtn.addEventListener("click", function(e){
      e.preventDefault();
      clearDiscount();
    });

  if (applyBtn)
    applyBtn.addEventListener("click", function(e){
      e.preventDefault();
      applyDiscount(discountCodeInput.value);
    });

  function clearDiscount() {
    appliedDiscounts = [];
    localStorage.setItem("appliedDiscounts", JSON.stringify(appliedDiscounts));
    discountCodeValue.innerHTML = "";
    discountCodeError.innerHTML = "";
    fetch(`${Shopify.routes.root}checkout/clear`);
    location.reload();
  }

  function applyAutomaticDiscounts(discountCodes) {
    discountCodes.forEach(code => applyDiscount(code));
  }

  async function applyDiscount(code) {
    if(applyBtn) {
      applyBtn.innerHTML = "APPLYING <div class='loader'></div>";
      applyBtn.style.pointerEvents = "none";
    }

    try {
      // First, validate the new discount code
      const validationResponse = await validateDiscountCode(code);
      if (!validationResponse.isValid) {
        throw new Error(validationResponse.error);
      }

      // If valid, add to applied discounts and apply all
      if (!appliedDiscounts.includes(code)) {
        appliedDiscounts.push(code);
      }
      
      const applyResponse = await applyDiscounts(appliedDiscounts);
      if (applyResponse.success) {
        updateUI(applyResponse.discounts);
        localStorage.setItem("appliedDiscounts", JSON.stringify(appliedDiscounts));
      } else {
        throw new Error(applyResponse.error);
      }

    } catch (error) {
      console.error('Error:', error);
      if(discountCodeError) discountCodeError.innerHTML = error.message;
    } finally {
      if(applyBtn) {
        applyBtn.innerHTML = "APPLY";
        applyBtn.style.pointerEvents = "all";
      }
    }
  }

  async function validateDiscountCode(code) {
    const shopify_features_script = document.querySelector("script[id='shopify-features']");
    const shopify_features_json = JSON.parse(shopify_features_script.innerHTML);
    const cart = await fetch(`${Shopify.routes.root}cart.js`).then(response => response.json());
    
    const headers = {
      Authorization: 'Basic ' + btoa(shopify_features_json.accessToken),
      Accept: '*/*',
      'Content-Type': 'application/json',
    };

    const body = {
      checkout: {
        line_items: cart.items,
        discount_code: code,
        country: Shopify.country,
        presentment_currency: cart.currency,
      },
    };

    const response = await fetch('/wallets/checkouts/', {
      method: 'POST',
      headers: headers,
      body: JSON.stringify(body),
      referrerPolicy: 'no-referrer',
    });

    const data = await response.json();

    if (data.errors && data.errors.discount_code) {
      return { isValid: false, error: data.errors.discount_code[0].message };
    }

    return { isValid: true };
  }

  async function applyDiscounts(discountCodes) {
    const response = await fetch(`${Shopify.routes.root}checkout?discount=${discountCodes.join(',')}&_=${Date.now()}`);
    const html = await response.text();
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    const discountScript = doc.querySelector('script[data-discount-allocations]');
    
    if (discountScript) {
      const discountData = JSON.parse(discountScript.textContent);
      return { success: true, discounts: discountData };
    } else {
      return { success: false, error: "Failed to apply discount" };
    }
  }

  function updateUI(discounts) {
    if(discountCodeWrapper) discountCodeWrapper.style.display = "inline";
    if(discountCodeError) discountCodeError.innerHTML = "";
    if(discountCodeValue) {
      discountCodeValue.innerHTML = discounts.map(discount => 
        `${discount.title} (${formatMoney(discount.amount)})`
      ).join(', ');
    }
    
    // update total if needed
    //might need to adjust this based on how your cart data is structured
    if(totalCartSelector) {
      const totalPrice = discounts.reduce((total, discount) => total - discount.amount, cart.total_price);
      totalCartSelector.innerHTML = `<s>${formatMoney(cart.total_price)}</s> ${formatMoney(totalPrice)}`;
    }

    // Refresh the cart to reflect changes
    location.reload();
  }

  function formatMoney(cents) {
    return (cents / 100).toLocaleString('en-US', {
      style: 'currency',
      currency: Shopify.currency.active
    });
  }
});

@procarrera
Copy link

procarrera commented Sep 17, 2024

@MaxDesignFR Thank you for those insights, I have been playing with it this past two days and believe I have a pretty decent solution. It is not 100% bullet proof yet. As far as I can tell the /wallets/checkouts/ endpoint seems some sort of pre-flight validation check to see if the discount code can be applied. Unfortunately as far as I can tell this only works for single discounts, it does not return any errors when trying to apply a discount that does not stack with an already applied discount.

Applying multiple discounts is possible using fetch(`${Shopify.routes.root}checkout?discount=${[newDiscountCode, ...alreadyAppliedDiscounts].join(',')}&=${Date.now()}`);

This seems to never return any errors, even when no discounts are applied so you still have to check if the discount is actually applied. So far the best way I found is by creating a application/json script tag in the html when re-rendering the section, parsing that and cross referencing that with the code that was just added.

Intresting, it did applied multiple coupons, however, the first call returned 302 and a subsequent call to checkout returned 403

@MaxDesignFR
Copy link

Did anyone notice this error when fetching /checkout?discount=HELLO10 on iOS platform when ShopPay is enabled?

Fetch API cannot load https://shop.app/checkout/62371266751/cn/Z2NwLXVzLWVhc3QxOjAxSjg3NkY2MFhFNkFXVjJIRDkwMThXU1Ew/shop_pay_callback?discount_code=HELLO10&locale=fr-NL&.....[immense string].... due to access control checks. TypeError: Load failed

It works fine on Android, but seemingly fetching /checkout when ShopPay is enabled on an iOS device generate this error. Curious if someone can replicate this issue, looking for solutions right now (damn iOS, always something!)

@JanIshkaster
Copy link

Thank you, kind Sir.

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