--- title: Build a discounts UI with Remix description: Create a UI that merchants can use to configure your discount function. source_url: html: https://shopify.dev/docs/apps/build/discounts/build-ui-with-remix?extension=javascript md: https://shopify.dev/docs/apps/build/discounts/build-ui-with-remix.md?extension=javascript --- # Build a discounts UI with Remix To build a UI that merchants can use to configure a Discount Function, you can use Admin UI Extensions to display a form on the discount details page, or you can add a page to a Remix app. This tutorial describes how to add a page to a Remix app, which will render the Discount Function editing UI at a path of your own specification. ## What you'll learn In this tutorial, you'll learn how to do the following tasks: 1. **Scaffold a Remix app with the Shopify CLI** Build your app using the provided template with all necessary components. 2. **Review frontend UI components** Explore the app structure, including routes, forms, and Polaris components that create the merchant interface for discount configuration. 3. **Set up a discount Function** Configure the discount function to handle product, order, and shipping discounts using metafields and GraphQL operations. 4. **Configure a test discount** Using your app's interface, create an automatic discount with specific percentages for products, orders, and shipping. 5. **Verify the discount functionality** Test the discount in your development store's cart and checkout flow, confirming all discount types apply correctly. 6. **Deploy the app to Shopify** Deploy and release your app version to Shopify. ![The UI for configuring the discount](https://cdn.shopify.com/shopifycloud/shopify-dev/production/assets/assets/apps/discounts/discount-function-order-product-shipping-wFP7iSsc.png) ## Requirements [Create a Partner account](https://www.shopify.com/partners) [Create a development store](https://shopify.dev/docs/apps/tools/development-stores#create-a-development-store-to-test-your-app) [Install Node.js](https://nodejs.org/en/download) Install Node.js 22 or higher. ## Project ![](https://shopify.dev/images/logos/JS.svg)![](https://shopify.dev/images/logos/JS-dark.svg) JavaScript [View on GitHub](https://github.com/Shopify/discounts-reference-app/tree/main/examples/remix-app) ## Scaffold the Remix app To scaffold a complete Remix app, run this command to set up your development environment with a fully functional Remix app. ## Terminal ```bash shopify app init --template https://github.com/Shopify/discounts-reference-app/examples/remix-app ``` This command sets up your development environment with a fully functional Remix app. The next steps walk you through the key components. ## Understand the discount configuration UI The app structure includes these key directories. * πŸ“ `components/` Reusable UI components * πŸ“ `graphql/` GraphQL queries and mutations for functions, collections, and discounts * πŸ“ `hooks/` Custom React hooks, including `useDiscountForm` for discount management * πŸ“ `routes/` Remix routes handling app navigation, authentication, and discount management * πŸ“ `types/` TypeScript definitions including form types, admin types, and generated types * πŸ“ `utils/` Utility functions including navigation helpers Info The app integrates [@shopify/polaris](https://www.npmjs.com/package/@shopify/polaris) components to match the Shopify admin interface. ## Review routes, UI components, and discount configuration The route files manage discount creation and editing. ### Ensure the correct access scopes are set Review the `shopify.app.toml` file to ensure the correct access scopes are set. Your app needs `write_discounts` and `read_products` scopes. ## shopify.app.toml ```toml # This file stores configurations for your Shopify app. scopes = "write_discounts,read_products" [webhooks] api_version = "2024-10" # Handled by: /app/routes/webhooks.app.uninstalled.tsx [[webhooks.subscriptions]] uri = "/webhooks/app/uninstalled" topics = ["app/uninstalled"] # Handled by: /app/routes/webhooks.app.scopes_update.tsx [[webhooks.subscriptions]] topics = [ "app/scopes_update" ] uri = "/webhooks/app/scopes_update" # Webhooks can have filters # Only receive webhooks for product updates with a product price >= 10.00 # See: https://shopify.dev/docs/apps/build/webhooks/customize/filters # [[webhooks.subscriptions]] # topics = ["products/update"] # uri = "/webhooks/products/update" # filter = "variants.price:>=10.00" # Mandatory compliance topic for public apps only # See: https://shopify.dev/docs/apps/build/privacy-law-compliance # [[webhooks.subscriptions]] # uri = "/webhooks/customers/data_request" # compliance_topics = ["customers/data_request"] # [[webhooks.subscriptions]] # uri = "/webhooks/customers/redact" # compliance_topics = ["customers/redact"] # [[webhooks.subscriptions]] # uri = "/webhooks/shop/redact" # compliance_topics = ["shop/redact"] ``` ### Explore the create discount route Review the discount creation logic in the create route file. The [`action`](https://remix.run/docs/main/route/action) function processes form submissions through the [`discountCodeAppCreate`](https://shopify.dev/docs/api/admin-graphql/latest/mutations/discountCodeAppCreate) mutation. ### Examine the edit discount route To understand discount editing, review the edit route file. ### Review the `DiscountForm` component The DiscountForm component is responsible for rendering the form that allows merchants to configure the discount: This form includes fields required by the discount creation mutation: * `title` * `method` * `code` * `combinesWith` * `discountClasses` * `usageLimit` * `appliesOncePerCustomer` * `startsAt` * `endsAt` * `metafield` ### Understand the `discountClasses` configuration The form includes a section for selecting discount classes. This section allows merchants to apply discounts to `PRODUCT`, `ORDER`, and `SHIPPING` classes. ## app/components/DiscountForm/DiscountForm.tsx ```tsx import { Form } from "@remix-run/react"; import { Banner, Card, Text, Layout, PageActions, TextField, BlockStack, Box, Checkbox, Select, InlineStack, } from "@shopify/polaris"; import { returnToDiscounts } from "app/utils/navigation"; import { useCallback, useMemo, useState } from "react"; import { useDiscountForm } from "../../hooks/useDiscountForm"; import { DiscountClass } from "../../types/admin.types.d"; import { DiscountMethod } from "../../types/types"; import { CollectionPicker } from "../CollectionPicker/CollectionPicker"; import { DatePickerField } from "../DatePickerField/DatePickerField"; interface SubmitError { message: string; field: string[]; } interface DiscountFormProps { initialData?: { title: string; method: DiscountMethod; code: string; combinesWith: { orderDiscounts: boolean; productDiscounts: boolean; ``` ### Review the discount percentage configuration Examine how merchants set discount percentages. This section of the form allows merchants to set the discount percentage for each class, using a number input. ## app/components/DiscountForm/DiscountForm.tsx ```tsx import { Form } from "@remix-run/react"; import { Banner, Card, Text, Layout, PageActions, TextField, BlockStack, Box, Checkbox, Select, InlineStack, } from "@shopify/polaris"; import { returnToDiscounts } from "app/utils/navigation"; import { useCallback, useMemo, useState } from "react"; import { useDiscountForm } from "../../hooks/useDiscountForm"; import { DiscountClass } from "../../types/admin.types.d"; import { DiscountMethod } from "../../types/types"; import { CollectionPicker } from "../CollectionPicker/CollectionPicker"; import { DatePickerField } from "../DatePickerField/DatePickerField"; interface SubmitError { message: string; field: string[]; } interface DiscountFormProps { initialData?: { title: string; method: DiscountMethod; code: string; combinesWith: { orderDiscounts: boolean; productDiscounts: boolean; ``` ### Explore the `CollectionPicker` component Review how collection selection works. This section uses the AppBridge `ResourcePicker` component to allow merchants to select collections, to make sure that discounts are applied to the expected products. Info The discount configuration uses metafields for storage. Learn more about [using metafields with input queries](https://shopify.dev/docs/apps/build/functions/input-output/metafields-for-input-queries). ## Review server-side discount management To learn how the Remix app handles server-side operations, examine the files in the app/models directory. These files query and mutate resources using Shopify's GraphQL Admin API. ### Explore functions server file This file queries the Functions associated with your app. We use this query to populate the `functionId` field used in the `discountAutomaticAppCreate` and `discountCodeAppCreate` mutations. ### Review collections server file This file handles collection data stored in discount metafields. We use this query to populate the list of collections displayed in the section where merchants can select collections to target with the discount. When editing a discount, we use this query to populate the list of collections displayed below the resource picker. ### Examine discounts server file This file manages discount creation, updates, and retrieval. We use this file to create, read, and update discounts. ## Review Graph​QL operations In this step, you'll examine the app's GraphQL queries and mutations. These operations communicate with the Shopify Admin GraphQL API to create, read, and update discounts, retrieve collections, and retrieve functions. ### Review discount graphql file These queries and mutations handle, retrieving discounts, creating code and automatic discounts, and updating code and automatic discounts. ### Examine collections graphql file This file contains queries for collection data which is used to populate the list of collections displayed in the section where merchants can select collections. When editing a discount, we use this query to populate the list of collections displayed below the resource picker. ### Review functions graphql file This query returns functions for your app when the app is installed on a merchant's store. This example uses this query to populate the app's home page, which allows you to navigate to the create discount page and it also populates the `functionId` field used in the `discountAutomaticAppCreate` and `discountCodeAppCreate` mutations. ## Set up the Discount Function Now, create a Discount Function. This function will be used to apply discounts to products, orders, and shipping, and merchants can configure these discounts using your Remix app's UI. Run this command to scaffold your Discount Function: ## Terminal ```bash shopify app generate extension --template discount --name discount-function-js ``` ## Configure the Discount Function In this step, you'll configure the Discount Function to apply discounts to products, orders, and shipping based on the discount configuration that is stored on the discount instance and its metafield. Note Your Function should only return operations for `discountClasses` that the discount applies to. For example, if the discount is configured to apply to `PRODUCT` and `ORDER`, but not `SHIPPING`, your Function should only return operations for `PRODUCT` and `ORDER`. ### Define the UI paths and input variables 1. Update the UI paths in `shopify.extension.toml`. This property tells the Shopify admin where to find the UI that allows merchants to configure discounts associated with your Discount Function. 2. Register a metafield variable that your Function will use as a dynamic input. Refer to [variables in input queries](https://shopify.dev/docs/apps/build/functions/input-output/use-variables-input-queries) for more information. In this example, the `collectionIds` property of the metafield object is used as the input variable for the Function. ## extensions/shopify.extension.toml ```toml api_version = "2025-04" [[extensions]] name = "t:name" description = "t:description" handle = "discount-function-rs" type = "function" [[extensions.targeting]] target = "cart.lines.discounts.generate.run" input_query = "src/generate_cart_run.graphql" export = "generate-cart-run" [[extensions.targeting]] target = "cart.delivery-options.discounts.generate.run" input_query = "src/generate_delivery_run.graphql" export = "generate-delivery-run" [extensions.build] command = "" path = "dist/function.wasm" [extensions.input.variables] namespace = "$app:example-discounts--ui-extension" key = "function-configuration" [extensions.ui] handle = "ui-multiclass-metafield-js" ``` ### Query the data needed for your Function cart run target The `cart_lines_discounts_generate_run.graphql` file drives your function logic by querying essential cart data which is used as the input for your Function. This file queries: * Cart properties to use with the `inAnyCollection` field for determining which collections your Function will target, `$collectionIds` are passed to the query as a variable. * The `discountClasses` property to identify which discount classes (PRODUCT, ORDER, SHIPPING) your Function will return discounts for. * Metafield data to retrieve collection IDs and discount percentage values. The metafield is queried by its key and namespace. Note The [`inAnyCollection`](https://shopify.dev/docs/api/functions/reference/discount/graphql/common-objects/product) field is used to determine whether a product belongs to one of the specified collections. This field is `true` when a product variant is associated with the specified set of collections, and `false` otherwise. Note that if the collection set is empty, it returns `false`. ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.graphql ```graphql query CartInput($collectionIds: [ID!]) { cart { lines { id cost { subtotalAmount { amount } } merchandise { __typename ... on ProductVariant { product { inAnyCollection(ids: $collectionIds) } } } } } discount { discountClasses metafield( namespace: "$app:example-discounts--ui-extension" key: "function-configuration" ) { value } } } ``` ### Query the data needed for your Function delivery run target The `cart_delivery_options_discounts_generate_run.graphql` file drives your function logic by querying essential delivery data which is used as the input for your Function. This file queries: * Cart `deliveryGroups` to retrieve the delivery options available to the customer. * The `discountClasses` property to determine whether the SHIPPING discount class is set. * Metafield data to retrieve the discount percentage for delivery options. The metafield is queried by its key and namespace. ## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_run.graphql ```graphql query DeliveryInput { cart { deliveryGroups { id } } discount { discountClasses metafield( namespace: "$app:example-discounts--ui-extension" key: "function-configuration" ) { value } } } ``` ### Create your cart run Function logic Using the input data from the `cart_lines_discounts_generate_run.graphql` file, you can create your Function's logic. In this example, you retrieve the metafield object which contains the cart line and order discount percentages, the collection IDs for which the discount applies and the discountClasses that your discount will apply to. You can then use this data to create your Function's logic. First, you parse the metafield, then you can conditionally add [ProductDiscountsAddOperation](https://shopify.dev/docs/api/functions/reference/discount/graphql/common-objects/productdiscountsaddoperation) and [OrderDiscountsAddOperation](https://shopify.dev/docs/api/functions/reference/discount/graphql/common-objects/orderdiscountsaddoperation) operations to the return value based on whether the cart line's product is part of a collection that your discount targets, and whether the discountClasses for the discount are set to `PRODUCT` or `ORDER`. ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.js ```javascript import { OrderDiscountSelectionStrategy, ProductDiscountSelectionStrategy, DiscountClass, } from "../generated/api"; export function cartLinesDiscountsGenerateRun(input) { if (!input.cart.lines.length) { throw new Error("No cart lines found"); } const { cartLinePercentage, orderPercentage, collectionIds } = parseMetafield( input.discount.metafield, ); const hasOrderDiscountClass = input.discount.discountClasses.includes( DiscountClass.Order, ); const hasProductDiscountClass = input.discount.discountClasses.includes( DiscountClass.Product, ); if (!hasOrderDiscountClass && !hasProductDiscountClass) { return { operations: [] }; } const operations = []; // Add product discounts first if available and allowed if (hasProductDiscountClass && cartLinePercentage > 0) { const cartLineTargets = input.cart.lines.reduce((targets, line) => { if ( "product" in line.merchandise && (line.merchandise.product.inAnyCollection || collectionIds.length === 0) ) { targets.push({ cartLine: { id: line.id, }, }); } return targets; }, []); if (cartLineTargets.length > 0) { operations.push({ productDiscountsAdd: { candidates: [ { message: `${cartLinePercentage}% OFF PRODUCT`, targets: cartLineTargets, value: { percentage: { value: cartLinePercentage, }, }, }, ], selectionStrategy: ProductDiscountSelectionStrategy.First, }, }); } } // Then add order discounts if available and allowed if (hasOrderDiscountClass && orderPercentage > 0) { operations.push({ orderDiscountsAdd: { candidates: [ { message: `${orderPercentage}% OFF ORDER`, targets: [ { orderSubtotal: { excludedCartLineIds: [], }, }, ], value: { percentage: { value: orderPercentage, }, }, }, ], selectionStrategy: OrderDiscountSelectionStrategy.First, }, }); } return { operations }; } function parseMetafield(metafield) { try { const value = JSON.parse(metafield.value); return { cartLinePercentage: value.cartLinePercentage || 0, orderPercentage: value.orderPercentage || 0, collectionIds: value.collectionIds || [], }; } catch (error) { console.error("Error parsing metafield", error); return { cartLinePercentage: 0, orderPercentage: 0, collectionIds: [], }; } } ``` ### Create your delivery run Function logic Using the input data from the `cart_delivery_options_discounts_generate_run.graphql` file, you can create your Function's logic. In this example, you retrieve the metafield object which contains the delivery discount percentage. You can then use this data to create your Function's logic. First, you parse the metafield, then you can conditionally add [DeliveryDiscountsAddOperation](https://shopify.dev/docs/api/functions/reference/discount/graphql/common-objects/deliverydiscountsaddoperation) operations to the return value based on whether the discountClasses for the discount are set to `SHIPPING`. ## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_run.js ```javascript import { DeliveryDiscountSelectionStrategy, DiscountClass, } from "../generated/api"; export function cartDeliveryOptionsDiscountsGenerateRun(input) { const firstDeliveryGroup = input.cart.deliveryGroups[0]; if (!firstDeliveryGroup) { throw new Error("No delivery groups found"); } const { deliveryPercentage } = parseMetafield(input.discount.metafield); const hasShippingDiscountClass = input.discount.discountClasses.includes( DiscountClass.Shipping, ); if (!hasShippingDiscountClass) { return { operations: [] }; } const operations = []; if (hasShippingDiscountClass && deliveryPercentage > 0) { operations.push({ deliveryDiscountsAdd: { candidates: [ { message: `${deliveryPercentage}% OFF DELIVERY`, targets: [ { deliveryGroup: { id: firstDeliveryGroup.id, }, }, ], value: { percentage: { value: deliveryPercentage, }, }, }, ], selectionStrategy: DeliveryDiscountSelectionStrategy.All, }, }); } return { operations }; } function parseMetafield(metafield) { try { const value = JSON.parse(metafield.value); return { deliveryPercentage: value.deliveryPercentage || 0 }; } catch (error) { console.error("Error parsing metafield", error); return { deliveryPercentage: 0 }; } } ``` ### Start your app to test 1. Save your updated configuration TOML file. 2. Start `app dev` if it's not already running: ## Terminal ```bash shopify app dev ``` The configuration TOML file changes will be applied automatically on the development store. ## Create a test discount 1. In your Shopify admin, navigate to **Discounts**. 2. To prevent conflicting discounts from activating, deactivate any existing discounts. 3. Click **Create discount**. 4. Under your app name, select your discount function. 5. Configure the discount with these values: * **Method**: **Automatic** * **Title**: **Product, Order, Shipping Discount** * **DiscountClasses**: Select **Product**, **Order**, and **Shipping** * **Product discount percentage**: **20** * **Order discount percentage**: **10** * **Shipping discount percentage**: **5** * **Collection IDs**: Select your test collections 6. Click **Save** ## Test the discount 1. Open **Discounts** in your Shopify admin 2. Locate your new cart line, order, and shipping discount ![A list of all active discounts for the store.](https://cdn.shopify.com/shopifycloud/shopify-dev/production/assets/assets/apps/discounts/functions-discount-list-multi-class-bMqqn_8b.png) 3. Now, go to your development store and add products to your cart. Your cart page displays: * Product line discounts * Order subtotal discount Your checkout page displays: * Product line discounts * Order subtotal discount * Shipping rate discounts (after entering shipping address) ![A checkout summary that lists discounts for all three classes](https://cdn.shopify.com/shopifycloud/shopify-dev/production/assets/assets/apps/discounts/multi-class-DqNqJQCF.png) ### Review the execution of the Function ### Review the Function execution 1. In the terminal where `shopify app dev` is running, review your Function executions. When [testing Functions on development stores](https://shopify.dev/docs/apps/build/functions/test-debug-functions#test-your-function-on-a-development-store), the `dev` output shows Function executions, debug logs you've added, and a link to a local file containing full execution details. 2. In a new terminal window, use the Shopify CLI command [`app function replay`](https://shopify.dev/docs/api/shopify-cli/app/app-function-replay) to [replay a Function execution locally](https://shopify.dev/docs/apps/build/functions/test-debug-functions#execute-the-function-locally-using-shopify-cli). This lets you debug your Function without triggering it again on Shopify. ## Terminal ```terminal shopify app function replay ``` 3. Select the Function execution from the top of the list. Press `q` to quit when you are finished debugging. ## Deploy your app When you're ready to release your changes to users, you can create and release an [app version](https://shopify.dev/docs/apps/launch/deployment/app-versions). An app version is a snapshot of your app configuration and all extensions. 1. Navigate to your app directory. 2. Run the following command. Optionally, you can provide a name or message for the version using the `--version` and `--message` flags. ## Terminal ```terminal shopify app deploy ``` Releasing an app version replaces the current active version that's served to stores that have your app installed. It might take several minutes for app users to be upgraded to the new version. Tip If you want to create a version, but avoid releasing it to users, then run the `deploy` command with a `--no-release` flag. You can release the unreleased app version using Shopify CLI's [`release`](https://shopify.dev/docs/api/shopify-cli/app/app-release) command, or through the Dev Dashboard. ## shopify.app.toml ```toml # This file stores configurations for your Shopify app. scopes = "write_discounts,read_products" [webhooks] api_version = "2024-10" # Handled by: /app/routes/webhooks.app.uninstalled.tsx [[webhooks.subscriptions]] uri = "/webhooks/app/uninstalled" topics = ["app/uninstalled"] # Handled by: /app/routes/webhooks.app.scopes_update.tsx [[webhooks.subscriptions]] topics = [ "app/scopes_update" ] uri = "/webhooks/app/scopes_update" # Webhooks can have filters # Only receive webhooks for product updates with a product price >= 10.00 # See: https://shopify.dev/docs/apps/build/webhooks/customize/filters # [[webhooks.subscriptions]] # topics = ["products/update"] # uri = "/webhooks/products/update" # filter = "variants.price:>=10.00" # Mandatory compliance topic for public apps only # See: https://shopify.dev/docs/apps/build/privacy-law-compliance # [[webhooks.subscriptions]] # uri = "/webhooks/customers/data_request" # compliance_topics = ["customers/data_request"] # [[webhooks.subscriptions]] # uri = "/webhooks/customers/redact" # compliance_topics = ["customers/redact"] # [[webhooks.subscriptions]] # uri = "/webhooks/shop/redact" # compliance_topics = ["shop/redact"] ``` ## Tutorial complete! You've successfully created a Discount Function and Remix app that allows merchants to set the discounts applied by that Function. Now, you can use this Function to apply discounts that target cart lines, order subtotals, and shipping rates. *** ### Next Steps [![](https://shopify.dev/images/icons/32/tutorial.png)![](https://shopify.dev/images/icons/32/tutorial-dark.png)](https://shopify.dev/docs/apps/build/discounts/network-access) [Add network access to your discount Function](https://shopify.dev/docs/apps/build/discounts/network-access) [Learn how to add network access to your discount Function to query an external system for discount code validation.](https://shopify.dev/docs/apps/build/discounts/network-access) [![](https://shopify.dev/images/icons/32/gear.png)![](https://shopify.dev/images/icons/32/gear-dark.png)](https://shopify.dev/docs/apps/build/discounts/ux-for-discounts) [Review the UX guidelines](https://shopify.dev/docs/apps/build/discounts/ux-for-discounts) [Review the UX guidelines to learn how to implement discounts in user interfaces.](https://shopify.dev/docs/apps/build/discounts/ux-for-discounts) [![](https://shopify.dev/images/icons/32/gear.png)![](https://shopify.dev/images/icons/32/gear-dark.png)](https://shopify.dev/docs/apps/build/functions) [Learn more about Shopify Functions](https://shopify.dev/docs/apps/build/functions) [Learn more about how Shopify Functions work and the benefits of using Shopify Functions.](https://shopify.dev/docs/apps/build/functions) [![](https://shopify.dev/images/icons/32/graphql.png)![](https://shopify.dev/images/icons/32/graphql-dark.png)](https://shopify.dev/docs/api/functions) [Consult the Shopify Functions API references](https://shopify.dev/docs/api/functions) [Consult the API references for Shopify Functions](https://shopify.dev/docs/api/functions) [![](https://shopify.dev/images/icons/32/app.png)![](https://shopify.dev/images/icons/32/app-dark.png)](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions) [Learn more about deploying app versions](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions) [Learn more about deploying app versions to Shopify](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions)