--- 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. ![The discount details showing an Admin UI Extension block](https://cdn.shopify.com/shopifycloud/shopify-dev/production/assets/assets/apps/discounts/discount-ui-extension-DKy8Wcvq.png) ## 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 ![](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/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. ![The Discount Function settings block with the extension name](https://cdn.shopify.com/shopifycloud/shopify-dev/production/assets/assets/apps/discounts/discount-extension-name-card-BptDgxX_.png) ### 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. ![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) ### 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/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)