Version 2025-07 is the last API version to support React-based UI components. Later versions use web components, native UI elements with built-in accessibility, better performance, and consistent styling with Shopify's design system. Check out the upgrade guide to upgrade your extension.
Discounts
Discount details pages display information about a specific discount, including its type, value, conditions, and usage limits. Extensions on these pages help merchants manage promotional campaigns and customize discount workflows.
Anchor to Use casesUse cases
- Sync discounts externally: Push discount codes and rules to external marketing platforms, email services, or affiliate networks.
- Validate discount rules: Check discount configurations against business rules or inventory levels before activation.
- Generate discount reports: Create performance reports showing discount usage, revenue impact, and customer redemption patterns.
- Bulk discount management: Apply changes to multiple discounts at once, such as extending expiration dates or adjusting values.
- Schedule promotions: Set up automated discount activation and deactivation based on marketing calendars or inventory triggers.

Anchor to Discount details targetsDiscount details targets
Use action targets to extend the discount details page with workflows. Action targets open as modal overlays from the More actions menu.
The examples demonstrate fetching data from Shopify's direct API or your app's backend.
Anchor to Discount details action ,[object Object]Discount details action target
admin.discount-details.action.render
Renders an admin action extension on the discounts details page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on discounts data, such as syncing with external systems, exporting discounts information, or managing credit terms.
Extensions at this target can access discount data through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add an action extension that syncs the current discount to an external POS system. This example demonstrates calling your app backend with discount details, handling sync options, and providing feedback on the sync status.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.discount-details.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(''); const [posLocations, setPosLocations] = useState(['all']); const [syncOptions, setSyncOptions] = useState({ includeUsageLimits: true, notifyStaff: false, }); const discountId = data.selected[0].id; const handleSync = async () => { setLoading(true); setError(''); try { const response = await fetch('https://your-app.com/api/pos/sync-discount', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ discountId, locations: posLocations, includeUsageLimits: syncOptions.includeUsageLimits, notifyStaff: syncOptions.notifyStaff, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Sync failed'); } setSuccess(true); close(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to sync discount to POS'); } finally { setLoading(false); } }; return ( <AdminAction title="Sync Discount to POS" primaryAction={ <Button onPress={handleSync} disabled={loading || success}> {loading ? 'Syncing...' : 'Sync to POS'} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && ( <Banner tone="success">Discount synced to POS successfully!</Banner> )} {error && <Banner tone="critical">{error}</Banner>} <ChoiceList title="POS Locations" name="locations" value={posLocations} onChange={setPosLocations} choices={[ { label: 'All locations', value: 'all' }, { label: 'Main Store', value: 'main-store' }, { label: 'Downtown Branch', value: 'downtown' }, { label: 'Mall Kiosk', value: 'mall-kiosk' }, ]} /> <Divider /> <Box padding="base"> <BlockStack gap="base"> <Checkbox checked={syncOptions.includeUsageLimits} onChange={(checked) => setSyncOptions((prev) => ({ ...prev, includeUsageLimits: checked })) } > Include usage limits </Checkbox> <Checkbox checked={syncOptions.notifyStaff} onChange={(checked) => setSyncOptions((prev) => ({ ...prev, notifyStaff: checked })) } > Notify store staff </Checkbox> </BlockStack> </Box> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.discount-details.action.render', (root, api) => { let loading = false; let success = false; let error = ''; let posLocations = ['all']; let includeUsageLimits = true; let notifyStaff = false; const discountId = api.data.selected[0].id; const handleSync = async () => { loading = true; error = ''; updateUI(); try { const response = await fetch('https://your-app.com/api/pos/sync-discount', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ discountId, locations: posLocations, includeUsageLimits, notifyStaff, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Sync failed'); } success = true; updateUI(); api.close(); } catch (err) { error = err instanceof Error ? err.message : 'Failed to sync discount to POS'; updateUI(); } finally { loading = false; updateUI(); } }; const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const content = root.createFragment(); const updateUI = () => { primaryAction.replaceChildren( root.createComponent( Button, { onPress: handleSync, disabled: loading || success }, loading ? 'Syncing...' : 'Sync to POS' ) ); content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, 'Discount synced to POS successfully!') ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } stack.appendChild( root.createComponent(ChoiceList, { title: 'POS Locations', name: 'locations', value: posLocations, onChange: (val) => { posLocations = val; updateUI(); }, choices: [ { label: 'All locations', value: 'all' }, { label: 'Main Store', value: 'main-store' }, { label: 'Downtown Branch', value: 'downtown' }, { label: 'Mall Kiosk', value: 'mall-kiosk' }, ], }) ); stack.appendChild(root.createComponent(Divider, {})); const optionsBox = root.createComponent(Box, { padding: 'base' }); const optionsStack = root.createComponent(BlockStack, { gap: 'base' }); optionsStack.appendChild( root.createComponent( Checkbox, { checked: includeUsageLimits, onChange: (val) => { includeUsageLimits = val; updateUI(); }, }, 'Include usage limits' ) ); optionsStack.appendChild( root.createComponent( Checkbox, { checked: notifyStaff, onChange: (val) => { notifyStaff = val; updateUI(); }, }, 'Notify store staff' ) ); optionsBox.appendChild(optionsStack); stack.appendChild(optionsBox); content.appendChild(stack); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Sync Discount to POS', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that displays performance analytics for the current discount. This example demonstrates querying the GraphQL Admin API for usage statistics, redemption counts, and active period information.
React
import React, { useState, useEffect } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Divider, Text, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.discount-details.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close, query } = useApi(TARGET); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [analytics, setAnalytics] = useState<any>(null); useEffect(() => { fetchDiscountAnalytics(); }, []); const fetchDiscountAnalytics = async () => { const discountId = data.selected[0].id; try { const result = await query( `query GetDiscountAnalytics($id: ID!) { discountNode(id: $id) { id discount { ... on DiscountCodeBasic { title status usageLimit asyncUsageCount startsAt endsAt codes(first: 5) { nodes { code usageCount } } } ... on DiscountCodeFreeShipping { title status usageLimit asyncUsageCount startsAt endsAt } ... on DiscountCodeBxgy { title status usageLimit asyncUsageCount startsAt endsAt } } } }`, { variables: { id: discountId } } ); if (result.data?.discountNode) { setAnalytics(result.data.discountNode); } else { setError('Could not load discount data'); } } catch (err) { setError('Failed to fetch analytics'); } finally { setLoading(false); } }; const discount = analytics?.discount; const usagePercent = discount?.usageLimit ? Math.round((discount.asyncUsageCount / discount.usageLimit) * 100) : null; return ( <AdminAction title="Discount Analytics" secondaryAction={<Button onPress={close}>Close</Button>} > {loading && <Banner tone="info">Loading analytics...</Banner>} {error && <Banner tone="critical">{error}</Banner>} {discount && ( <BlockStack gap="base"> <Box padding="base"> <BlockStack gap="tight"> <Text fontWeight="bold">{discount.title}</Text> <Text>Status: {discount.status}</Text> </BlockStack> </Box> <Divider /> <Box padding="base"> <BlockStack gap="tight"> <Text fontWeight="bold">Usage Statistics</Text> <Text>Total Redemptions: {discount.asyncUsageCount || 0}</Text> {discount.usageLimit && ( <Text>Usage Limit: {discount.usageLimit} ({usagePercent}% used)</Text> )} {!discount.usageLimit && <Text>Usage Limit: Unlimited</Text>} </BlockStack> </Box> <Divider /> <Box padding="base"> <BlockStack gap="tight"> <Text fontWeight="bold">Active Period</Text> <Text>Starts: {new Date(discount.startsAt).toLocaleDateString()}</Text> <Text>Ends: {discount.endsAt ? new Date(discount.endsAt).toLocaleDateString() : 'No end date'}</Text> </BlockStack> </Box> </BlockStack> )} </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Divider, Text, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.discount-details.action.render', async (root, api) => { const content = root.createFragment(); const secondaryAction = root.createFragment(); secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Close') ); const loadingBanner = root.createComponent( Banner, { tone: 'info' }, 'Loading analytics...' ); content.appendChild(loadingBanner); const adminAction = root.createComponent( AdminAction, { title: 'Discount Analytics', secondaryAction } ); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); const discountId = api.data.selected[0].id; try { const result = await api.query( `query GetDiscountAnalytics($id: ID!) { discountNode(id: $id) { id discount { ... on DiscountCodeBasic { title status usageLimit asyncUsageCount startsAt endsAt } ... on DiscountCodeFreeShipping { title status usageLimit asyncUsageCount startsAt endsAt } ... on DiscountCodeBxgy { title status usageLimit asyncUsageCount startsAt endsAt } } } }`, { variables: { id: discountId } } ); content.replaceChildren(); if (!result.data?.discountNode) { content.appendChild( root.createComponent(Banner, { tone: 'critical' }, 'Could not load discount data') ); return; } const discount = result.data.discountNode.discount; const usagePercent = discount.usageLimit ? Math.round((discount.asyncUsageCount / discount.usageLimit) * 100) : null; const mainStack = root.createComponent(BlockStack, { gap: 'base' }); // Title section const titleBox = root.createComponent(Box, { padding: 'base' }); const titleStack = root.createComponent(BlockStack, { gap: 'tight' }); titleStack.appendChild(root.createComponent(Text, { fontWeight: 'bold' }, discount.title)); titleStack.appendChild(root.createComponent(Text, {}, `Status: ${discount.status}`)); titleBox.appendChild(titleStack); mainStack.appendChild(titleBox); mainStack.appendChild(root.createComponent(Divider, {})); // Usage section const usageBox = root.createComponent(Box, { padding: 'base' }); const usageStack = root.createComponent(BlockStack, { gap: 'tight' }); usageStack.appendChild(root.createComponent(Text, { fontWeight: 'bold' }, 'Usage Statistics')); usageStack.appendChild(root.createComponent(Text, {}, `Total Redemptions: ${discount.asyncUsageCount || 0}`)); if (discount.usageLimit) { usageStack.appendChild(root.createComponent(Text, {}, `Usage Limit: ${discount.usageLimit} (${usagePercent}% used)`)); } else { usageStack.appendChild(root.createComponent(Text, {}, 'Usage Limit: Unlimited')); } usageBox.appendChild(usageStack); mainStack.appendChild(usageBox); mainStack.appendChild(root.createComponent(Divider, {})); // Period section const periodBox = root.createComponent(Box, { padding: 'base' }); const periodStack = root.createComponent(BlockStack, { gap: 'tight' }); periodStack.appendChild(root.createComponent(Text, { fontWeight: 'bold' }, 'Active Period')); periodStack.appendChild(root.createComponent(Text, {}, `Starts: ${new Date(discount.startsAt).toLocaleDateString()}`)); periodStack.appendChild(root.createComponent(Text, {}, `Ends: ${discount.endsAt ? new Date(discount.endsAt).toLocaleDateString() : 'No end date'}`)); periodBox.appendChild(periodStack); mainStack.appendChild(periodBox); content.appendChild(mainStack); } catch (err) { content.replaceChildren(); content.appendChild( root.createComponent(Banner, { tone: 'critical' }, 'Failed to fetch analytics') ); } } );
Anchor to Discount details action (should render) ,[object Object]Discount details action (should render) target
admin.discount-details.action.should-render
Controls the render state of an admin action extension on the discounts details page. Use this target to conditionally show or hide your action extension based on the discount's properties, such as status, configuration, or specific business requirements.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Conditionally display an analytics action only for discounts that have been redeemed at least once. This example demonstrates checking redemption count before showing the extension.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.discount-details.action.should-render', async ({data}) => { const discountId = data.selected[0].id; try { // Check with app backend if discount has performance data const response = await fetch('https://your-app.com/api/discount-analytics/check', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ discountId, }), }); if (!response.ok) { return {display: false}; } const analyticsData = await response.json(); // Only show performance action if discount has been redeemed // and has meaningful data to display const hasRedemptions = analyticsData.totalRedemptions > 0; const hasRevenueData = analyticsData.revenueImpact !== null; const isTrackingEnabled = analyticsData.trackingEnabled === true; return { display: hasRedemptions && hasRevenueData && isTrackingEnabled, }; } catch (err) { // Don't show action if we can't verify analytics availability return {display: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.discount-details.action.should-render', async ({data}) => { const discountId = data.selected[0].id; try { // Check with app backend if discount has performance data const response = await fetch('https://your-app.com/api/discount-analytics/check', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ discountId, }), }); if (!response.ok) { return {display: false}; } const analyticsData = await response.json(); // Only show performance action if discount has been redeemed // and has meaningful data to display const hasRedemptions = analyticsData.totalRedemptions > 0; const hasRevenueData = analyticsData.revenueImpact !== null; const isTrackingEnabled = analyticsData.trackingEnabled === true; return { display: hasRedemptions && hasRevenueData && isTrackingEnabled, }; } catch (err) { // Don't show action if we can't verify analytics availability return {display: false}; } } );Description
Add a should-render extension that checks if a discount applies to specific products rather than all products, enabling a 'View Eligible Products' action only when relevant.
React
import {extension} from '@shopify/ui-extensions/admin'; const TARGET = 'admin.discount-details.action.should-render'; export default extension(TARGET, async ({data, query}) => { const discountId = data.selected[0].id; try { const {data: responseData, errors} = await query(` query GetDiscountDetails($id: ID!) { discountNode(id: $id) { discount { ... on DiscountCodeBasic { title customerGets { items { ... on DiscountProducts { productVariantsCount { count } products(first: 1) { nodes { id } } } ... on AllDiscountItems { allItems } } } } ... on DiscountCodeBxgy { title customerGets { items { ... on DiscountProducts { products(first: 1) { nodes { id } } } } } } } } } `, {variables: {id: discountId}}); if (errors?.length) { return {display: false}; } const discount = responseData?.discountNode?.discount; if (!discount) { return {display: false}; } const customerGets = discount.customerGets; if (!customerGets?.items) { return {display: false}; } // Check if discount applies to specific products (not all items) const items = customerGets.items; const hasSpecificProducts = items.products?.nodes?.length > 0; const appliesToAllItems = items.allItems === true; // Only show action if discount targets specific products return {display: hasSpecificProducts && !appliesToAllItems}; } catch (err) { return {display: false}; } });TS
import {extension} from '@shopify/ui-extensions/admin'; const TARGET = 'admin.discount-details.action.should-render'; export default extension(TARGET, async ({data, query}) => { const discountId = data.selected[0].id; try { const {data: responseData, errors} = await query(` query GetDiscountDetails($id: ID!) { discountNode(id: $id) { discount { ... on DiscountCodeBasic { title customerGets { items { ... on DiscountProducts { productVariantsCount { count } products(first: 1) { nodes { id } } } ... on AllDiscountItems { allItems } } } } ... on DiscountCodeBxgy { title customerGets { items { ... on DiscountProducts { products(first: 1) { nodes { id } } } } } } } } } `, {variables: {id: discountId}}); if (errors?.length) { return {display: false}; } const discount = responseData?.discountNode?.discount; if (!discount) { return {display: false}; } const customerGets = discount.customerGets; if (!customerGets?.items) { return {display: false}; } // Check if discount applies to specific products (not all items) const items = customerGets.items; const hasSpecificProducts = items.products?.nodes?.length > 0; const appliesToAllItems = items.allItems === true; // Only show action if discount targets specific products return {display: hasSpecificProducts && !appliesToAllItems}; } catch (err) { return {display: false}; } });
Anchor to Discount index targetsDiscount index targets
Use action targets to extend the discount index page with bulk operations and workflows that help merchants manage multiple discounts efficiently.
Anchor to Discount index action ,[object Object]Discount index action target
admin.discount-index.action.render
Renders an admin action extension on the discounts index page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on discounts data, such as syncing with external systems, exporting discounts information, or managing credit terms.
Extensions at this target can access discount data through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Supported components
- Admin
Action - Admin
Block - Admin
Print Action - Badge
- Banner
- Block
Stack - Box
- Button
- Checkbox
- Choice
List - Color
Picker - Date
Field - Date
Picker - Divider
- Email
Field - Form
- Function
Settings - Heading
- Heading
Group - Icon
- Image
- Inline
Stack - Link
- Money
Field - Number
Field - Paragraph
- Password
Field - Pressable
- Progress
Indicator - Section
- Select
- Text
- Text
Area - Text
Field - URLField
Available APIs
Examples
Description
Add an action extension that syncs selected discounts to an external POS or marketing system. This example demonstrates batch syncing with configurable options for location targeting and overwrite behavior.
React
import React, { useState } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.discount-index.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close } = useApi(TARGET); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState<string | null>(null); const [selectedLocations, setSelectedLocations] = useState<string[]>(['all']); const [overwriteExisting, setOverwriteExisting] = useState(true); const selectedDiscounts = data.selected.map(item => item.id); const handleSync = async () => { setLoading(true); setError(null); try { const response = await fetch('https://your-app.com/api/pos/sync-discounts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ discountIds: selectedDiscounts, locations: selectedLocations, overwriteExisting, }), }); if (response.ok) { setSuccess(true); close(); } else { const result = await response.json(); setError(result.message || 'Failed to sync discounts to POS'); } } catch (err) { setError('Connection error. Please try again.'); } finally { setLoading(false); } }; return ( <AdminAction title="Sync Discounts to POS" primaryAction={ <Button onPress={handleSync} disabled={loading || success}> {loading ? 'Syncing...' : `Sync ${selectedDiscounts.length} Discount(s)`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && ( <Banner tone="success"> Successfully synced {selectedDiscounts.length} discount(s) to POS! </Banner> )} {error && <Banner tone="critical">{error}</Banner>} <Box padding="base"> <BlockStack gap="base"> <ChoiceList name="locations" title="POS Locations" choices={[ { id: 'all', label: 'All locations' }, { id: 'store-1', label: 'Main Store' }, { id: 'store-2', label: 'Downtown Location' }, { id: 'store-3', label: 'Mall Kiosk' }, ]} value={selectedLocations} onChange={setSelectedLocations} /> <Divider /> <Checkbox checked={overwriteExisting} onChange={setOverwriteExisting} > Overwrite existing discounts in POS </Checkbox> </BlockStack> </Box> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Box, Button, Checkbox, ChoiceList, Divider, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.discount-index.action.render', (root, api) => { let loading = false; let success = false; let error: string | null = null; let selectedLocations: string[] = ['all']; let overwriteExisting = true; const selectedDiscounts = api.data.selected.map(item => item.id); const handleSync = async () => { loading = true; error = null; updateUI(); try { const response = await fetch('https://your-app.com/api/pos/sync-discounts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ discountIds: selectedDiscounts, locations: selectedLocations, overwriteExisting, }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { const result = await response.json(); error = result.message || 'Failed to sync discounts to POS'; } } catch (err) { error = 'Connection error. Please try again.'; } finally { loading = false; updateUI(); } }; const content = root.createFragment(); const updateUI = () => { content.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, `Successfully synced ${selectedDiscounts.length} discount(s) to POS!` ) ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } const box = root.createComponent(Box, { padding: 'base' }); const innerStack = root.createComponent(BlockStack, { gap: 'base' }); innerStack.appendChild( root.createComponent(ChoiceList, { name: 'locations', title: 'POS Locations', choices: [ { id: 'all', label: 'All locations' }, { id: 'store-1', label: 'Main Store' }, { id: 'store-2', label: 'Downtown Location' }, { id: 'store-3', label: 'Mall Kiosk' }, ], value: selectedLocations, onChange: (val: string[]) => { selectedLocations = val; updateUI(); }, }) ); innerStack.appendChild(root.createComponent(Divider, {})); innerStack.appendChild( root.createComponent(Checkbox, { checked: overwriteExisting, onChange: (val: boolean) => { overwriteExisting = val; }, }, 'Overwrite existing discounts in POS') ); box.appendChild(innerStack); stack.appendChild(box); content.appendChild(stack); }; const primaryAction = root.createFragment(); primaryAction.appendChild( root.createComponent(Button, { onPress: handleSync, disabled: loading || success, }, loading ? 'Syncing...' : `Sync ${selectedDiscounts.length} Discount(s)`) ); const secondaryAction = root.createFragment(); secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Sync Discounts to POS', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );Description
Add an action extension that lets merchants tag selected discounts with a campaign name for organization. This example demonstrates working with selected items from the index page and sending batch updates to your app backend using the [direct API](/docs/api/admin-extensions/2025-07#direct-api-access).
React
import React, { useState } from 'react'; import { reactExtension, useApi, AdminAction, Banner, BlockStack, Button, TextField, Text, } from '@shopify/ui-extensions-react/admin'; const TARGET = 'admin.discount-index.action.render'; export default reactExtension(TARGET, () => <App />); function App() { const { data, close } = useApi(TARGET); const [campaignTag, setCampaignTag] = useState(''); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState<string | null>(null); const selectedCount = data.selected?.length || 0; const discountIds = data.selected?.map((item) => item.id) || []; const handleTagDiscounts = async () => { if (!campaignTag.trim()) { setError('Please enter a campaign tag'); return; } setLoading(true); setError(null); try { const response = await fetch('https://your-app.com/api/discounts/tag', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ discountIds, tag: campaignTag.trim(), }), }); if (response.ok) { setSuccess(true); close(); } else { const result = await response.json(); setError(result.message || 'Failed to tag discounts'); } } catch (err) { setError('Network error. Please try again.'); } finally { setLoading(false); } }; return ( <AdminAction title="Tag Discounts by Campaign" primaryAction={ <Button onPress={handleTagDiscounts} disabled={loading || success || !campaignTag.trim()}> {loading ? 'Tagging...' : `Tag ${selectedCount} Discount${selectedCount !== 1 ? 's' : ''}`} </Button> } secondaryAction={<Button onPress={close}>Cancel</Button>} > <BlockStack gap="base"> {success && ( <Banner tone="success"> Successfully tagged {selectedCount} discount{selectedCount !== 1 ? 's' : ''} with "{campaignTag}" </Banner> )} {error && <Banner tone="critical">{error}</Banner>} <Text> Add a campaign tag to {selectedCount} selected discount{selectedCount !== 1 ? 's' : ''} for easier organization and filtering. </Text> <TextField label="Campaign tag" value={campaignTag} onChange={setCampaignTag} placeholder="e.g., Summer Sale 2025, Black Friday" /> </BlockStack> </AdminAction> ); }TS
import { extension, AdminAction, Banner, BlockStack, Button, TextField, Text, } from '@shopify/ui-extensions/admin'; export default extension( 'admin.discount-index.action.render', (root, api) => { let campaignTag = ''; let loading = false; let success = false; let error: string | null = null; const selectedCount = api.data.selected?.length || 0; const discountIds = api.data.selected?.map((item) => item.id) || []; const content = root.createFragment(); const primaryAction = root.createFragment(); const secondaryAction = root.createFragment(); const handleTagDiscounts = async () => { if (!campaignTag.trim()) { error = 'Please enter a campaign tag'; updateUI(); return; } loading = true; error = null; updateUI(); try { const response = await fetch('https://your-app.com/api/discounts/tag', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ discountIds, tag: campaignTag.trim(), }), }); if (response.ok) { success = true; updateUI(); api.close(); } else { const result = await response.json(); error = result.message || 'Failed to tag discounts'; updateUI(); } } catch (err) { error = 'Network error. Please try again.'; updateUI(); } finally { loading = false; updateUI(); } }; const updateUI = () => { content.replaceChildren(); primaryAction.replaceChildren(); const stack = root.createComponent(BlockStack, { gap: 'base' }); if (success) { stack.appendChild( root.createComponent(Banner, { tone: 'success' }, `Successfully tagged ${selectedCount} discount${selectedCount !== 1 ? 's' : ''} with "${campaignTag}"`) ); } if (error) { stack.appendChild(root.createComponent(Banner, { tone: 'critical' }, error)); } stack.appendChild( root.createComponent(Text, {}, `Add a campaign tag to ${selectedCount} selected discount${selectedCount !== 1 ? 's' : ''} for easier organization and filtering.`) ); stack.appendChild( root.createComponent(TextField, { label: 'Campaign tag', value: campaignTag, onChange: (val) => { campaignTag = val; }, placeholder: 'e.g., Summer Sale 2025, Black Friday', }) ); content.appendChild(stack); primaryAction.appendChild( root.createComponent(Button, { onPress: handleTagDiscounts, disabled: loading || success || !campaignTag.trim(), }, loading ? 'Tagging...' : `Tag ${selectedCount} Discount${selectedCount !== 1 ? 's' : ''}`) ); }; secondaryAction.appendChild( root.createComponent(Button, { onPress: () => api.close() }, 'Cancel') ); updateUI(); const adminAction = root.createComponent(AdminAction, { title: 'Tag Discounts by Campaign', primaryAction, secondaryAction, }); adminAction.appendChild(content); root.appendChild(adminAction); root.mount(); } );
Anchor to Discount index action (should render) ,[object Object]Discount index action (should render) target
admin.discount-index.action.should-render
Controls the render state of an admin action extension on the discounts index page. Use this target to conditionally show or hide your action extension based on the discount's properties, such as status, configuration, or specific business requirements.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
Examples
Description
Conditionally display a bulk action only when the merchant has configured their external system integration. This example demonstrates checking app configuration before showing the extension.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.discount-index.action.should-render', async ({data}) => { const selectedIds = data.selected.map((item) => item.id); if (selectedIds.length === 0) { return {display: false}; } try { // Check with app backend if discounts have performance data const response = await fetch('https://your-app.com/api/discounts/has-analytics', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({discountIds: selectedIds}), }); if (!response.ok) { return {display: false}; } const result = await response.json(); // Show action only if at least one discount has redemption data // Backend returns: { hasAnalytics: boolean, minRedemptions: number } return { display: result.hasAnalytics && result.minRedemptions >= 5, }; } catch (err) { console.error('Failed to check discount analytics availability:', err); return {display: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.discount-index.action.should-render', async ({data}) => { const selectedIds = data.selected.map((item) => item.id); if (selectedIds.length === 0) { return {display: false}; } try { // Check with app backend if discounts have performance data const response = await fetch('https://your-app.com/api/discounts/has-analytics', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({discountIds: selectedIds}), }); if (!response.ok) { return {display: false}; } const result = await response.json(); // Show action only if at least one discount has redemption data // Backend returns: { hasAnalytics: boolean, minRedemptions: number } return { display: result.hasAnalytics && result.minRedemptions >= 5, }; } catch (err) { console.error('Failed to check discount analytics availability:', err); return {display: false}; } } );Description
Conditionally display a bulk action only when the selected discount is currently active. This example demonstrates checking discount status using the GraphQL Admin API before showing the extension.
React
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.discount-index.action.should-render', async ({data, query}) => { const discountId = data.selected[0].id; try { const {data: responseData, errors} = await query( `query GetDiscountStatus($id: ID!) { discountNode(id: $id) { discount { ... on DiscountCodeBasic { status startsAt endsAt } ... on DiscountCodeFreeShipping { status startsAt endsAt } ... on DiscountAutomaticBasic { status startsAt endsAt } } } }`, {variables: {id: discountId}} ); if (errors || !responseData?.discountNode) { return {display: false}; } const discount = responseData.discountNode.discount; // Only show action for active discounts const isActive = discount?.status === 'ACTIVE'; return {display: isActive}; } catch (err) { console.error('Failed to check discount status:', err); return {display: false}; } } );TS
import {extension} from '@shopify/ui-extensions/admin'; export default extension( 'admin.discount-index.action.should-render', async ({data, query}) => { const discountId = data.selected[0].id; try { const {data: responseData, errors} = await query( `query GetDiscountStatus($id: ID!) { discountNode(id: $id) { discount { ... on DiscountCodeBasic { status startsAt endsAt } ... on DiscountCodeFreeShipping { status startsAt endsAt } ... on DiscountAutomaticBasic { status startsAt endsAt } } } }`, {variables: {id: discountId}} ); if (errors || !responseData?.discountNode) { return {display: false}; } const discount = responseData.discountNode.discount; // Only show action for active discounts const isActive = discount?.status === 'ACTIVE'; return {display: isActive}; } catch (err) { console.error('Failed to check discount status:', err); return {display: false}; } } );
Anchor to Best practicesBest practices
- Differentiate discount types: Discounts come in multiple types (basic, BXGY, free shipping, automatic vs code-based). Before displaying discount actions, check the discount type using GraphQL to ensure your extension supports it. For example, POS sync may only work with certain discount types.
- Validate discount dates: Always check
startsAtandendsAtwhen displaying or syncing discounts. Syncing expired or not-yet-active discounts to external systems can create customer confusion and needs special handling or filtering. - Provide clear feedback: Discounts workflows often involve external systems. Always provide clear success and error messages, and help merchants understand what happened and what to do next if something goes wrong.
- Handle discount combinations: Shopify has complex discount combination rules. If your extension recommends or creates discounts, validate that they're compatible with existing discount configurations to avoid conflicts that prevent customers from completing checkouts.
- Account for attribution delays: Discount usage counts (
asyncUsageCount) update asynchronously and may lag by several minutes. When displaying real-time analytics, indicate that counts are approximate and mention the last update time if available from your system.
Anchor to LimitationsLimitations
- Single target per module: Each
[[extensions.targeting]]entry in your TOML configuration maps one target to one module file. - Multiple discount type schemas: Discounts are a GraphQL union type with eight possible types:
DiscountAutomaticBasic,DiscountCodeBasic,DiscountAutomaticBxgy,DiscountCodeBxgy,DiscountAutomaticFreeShipping,DiscountCodeFreeShipping,DiscountCodeApp,DiscountAutomaticApp. Each type has different fields. - Asynchronous usage count: The
asyncUsageCountfield on GraphQLDiscountunion types is updated asynchronously and might show a lower count until the process is completed.