---
title: Build a discounts UI with Admin UI extensions
description: Learn how to use an Admin UI Extension to create a UI in Shopify admin for configuring a Discount Function.
source_url:
html: https://shopify.dev/docs/apps/build/discounts/build-ui-extension?extension=javascript
md: https://shopify.dev/docs/apps/build/discounts/build-ui-extension.md?extension=javascript
---
# Build a discounts UI with Admin UI extensions
With [Shopify Functions](https://shopify.dev/docs/apps/build/functions), you can create new types of discounts that apply to cart lines, order subtotals, shipping rates, or any combination of these.
Additionally, you can use Admin UI extensions to create a UI, displayed on the discount details page of Shopify admin, for merchants to configure your app's Discount Function. To do this, this custom UI will use metafields to communicate with your Discount Function.
This tutorial describes how to create an [Admin UI Extension](https://shopify.dev/docs/api/admin-extensions) that merchants can use to configure a Discount Function.

## What you'll learn
In this tutorial, you'll learn how to do the following tasks:
* Create an Admin UI Extension, written in React and hosted by Shopify
* Build a Discount Function that applies a percentage off cart lines, order subtotal and shipping rates
* Associate your Admin UI extension to your Discount Function, so merchants can use the UI to configure the discounts provided by the Discount Function
* Configure app-scoped metafields to store discount settings
After completing this tutorial, you'll be able to use your Shopify admin to configure a Discount Function that discounts cart lines (filtered by collection), order subtotal, and shipping.
## 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)
[Scaffold an app with a Discount Function](https://shopify.dev/docs/apps/build/discounts/build-discount-function)
For example, follow the [Build a Discount Function](https://shopify.dev/docs/apps/build/discounts/build-discount-function) tutorial, or build your own app and Discount Function using the [Discount API](https://shopify.dev/docs/apps/build/discounts).
## Project

JavaScript
[View on GitHub](https://github.com/Shopify/discounts-reference-app/tree/main/examples/javascript/metafield)
## Create an Admin UI Extension
First, scaffold the Admin UI extension that will provide the UI that merchants can use to configure your Discount Function.
1. Navigate to your app's directory:
## Terminal
```bash
cd directory
```
1. Run the following command to create a new Discount Function Settings Admin UI Extension:
## Terminal
```bash
shopify app generate extension --template discount_details_function_settings --name discount-ui-js --flavor react
```
## Configure your Admin UI Extension
Your Admin UI extension will use a metafield to tell your Discount Function which collections of products to discount, and the specific discount percentage to offer for products, order subtotal, and shipping. The Discount Function will use this metafield to dynamically create discount operations.

### Metadata
Specify your extension's metadata in the `[[extensions]]` section of its associated configuration file. In this case, `name` and `description` will appear in the **Discounts** and discount details page of the Shopify admin.
Info
Shopify provides localization tools, and you can use them to localize the name and description of Discount Functions. To learn more, refer to [localizing your Shopify app](https://shopify.dev/docs/apps/build/localize-your-app).
## extensions/ui-extension/shopify.extension.toml
```toml
api_version = "2024-10"
[[extensions]]
# Change the merchant-facing name of the extension in locales/en.default.json
name = "t:name"
description = "t:description"
handle = "discount-ui-js"
type = "ui_extension"
# Only 1 target can be specified for each Admin block extension
[[extensions.targeting]]
module = "./src/DiscountFunctionSettings.jsx"
# The target used here must match the target used in the module file (./src/DiscountFunctionSettings.jsx)
target = "admin.discount-details.function-settings.render"
```
### Target
In the `[[extensions.targeting]]` section of your extension's configuration file, you'll define its `target` and `module`.
`target` specifies where in the Shopify Admin surface the extension should display. For this tutorial, set `target` to `admin.discount-details.function-settings.render`.
`module` specifies a path to the file that contains the main export.
Refer to [`App Extensions Common properties`](https://shopify.dev/docs/apps/build/app-extensions/configure-app-extensions#common-properties) for more information.
## extensions/ui-extension/shopify.extension.toml
```toml
api_version = "2024-10"
[[extensions]]
# Change the merchant-facing name of the extension in locales/en.default.json
name = "t:name"
description = "t:description"
handle = "discount-ui-js"
type = "ui_extension"
# Only 1 target can be specified for each Admin block extension
[[extensions.targeting]]
module = "./src/DiscountFunctionSettings.jsx"
# The target used here must match the target used in the module file (./src/DiscountFunctionSettings.jsx)
target = "admin.discount-details.function-settings.render"
```
### Associate your Discount Function with the Admin UI Extension
Your Admin UI extension can configure your Discount Function. To connect them, open your Function's `.toml` configuration file and find the `[extensions.ui]` section. Set the handle property to match the handle in your Admin UI extension's `shopify.extension.toml` file.
## extensions/discount-function/shopify.extension.toml
```toml
api_version = "2025-04"
[[extensions]]
name = "t:name"
description = "t:description"
handle = "discount-function-js"
type = "function"
[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
export = "cart-lines-discounts-generate-run"
[[extensions.targeting]]
target = "cart.delivery-options.discounts.generate.run"
input_query = "src/cart_delivery_options_discounts_generate_run.graphql"
export = "cart-delivery-options-discounts-generate-run"
[extensions.build]
command = ""
path = "dist/function.wasm"
[extensions.input.variables]
namespace = "$app:example-discounts--ui-extension"
key = "function-configuration"
[extensions.ui]
handle = "discount-ui-js"
```
## Review the Admin UI Extension
Before continuing on with the tutorial, review the code for your Admin UI extension, so that you understand how it's configured, how it displays data, and how it manages state.
### Initialize the Admin UI Extension
The discount details page renders the `admin.discount-details.function-settings.render` target. The `target` must be provided to the `reactExtension` function, and must match the `extensions.targeting.target` defined in the [previous step](#target).
## extensions/ui-extension/src/DiscountFunctionSettings.jsx
```jsx
import {
reactExtension,
useApi,
BlockStack,
FunctionSettings,
Section,
Text,
Form,
NumberField,
Box,
InlineStack,
Heading,
TextField,
Button,
Icon,
Link,
Divider,
Select,
} from "@shopify/ui-extensions-react/admin";
import { useState, useMemo, useEffect } from "react";
const TARGET = "admin.discount-details.function-settings.render";
export default reactExtension(TARGET, async (api) => {
const existingDefinition = await getMetafieldDefinition(api.query);
if (!existingDefinition) {
// Create a metafield definition for persistence if no pre-existing definition exists
const metafieldDefinition = await createMetafieldDefinition(api.query);
if (!metafieldDefinition) {
throw new Error("Failed to create metafield definition");
}
}
return ;
});
```
### Use UI components
Admin UI extensions are rendered using [Remote UI](https://github.com/Shopify/remote-dom/tree/remote-ui), which is a fast and secure remote-rendering framework. Because Shopify renders the UI remotely, components used in the extensions must comply with a contract in the Shopify host. Shopify provides these components through the Admin UI extensions library.
[Admin UI extensions components](https://shopify.dev/docs/api/admin-extensions/components)
## extensions/ui-extension/src/DiscountFunctionSettings.jsx
```jsx
import {
reactExtension,
useApi,
BlockStack,
FunctionSettings,
Section,
Text,
Form,
NumberField,
Box,
InlineStack,
Heading,
TextField,
Button,
Icon,
Link,
Divider,
Select,
} from "@shopify/ui-extensions-react/admin";
import { useState, useMemo, useEffect } from "react";
const TARGET = "admin.discount-details.function-settings.render";
export default reactExtension(TARGET, async (api) => {
const existingDefinition = await getMetafieldDefinition(api.query);
if (!existingDefinition) {
// Create a metafield definition for persistence if no pre-existing definition exists
const metafieldDefinition = await createMetafieldDefinition(api.query);
if (!metafieldDefinition) {
throw new Error("Failed to create metafield definition");
}
}
return ;
});
```
### The `App` component
The `App` component is the main component of the extension. It renders a form with percentage fields and a collection picker. It uses the `useExtensionData` hook to manage the state of the form.
## extensions/ui-extension/src/DiscountFunctionSettings.jsx
```jsx
import {
reactExtension,
useApi,
BlockStack,
FunctionSettings,
Section,
Text,
Form,
NumberField,
Box,
InlineStack,
Heading,
TextField,
Button,
Icon,
Link,
Divider,
Select,
} from "@shopify/ui-extensions-react/admin";
import { useState, useMemo, useEffect } from "react";
const TARGET = "admin.discount-details.function-settings.render";
export default reactExtension(TARGET, async (api) => {
const existingDefinition = await getMetafieldDefinition(api.query);
if (!existingDefinition) {
// Create a metafield definition for persistence if no pre-existing definition exists
const metafieldDefinition = await createMetafieldDefinition(api.query);
if (!metafieldDefinition) {
throw new Error("Failed to create metafield definition");
}
}
return ;
});
```
### The `AppliesToCollections` and `CollectionsSection` components
The `AppliesToCollections` component renders a `Select` component which allows users to dynamically select `Collections` from your shop. To do this, it uses the [Admin UI Extensions resource picker API](https://shopify.dev/docs/api/app-home/apis/resource-picker).
The `CollectionsSection` component renders the collections section, displaying the selected collections and a button to add or remove collections.
## extensions/ui-extension/src/DiscountFunctionSettings.jsx
```jsx
import {
reactExtension,
useApi,
BlockStack,
FunctionSettings,
Section,
Text,
Form,
NumberField,
Box,
InlineStack,
Heading,
TextField,
Button,
Icon,
Link,
Divider,
Select,
} from "@shopify/ui-extensions-react/admin";
import { useState, useMemo, useEffect } from "react";
const TARGET = "admin.discount-details.function-settings.render";
export default reactExtension(TARGET, async (api) => {
const existingDefinition = await getMetafieldDefinition(api.query);
if (!existingDefinition) {
// Create a metafield definition for persistence if no pre-existing definition exists
const metafieldDefinition = await createMetafieldDefinition(api.query);
if (!metafieldDefinition) {
throw new Error("Failed to create metafield definition");
}
}
return ;
});
```
### Manage the form's dirty state
Since the `ResourcePicker` doesn't directly update the form's state, it doesn't enable the **Save** action on the discount details page. To fix this, add a hidden `TextField`, inside a `Box` component, and use it to store the selected collection IDs as a serialized value. This hidden field integrates with the [`FunctionSettings`](https://shopify.dev/docs/api/admin-extensions/components/forms/functionsettings) component, enabling the form to detect changes and allowing merchants to save their discount settings.
## extensions/ui-extension/src/DiscountFunctionSettings.jsx
```jsx
import {
reactExtension,
useApi,
BlockStack,
FunctionSettings,
Section,
Text,
Form,
NumberField,
Box,
InlineStack,
Heading,
TextField,
Button,
Icon,
Link,
Divider,
Select,
} from "@shopify/ui-extensions-react/admin";
import { useState, useMemo, useEffect } from "react";
const TARGET = "admin.discount-details.function-settings.render";
export default reactExtension(TARGET, async (api) => {
const existingDefinition = await getMetafieldDefinition(api.query);
if (!existingDefinition) {
// Create a metafield definition for persistence if no pre-existing definition exists
const metafieldDefinition = await createMetafieldDefinition(api.query);
if (!metafieldDefinition) {
throw new Error("Failed to create metafield definition");
}
}
return ;
});
```
### Manage state with the `useExtensionData` hook
The `useExtensionData` hook manages the state of the form. It uses the `useApi` hook to interact with the GraphQL Admin API. The `useEffect` hook is used to set the initial state of the form.
## extensions/ui-extension/src/DiscountFunctionSettings.jsx
```jsx
import {
reactExtension,
useApi,
BlockStack,
FunctionSettings,
Section,
Text,
Form,
NumberField,
Box,
InlineStack,
Heading,
TextField,
Button,
Icon,
Link,
Divider,
Select,
} from "@shopify/ui-extensions-react/admin";
import { useState, useMemo, useEffect } from "react";
const TARGET = "admin.discount-details.function-settings.render";
export default reactExtension(TARGET, async (api) => {
const existingDefinition = await getMetafieldDefinition(api.query);
if (!existingDefinition) {
// Create a metafield definition for persistence if no pre-existing definition exists
const metafieldDefinition = await createMetafieldDefinition(api.query);
if (!metafieldDefinition) {
throw new Error("Failed to create metafield definition");
}
}
return ;
});
```
## Metafields in Admin UI Extensions
Metafields are used to persist shared state between the Discount Function and the Admin UI Extension. Metafields are scoped to an app, and they're unique for each discount created.
### Review how the code creates metafields
For security reasons, you must create your metafield definition under a reserved namespace. Reserved metafield definitions keep your app's metafields private, and you have to set any additional permissions manually. Because this Admin UI extension runs in Shopify admin, you'll need to request these permissions when installing the app.
Use the `$app` scope to create a metafield definition under a reserved namespace. The namespace and key must be unique.
App scoped metafield definition
Learn more about [metafield definitions under an app-scoped namespace](https://shopify.dev/docs/apps/build/custom-data/reserved-prefixes#create-a-metafield-definition-under-a-reserved-namespace).
## extensions/ui-extension/src/DiscountFunctionSettings.jsx
```jsx
import {
reactExtension,
useApi,
BlockStack,
FunctionSettings,
Section,
Text,
Form,
NumberField,
Box,
InlineStack,
Heading,
TextField,
Button,
Icon,
Link,
Divider,
Select,
} from "@shopify/ui-extensions-react/admin";
import { useState, useMemo, useEffect } from "react";
const TARGET = "admin.discount-details.function-settings.render";
export default reactExtension(TARGET, async (api) => {
const existingDefinition = await getMetafieldDefinition(api.query);
if (!existingDefinition) {
// Create a metafield definition for persistence if no pre-existing definition exists
const metafieldDefinition = await createMetafieldDefinition(api.query);
if (!metafieldDefinition) {
throw new Error("Failed to create metafield definition");
}
}
return ;
});
```
### Review the code that updates metafields
When a merchant uses your Admin UI extension to update a discount's details, the `applyExtensionMetafieldChange` function saves the updated form values directly into the discount's metafield in Shopify.
## extensions/ui-extension/src/DiscountFunctionSettings.jsx
```jsx
import {
reactExtension,
useApi,
BlockStack,
FunctionSettings,
Section,
Text,
Form,
NumberField,
Box,
InlineStack,
Heading,
TextField,
Button,
Icon,
Link,
Divider,
Select,
} from "@shopify/ui-extensions-react/admin";
import { useState, useMemo, useEffect } from "react";
const TARGET = "admin.discount-details.function-settings.render";
export default reactExtension(TARGET, async (api) => {
const existingDefinition = await getMetafieldDefinition(api.query);
if (!existingDefinition) {
// Create a metafield definition for persistence if no pre-existing definition exists
const metafieldDefinition = await createMetafieldDefinition(api.query);
if (!metafieldDefinition) {
throw new Error("Failed to create metafield definition");
}
}
return ;
});
```
## Apply cart line and order subtotal discounts
Discount Functions return operations which contain discount candidates, that are responsible for applying discounts to cart lines, order subtotals, and shipping rates.
### Update the Discount Function configuration to use Metafields in input queries
Admin UI extensions can use metafields to share data with Discount Functions.
To allow your Discount Function to receive these metafield values as [input variables](https://shopify.dev/docs/apps/build/functions/input-output/use-variables-input-queries), update your Function's `shopify.extension.toml` configuration file. Under `[extensions.input.variables]`, set `namepace` and `key` to the same values set in your Admin UI extension code.
## extensions/discount-function/shopify.extension.toml
```toml
api_version = "2025-04"
[[extensions]]
name = "t:name"
description = "t:description"
handle = "discount-function-js"
type = "function"
[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
export = "cart-lines-discounts-generate-run"
[[extensions.targeting]]
target = "cart.delivery-options.discounts.generate.run"
input_query = "src/cart_delivery_options_discounts_generate_run.graphql"
export = "cart-delivery-options-discounts-generate-run"
[extensions.build]
command = ""
path = "dist/function.wasm"
[extensions.input.variables]
namespace = "$app:example-discounts--ui-extension"
key = "function-configuration"
[extensions.ui]
handle = "discount-ui-js"
```
### Use Metafield properties as input variables
In the [previous step](#update-the-discount-function-configuration-to-use-metafields-in-input-queries), you defined an input variable that uses a discount Metafield. This metafield is queried and updated by the UI Extension.
The input GraphQL query can specify reference fields found in the object stored by a metafield. For example, `collectionIds` is a key present in the metafield created by the Admin UI extension created above. This is particularly useful when using fields with arguments such as the `inAnyCollection` field.
## 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
}
}
}
```
### Use input variables to query `inAnyCollection` field
The [inAnyCollection](https://shopify.dev/docs/api/functions/reference/discount/graphql/common-objects/product) field on the `product` field allows your Discount Function to dynamically determine whether a given `product` in the cart belongs to any of many collections.
## 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 metafield definition to get the discount percentages
The `namespace` and `key` must be the same as the `namespace` and `key` used in the Admin UI Extension, since that metafield stores the discount percentages configured by the extension.
## 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
}
}
}
```
### Update the Discount Function to parse the metafield
Parse the metafield which is stored on the `discount` field to get the function configuration for the `cart.lines.discounts.generate.run` target.
## 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: [],
};
}
}
```
### Apply cart line and order subtotal discounts
Add the cart line and order subtotal operations, using the percentage values set in the discount metafield configuration.
## 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: [],
};
}
}
```
### Discount cart lines based on collection membership
Conditionally add targets to the candidates using the `line.merchandise.product.inAnyCollection` from the input query.
## 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: [],
};
}
}
```
## Use metafields in Discount Function delivery options target
The Delivery Options target is used to apply discounts to shipping rates.
### Query the metafield definition
The `namespace` and `key` must be the same as the `namespace` and `key` used in the Admin UI Extension. This metafield definition is used to obtain the defined percentage for shipping rate discounts.
## 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
}
}
}
```
### Update the Discount Function to parse the metafield
Parse the metafield on the `discount` field to get the percentage value to reduce the delivery groups shipping rates.
## 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 };
}
}
```
### Apply discounts to delivery groups
Add the delivery groups operations using the percentage values from the discount metafield configuration.
## 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 };
}
}
```
## Request the access scopes for reading collections
To access collections, your app must have the `read_products` app scope. To set this up, add the `read_products` scope to your `shopify.app.toml` file.
## shopify.app.toml
```toml
# This file stores configurations for your Shopify app.
# It must exist at the root of your app.
scopes = "write_discounts,read_products"
name="Example Discount App"
```
### Start your app to preview changes
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.
## Test your Discount Function
### Create a discount using your app
1. In the extension preview console opened in the previous step, click on the preview link for the `admin-discount-details.function-settings.render` extension target. This opens the discount details page.
2. Your Admin UI Extension should appear on this page.
3. Configure your discount, and click **Save**.
You should see a new discount in the discounts list.

### Deactivate other discounts
To ensure that the newly created discount is the only active discount, deactivate or delete all other discounts.
### Build a cart
Open your development store and build a cart with a single item in it, use a product that belongs to the collection you configured in the Admin UI Extension.
### Validate discount application
On the cart page, you should see that your Discount Function has applied a discount to a cart line and the order subtotal. When you navigate to the checkout page and provide a shipping address, you'll see the shipping discount.
### 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.
## extensions/ui-extension/shopify.extension.toml
```toml
api_version = "2024-10"
[[extensions]]
# Change the merchant-facing name of the extension in locales/en.default.json
name = "t:name"
description = "t:description"
handle = "discount-ui-js"
type = "ui_extension"
# Only 1 target can be specified for each Admin block extension
[[extensions.targeting]]
module = "./src/DiscountFunctionSettings.jsx"
# The target used here must match the target used in the module file (./src/DiscountFunctionSettings.jsx)
target = "admin.discount-details.function-settings.render"
```
## Tutorial complete!
You've successfully created an Admin UI Extension paired with a Discount Function. You can now use this Function to apply discounts that target cart lines, order subtotals, and shipping rates.
***
### Next Steps
[](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/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/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/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/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)