Shopify Remix Discount App Tutorial (Part 1/3)
Learn how to create a discount App using the Shopify Remix App Framework
Table of contents
Intro
Welcome! In this tutorial, you will learn how to build a Shopify Discount App! It will be a fully working discount experience where the merchant can create a new discount in the Admin Discounts page ( similar to Shopify default Products or Order Discounts )
Preview of the App
What exactly are we going to build? I made a little recording of its functionality to get you excited!
We will create a Customer Tag Automatic Discount, which will allow merchants to discount customer's entire orders or only selected products based on a customer tag.
Because there is a lot to go through and I want this tutorial to be as detailed and packed with tips as possible I decided to split it into 3 different parts:
Part 1 - How to Set up a project, create a discount function, and install new discounts in the store.
Part 2 - How to use meta fields and input queries in Shopify Functions to replace hard-coded variables in your function with meta field values.
Part 3 - How to build a User interface (UI) with Remix and Polaris Components so the merchants can create and view the new Discounts inside the Shopify Admin Discounts page.
Development
With all that said let's jump straight into it!
Prerequisites
Before we begin make sure you have all you need:
Node.js 18 or higher.
NPM version 8 or higher
The latest version of Shopify CLI (version 3.58.2 as of writing this)
Shopify Partner account
Created dev store with checkout extensibility enabled
Previous experience with React and GraphQL (optional)
Shopify Remix App Setup
Open your terminal and run this command: npm init @shopify/app@latest
This will start generating a new Shopify App Project. You will have to name it, select a recommended Remix template, and choose between Javascript and Typescript (To keep this tutorial simple we will pick JavaScript).
Now navigate to your app folder cd customer-tags-discount-app
and run npm run dev
command. When running this command for the first time you need to select the organization, create it as a new project, confirm the name of your app, and choose a store on which you would like to develop it.
Finally, let's make sure we choose to automatically update our app's URL. This will ensure that the Shopify CLI will update the APP_URL
environment variable in your project to match the URL of the preview experience. This allows your app to make requests to the development store and interact with the preview experience that is an iFrame served from a Cloudflare URL.
And that's it! When you run npm run dev
you should be presented with information about some useful shortcuts:
Press 'p' in your terminal to open your app's preview and install the app. You should be presented with the home screen of your App provided by a Remix Template. In this part( and the next one), we will focus on building a Function extension, but we will come back to this page in part 3!
Discount Function extension setup
Go back to your terminal and run this command: npm run generate extension
. Select Discount products - Function type of extensions, give it a Name, and choose Javascript as a language in which you will write your function.
This should generate a new folder for us in this directory extensions/customer-tag-discount.
Three important files here:
run.graphql
- Used to define graphQL query which will be responsible for passing correct data to a Function.run.js
- Logic of a Function.shopify.extension.toml
- Extension configuration file
Updating RunInput GraphQL query
Open run.graphql
file and replace its content with:
query RunInput {
cart {
buyerIdentity {
customer {
hasAnyTag(tags: ["Loyal", "VIP"])
}
}
lines {
merchandise {
... on ProductVariant {
id
product {
id
}
}
__typename
}
}
}
}
This is where you define what data will be passed into your function. In your case, that's all the products/lines that are in the customer cart as well as a boolean value whatever customer is tagged with "Loyal" or "VIP" tags.
Generating types for your query
Now you will generate types and validate your function, run this command in your terminal (remember you need to run it in your extension file directory): npm run typegen
.
This is very useful to make sure there is nothing wrong with your query, in case there is a type or syntax error you should get useful info with more details about what caused it. If your query was correct you should see this:
/generated/api.ts
. It contains the schema of all the types we can query on, super useful if you don't want to look for available types in the docs!Updating Function Logic
Now it's time to work on your function run.js
file, open it, and replace its content with the below code. Read through all the comments, I hope those will help you to understand the logic behind it and what needs to be returned from it.
import { DiscountApplicationStrategy } from "../generated/api";
// ---
// 1. Define empty Discount object, we will return it
// any time we decide not to apply the any discount
// ---
const EMPTY_DISCOUNT = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: [],
};
// ---
// 2. Define the run function, it accepts input object
// (remember your graphQL query? Yes! now we have all that
// data available here)
// ---
export function run(input) {
//---
// 3. Check if the exists using optional chaining,
// if it doesn't exist(meaning user/customer is not logged in)
// - return empty discount
// ---
if (!input?.cart?.buyerIdentity?.customer) {
return EMPTY_DISCOUNT;
}
//---
// 4. Check if one of the tag's that we defined in graphQL query
// exists in the customer's tags (return value is either true or false)
// ---
const customerTagFound = input.cart.buyerIdentity.customer.hasAnyTag;
//---
// 5. If the customer doesn't have one of the tags, return empty discount
// ---
if (!customerTagFound) {
return EMPTY_DISCOUNT;
}
//---
// 6. Define Discount Type, Discount Value, Discount Message and Discount Products variables
// ---
const discountType = "orderDiscount"; // 'orderDiscount' or 'productsDiscount'
const discountProducts = []; // Pass product variant id's here if discountType is 'productsDiscount'
const discountValue = 10; // Discount value
const discountMessage = "10% VIP or Loyal Customer Discount"; // Discount message (It will be displayed in the cart and checkout page)
//---
// 7. Define targets array, those are all the products that we want to discount.
// Here we filter all the products in the cart and make sure to return
// only product variant id's
//---
let targets = input.cart.lines
.filter((line) => {
return line.merchandise.__typename === "ProductVariant";
})
.map((line) => {
return {
productVariant: {
id: line.merchandise.id,
},
};
});
//---
// 8. If discountType is 'productsDiscount', we will filter the products that
// are not in the discountProducts array, otherwise we will apply the discount
// to all products in the cart if it's 'orderDiscount'
//---
if (discountType === "productsDiscount") {
targets = targets.filter((target) => {
return discountProducts.some((product) => {
return product === target.productVariant.id;
});
});
}
//---
// 9. If targets(no products in the cart met your conditions) array is empty,
// return empty discount - this would be a valid scenario for the 'productsDiscount'
// type discount where no products in the cart met the conditions.
//---
if (targets == []) {
return EMPTY_DISCOUNT;
}
//---
// 10.Return the Discount object with discountApplicationStrategy,
// discounts array that contains the target products and discount
// value as well as the discount message
//---
const DISCOUNTED_ITEMS = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: [
{
targets: targets,
value: {
percentage: {
value: discountValue,
},
},
message: discountMessage,
},
],
};
//---
// 11. Return the DISCOUNTED_ITEMS object
//---
return DISCOUNTED_ITEMS;
}
discountType="productsDiscount"
and populate the products discount array with your store product variant IDs: discountProducts=["gid://shopify/ProductVariant/123", "gid://shopify/ProductVariant/234"]
to discount only selected products rather than the entire order.Deploying and Updating App Scopes
That's your Discount function logic done! It's time to deploy it 🚀 . Run npm run deploy
in the root of your remix project:
To add your new function to the store, there are a couple of queries that you need to run, but first, we need to update your app access scopes to be able to query and mutate different store data.
Open shopify.app.toml
file in the root of your project, and update the scopes
scopes = "read_customers,read_products,read_discounts,write_discounts"
Now that you updated the app scopes in shopify.app.toml
file we need to deploy it again (you have to each time you update it). Run npm run deploy
again and deploy as a new version:
See it picked up on your updated scopes!
Start the dev server again: npm run dev
, and press 'p' to open your app's preview, you will need to update it (This has to be done each time app scopes change)
Creating Discount
Press 'g' in your terminal, which will open the GraphiQL tool already connected to your store.
To create your new discount you will need to retrieve its function ID. Copy this query and run it in the GraphiQL tool:
query getStoreFunctions {
shopifyFunctions(first: 25) {
nodes {
app {
title
}
apiType
title
id
}
}
}
This should return you the first 25 Functions that are currently in the store, in my case there is only one. Copy the "id" you will need in the next query.
Open a new tab in the GraphiQL tool and run this query, remember to replace "functionId"
with the one you just copied!
mutation addAutomaticDiscountFunction {
discountAutomaticAppCreate(automaticAppDiscount: {
title: "Customer Tag Discount",
functionId: "[Paste Your Function ID here]",
startsAt: "2022-06-22T00:00:00"
}) {
automaticAppDiscount {
discountId
}
userErrors {
field
message
}
}
}
If everything goes well the query should return the "discountId"
:
That's it! Go to your store Discounts page, you should see your newly created discount!
Testing new Discount
All that's left is to go to your store and confirm your function is working correctly.
You should see that any products added to the cart are now discounted by the correct amount and display a message defined by your function in run.js
file
Debugging your function
If something still doesn't work as expected you will need to debug your function. The best way to do it is to investigate why the discount function didn't return an expected result. Open your partner Dashboard and navigate to Apps > customer-tags-discount-app > Extensions > customer-tag-discount. You should be able to see the history of all your function runs. Open the latest one to investigate both the Input and Output of your function. It's also a place where allconsole.logs
(if you added any) in therun.js
file will return its values.
Outro
That's it for this blog post, stay tuned for part 2 where we will learn how to use input queries and meta fields to replace hard-coded variables in your function with meta field values.
See ya in the next one :)
Edit: Part 2 is now live, you can read it here!