diff --git a/admin-action/package.json.liquid b/admin-action/package.json.liquid
index 92df9cd3..6d3ea4ea 100644
--- a/admin-action/package.json.liquid
+++ b/admin-action/package.json.liquid
@@ -11,8 +11,8 @@
{%- elsif flavor contains "react" %}
"dependencies": {
"react": "^18.0.0",
- "@shopify/ui-extensions": "2025.4.x",
- "@shopify/ui-extensions-react": "2025.4.x",
+ "@shopify/ui-extensions": "2025.7.x",
+ "@shopify/ui-extensions-react": "2025.7.x",
"react-reconciler": "0.29.0"
},
"devDependencies": {
@@ -20,7 +20,7 @@
}
{%- else %}
"dependencies": {
- "@shopify/ui-extensions": "2025.4.x"
+ "@shopify/ui-extensions": "2025.7.x"
}
{%- endif %}
}
\ No newline at end of file
diff --git a/admin-action/shopify.extension.toml.liquid b/admin-action/shopify.extension.toml.liquid
index 2d62900e..910ff787 100644
--- a/admin-action/shopify.extension.toml.liquid
+++ b/admin-action/shopify.extension.toml.liquid
@@ -1,7 +1,7 @@
{%- if flavor contains "preact" -%}
api_version = "2025-10"
{% else %}
-api_version = "2025-04"
+api_version = "2025-07"
{% endif -%}
[[extensions]]
diff --git a/admin-block/package.json.liquid b/admin-block/package.json.liquid
index 92df9cd3..6d3ea4ea 100644
--- a/admin-block/package.json.liquid
+++ b/admin-block/package.json.liquid
@@ -11,8 +11,8 @@
{%- elsif flavor contains "react" %}
"dependencies": {
"react": "^18.0.0",
- "@shopify/ui-extensions": "2025.4.x",
- "@shopify/ui-extensions-react": "2025.4.x",
+ "@shopify/ui-extensions": "2025.7.x",
+ "@shopify/ui-extensions-react": "2025.7.x",
"react-reconciler": "0.29.0"
},
"devDependencies": {
@@ -20,7 +20,7 @@
}
{%- else %}
"dependencies": {
- "@shopify/ui-extensions": "2025.4.x"
+ "@shopify/ui-extensions": "2025.7.x"
}
{%- endif %}
}
\ No newline at end of file
diff --git a/admin-block/shopify.extension.toml.liquid b/admin-block/shopify.extension.toml.liquid
index 99008e07..52269ef8 100644
--- a/admin-block/shopify.extension.toml.liquid
+++ b/admin-block/shopify.extension.toml.liquid
@@ -1,7 +1,7 @@
{%- if flavor contains "preact" -%}
api_version = "2025-10"
{% else %}
-api_version = "2025-04"
+api_version = "2025-07"
{% endif -%}
[[extensions]]
diff --git a/admin-print-action/package.json.liquid b/admin-print-action/package.json.liquid
index 92df9cd3..6d3ea4ea 100644
--- a/admin-print-action/package.json.liquid
+++ b/admin-print-action/package.json.liquid
@@ -11,8 +11,8 @@
{%- elsif flavor contains "react" %}
"dependencies": {
"react": "^18.0.0",
- "@shopify/ui-extensions": "2025.4.x",
- "@shopify/ui-extensions-react": "2025.4.x",
+ "@shopify/ui-extensions": "2025.7.x",
+ "@shopify/ui-extensions-react": "2025.7.x",
"react-reconciler": "0.29.0"
},
"devDependencies": {
@@ -20,7 +20,7 @@
}
{%- else %}
"dependencies": {
- "@shopify/ui-extensions": "2025.4.x"
+ "@shopify/ui-extensions": "2025.7.x"
}
{%- endif %}
}
\ No newline at end of file
diff --git a/admin-print-action/shopify.extension.toml.liquid b/admin-print-action/shopify.extension.toml.liquid
index 98e23cbd..24781021 100644
--- a/admin-print-action/shopify.extension.toml.liquid
+++ b/admin-print-action/shopify.extension.toml.liquid
@@ -1,7 +1,7 @@
{%- if flavor contains "preact" -%}
api_version = "2025-10"
{% else %}
-api_version = "2025-04"
+api_version = "2025-07"
{% endif -%}
[[extensions]]
diff --git a/admin-purchase-options-action/package.json.liquid b/admin-purchase-options-action/package.json.liquid
index 92df9cd3..6d3ea4ea 100644
--- a/admin-purchase-options-action/package.json.liquid
+++ b/admin-purchase-options-action/package.json.liquid
@@ -11,8 +11,8 @@
{%- elsif flavor contains "react" %}
"dependencies": {
"react": "^18.0.0",
- "@shopify/ui-extensions": "2025.4.x",
- "@shopify/ui-extensions-react": "2025.4.x",
+ "@shopify/ui-extensions": "2025.7.x",
+ "@shopify/ui-extensions-react": "2025.7.x",
"react-reconciler": "0.29.0"
},
"devDependencies": {
@@ -20,7 +20,7 @@
}
{%- else %}
"dependencies": {
- "@shopify/ui-extensions": "2025.4.x"
+ "@shopify/ui-extensions": "2025.7.x"
}
{%- endif %}
}
\ No newline at end of file
diff --git a/admin-purchase-options-action/shopify.extension.toml.liquid b/admin-purchase-options-action/shopify.extension.toml.liquid
index 1295ab2e..ee780c57 100644
--- a/admin-purchase-options-action/shopify.extension.toml.liquid
+++ b/admin-purchase-options-action/shopify.extension.toml.liquid
@@ -1,7 +1,7 @@
{%- if flavor contains "preact" -%}
api_version = "2025-10"
{% else %}
-api_version = "2025-04"
+api_version = "2025-07"
{% endif %}
[[extensions]]
diff --git a/customer-segment-template-extension/package.json.liquid b/customer-segment-template-extension/package.json.liquid
index a088f86e..6d3ea4ea 100644
--- a/customer-segment-template-extension/package.json.liquid
+++ b/customer-segment-template-extension/package.json.liquid
@@ -1,27 +1,26 @@
-{%- if flavor contains "react" -%}
{
"name": "{{ handle }}",
"private": true,
"version": "1.0.0",
"license": "UNLICENSED",
+{%- if flavor contains "preact" %}
+ "dependencies": {
+ "preact": "^10.10.x",
+ "@shopify/ui-extensions": "~2025.10.0-rc"
+ }
+{%- elsif flavor contains "react" %}
"dependencies": {
"react": "^18.0.0",
- "@shopify/ui-extensions": "2024.7.x",
- "@shopify/ui-extensions-react": "2024.7.x",
+ "@shopify/ui-extensions": "2025.7.x",
+ "@shopify/ui-extensions-react": "2025.7.x",
"react-reconciler": "0.29.0"
},
"devDependencies": {
"@types/react": "^18.0.0"
}
-}
-{%- else -%}
-{
- "name": "{{ handle }}",
- "private": true,
- "version": "1.0.0",
- "license": "UNLICENSED",
+{%- else %}
"dependencies": {
- "@shopify/ui-extensions": "2024.7.x"
+ "@shopify/ui-extensions": "2025.7.x"
}
-}
-{%- endif -%}
+{%- endif %}
+}
\ No newline at end of file
diff --git a/customer-segment-template-extension/shopify.extension.toml.liquid b/customer-segment-template-extension/shopify.extension.toml.liquid
index 299954a9..c0f44805 100644
--- a/customer-segment-template-extension/shopify.extension.toml.liquid
+++ b/customer-segment-template-extension/shopify.extension.toml.liquid
@@ -1,4 +1,8 @@
-api_version = "2024-07"
+{%- if flavor contains "preact" -%}
+api_version = "2025-10"
+{% else %}
+api_version = "2025-07"
+{% endif -%}
[[extensions]]
# Change the merchant-facing name of the extension in locales/en.default.json
@@ -15,4 +19,4 @@ target = "admin.customers.segmentation-templates.render"
# Valid extension points:
-# - admin.customers.segmentation-templates.render
+# - admin.customers.segmentation-templates.render
\ No newline at end of file
diff --git a/customer-segment-template-extension/src/CustomerSegmentTemplate.liquid b/customer-segment-template-extension/src/CustomerSegmentTemplate.liquid
index c9239662..399855a0 100644
--- a/customer-segment-template-extension/src/CustomerSegmentTemplate.liquid
+++ b/customer-segment-template-extension/src/CustomerSegmentTemplate.liquid
@@ -1,4 +1,28 @@
-{%- if flavor contains "react" -%}
+{%- if flavor contains "preact" -%}
+import {render} from 'preact';
+
+const TARGET = 'admin.customers.segmentation-templates.render';
+
+export default function() {
+ render(, document.body);
+}
+
+function App() {
+ const query = 'number_of_orders = 1 AND products_purchased(id: (product_id)) = true';
+ const queryToInsert = 'number_of_orders = 1 AND products_purchased(id: (';
+
+ return (
+
+ );
+}
+
+{%- elsif flavor contains "react" -%}
import {
reactExtension,
CustomerSegmentTemplate,
@@ -49,4 +73,4 @@ export default extension("admin.customers.segmentation-templates.render", (root,
)
);
});
-{%- endif -%}
+{%- endif -%}
\ No newline at end of file
diff --git a/discount-details-function-settings-block/package.json.liquid b/discount-details-function-settings-block/package.json.liquid
index 8beecd7f..6d3ea4ea 100644
--- a/discount-details-function-settings-block/package.json.liquid
+++ b/discount-details-function-settings-block/package.json.liquid
@@ -1,28 +1,26 @@
-{%- if flavor contains "react" -%}
{
"name": "{{ handle }}",
"private": true,
"version": "1.0.0",
"license": "UNLICENSED",
+{%- if flavor contains "preact" %}
+ "dependencies": {
+ "preact": "^10.10.x",
+ "@shopify/ui-extensions": "~2025.10.0-rc"
+ }
+{%- elsif flavor contains "react" %}
"dependencies": {
"react": "^18.0.0",
- "@shopify/ui-extensions": "2024.10.x",
- "@shopify/ui-extensions-react": "2024.10.x",
+ "@shopify/ui-extensions": "2025.7.x",
+ "@shopify/ui-extensions-react": "2025.7.x",
"react-reconciler": "0.29.0"
},
"devDependencies": {
"@types/react": "^18.0.0"
}
-}
-{%- else -%}
-{
- "name": "{{ handle }}",
- "private": true,
- "version": "1.0.0",
- "license": "UNLICENSED",
+{%- else %}
"dependencies": {
- "@shopify/ui-extensions": "2024.10.x"
+ "@shopify/ui-extensions": "2025.7.x"
}
-}
-{%- endif -%}
-
+{%- endif %}
+}
\ No newline at end of file
diff --git a/discount-details-function-settings-block/shopify.extension.toml.liquid b/discount-details-function-settings-block/shopify.extension.toml.liquid
index c1badf6e..a1ae82b7 100644
--- a/discount-details-function-settings-block/shopify.extension.toml.liquid
+++ b/discount-details-function-settings-block/shopify.extension.toml.liquid
@@ -1,4 +1,8 @@
-api_version = "2024-10"
+{%- if flavor contains "preact" -%}
+api_version = "2025-10"
+{% else %}
+api_version = "2025-07"
+{% endif -%}
[[extensions]]
# Change the merchant-facing name of the extension in locales/en.default.json
@@ -12,4 +16,4 @@ type = "ui_extension"
[[extensions.targeting]]
module = "./src/DiscountFunctionSettings.{{ srcFileExtension }}"
# The target used here must match the target used in the module file (./src/DiscountFunctionSettings.{{ srcFileExtension }})
-target = "admin.discount-details.function-settings.render"
+target = "admin.discount-details.function-settings.render"
\ No newline at end of file
diff --git a/discount-details-function-settings-block/src/DiscountFunctionSettings.liquid b/discount-details-function-settings-block/src/DiscountFunctionSettings.liquid
index 01efc3a1..86c9ae65 100644
--- a/discount-details-function-settings-block/src/DiscountFunctionSettings.liquid
+++ b/discount-details-function-settings-block/src/DiscountFunctionSettings.liquid
@@ -1,4 +1,262 @@
-{% if flavor contains 'react' %}
+{%- if flavor contains "preact" -%}
+import {render} from 'preact';
+import {useState, useEffect} from 'preact/hooks';
+
+const TARGET = "admin.discount-details.function-settings.render";
+
+export default function() {
+ render(, document.body);
+}
+
+function App() {
+ const [loading, setLoading] = useState(true);
+ const [percentages, setPercentages] = useState({ product: 0, order: 0, shipping: 0 });
+ const [collections, setCollections] = useState([]);
+ const [appliesTo, setAppliesTo] = useState("all");
+ const [initialized, setInitialized] = useState(false);
+
+ useEffect(() => {
+ const initialize = async () => {
+ const existingDefinition = await getMetafieldDefinition();
+ if (!existingDefinition) {
+ await createMetafieldDefinition();
+ }
+
+ const metafieldConfig = parseMetafield(
+ shopify.data?.metafields?.find(m => m.key === "function-configuration")?.value
+ );
+
+ setPercentages(metafieldConfig.percentages);
+
+ if (metafieldConfig.collectionIds.length > 0) {
+ const fetchedCollections = await getCollections(metafieldConfig.collectionIds);
+ setCollections(fetchedCollections);
+ setAppliesTo("collections");
+ }
+
+ setLoading(false);
+ setInitialized(true);
+ };
+
+ initialize();
+ }, []);
+
+ const onPercentageChange = (type, value) => {
+ setPercentages(prev => ({
+ ...prev,
+ [type]: Number(value)
+ }));
+ };
+
+ const onAppliesToChange = (value) => {
+ setAppliesTo(value);
+ if (value === "all") {
+ setCollections([]);
+ }
+ };
+
+ const onAddCollections = async () => {
+ const selection = await shopify.resourcePicker({
+ type: "collection",
+ selectionIds: collections.map(c => ({ id: c.id })),
+ action: "select",
+ filter: {
+ archived: true,
+ variants: true,
+ },
+ });
+ if (selection) {
+ setCollections(selection);
+ }
+ };
+
+ const removeCollection = (id) => {
+ setCollections(prev => prev.filter(c => c.id !== id));
+ };
+
+ const saveSettings = async () => {
+ await shopify.applyMetafieldChange({
+ type: "updateMetafield",
+ namespace: "$app:example-discounts--ui-extension",
+ key: "function-configuration",
+ value: JSON.stringify({
+ cartLinePercentage: percentages.product,
+ orderPercentage: percentages.order,
+ deliveryPercentage: percentages.shipping,
+ collectionIds: collections.map(c => c.id),
+ }),
+ valueType: "json",
+ });
+ };
+
+ if (loading) {
+ return {shopify.i18n.translate("loading") || "Loading..."};
+ }
+
+ return (
+
+ {shopify.i18n.translate("title") || "Discount Configuration"}
+
+
+
+ onPercentageChange("product", value)}
+ suffix="%"
+ />
+
+
+
+
+ {appliesTo === "collections" && (
+
+
+ {shopify.i18n.translate("collections.buttonLabel") || "Add collections"}
+
+
+ )}
+
+
+ {appliesTo === "collections" && collections.length > 0 && (
+
+ {collections.map((collection) => (
+
+
+
+ {collection.title}
+
+ removeCollection(collection.id)}>
+ ✕
+
+
+
+
+ ))}
+
+ )}
+
+
+ {(appliesTo === "all" || collections.length === 0) && }
+
+ onPercentageChange("order", value)}
+ suffix="%"
+ />
+
+ onPercentageChange("shipping", value)}
+ suffix="%"
+ />
+
+
+
+
+ );
+}
+
+const METAFIELD_NAMESPACE = "$app:example-discounts--ui-extension";
+const METAFIELD_KEY = "function-configuration";
+
+async function getMetafieldDefinition() {
+ const query = `#graphql
+ query GetMetafieldDefinition {
+ metafieldDefinitions(first: 1, ownerType: DISCOUNT, namespace: "${METAFIELD_NAMESPACE}", key: "${METAFIELD_KEY}") {
+ nodes {
+ id
+ }
+ }
+ }
+ `;
+
+ const result = await shopify.query(query);
+ return result?.data?.metafieldDefinitions?.nodes[0];
+}
+
+async function createMetafieldDefinition() {
+ const definition = {
+ access: {
+ admin: "MERCHANT_READ_WRITE",
+ },
+ key: METAFIELD_KEY,
+ name: "Discount Configuration",
+ namespace: METAFIELD_NAMESPACE,
+ ownerType: "DISCOUNT",
+ type: "json",
+ };
+
+ const query = `#graphql
+ mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) {
+ metafieldDefinitionCreate(definition: $definition) {
+ createdDefinition {
+ id
+ }
+ }
+ }
+ `;
+
+ const variables = { definition };
+ await shopify.query(query, { variables });
+}
+
+function parseMetafield(value) {
+ try {
+ const parsed = JSON.parse(value || "{}");
+ return {
+ percentages: {
+ product: Number(parsed.cartLinePercentage ?? 0),
+ order: Number(parsed.orderPercentage ?? 0),
+ shipping: Number(parsed.deliveryPercentage ?? 0),
+ },
+ collectionIds: parsed.collectionIds ?? [],
+ };
+ } catch {
+ return {
+ percentages: { product: 0, order: 0, shipping: 0 },
+ collectionIds: [],
+ };
+ }
+}
+
+async function getCollections(collectionGids) {
+ const query = `#graphql
+ query GetCollections($ids: [ID!]!) {
+ collections: nodes(ids: $ids) {
+ ... on Collection {
+ id
+ title
+ }
+ }
+ }
+ `;
+ const result = await shopify.query(query, {
+ variables: { ids: collectionGids },
+ });
+ return result?.data?.collections ?? [];
+}
+
+{%- elsif flavor contains "react" -%}
import {
reactExtension,
useApi,
@@ -384,7 +642,7 @@
}
-{% else %}
+{%- else -%}
import {
FunctionSettings,
Text,
@@ -783,5 +1041,4 @@ export default extension(TARGET, async (root, api) => {
await api.query(query, { variables });
}
});
-{% endif %}
-
+{%- endif -%}
\ No newline at end of file
diff --git a/order-routing-location-rule/package.json.liquid b/order-routing-location-rule/package.json.liquid
index 92df9cd3..6d3ea4ea 100644
--- a/order-routing-location-rule/package.json.liquid
+++ b/order-routing-location-rule/package.json.liquid
@@ -11,8 +11,8 @@
{%- elsif flavor contains "react" %}
"dependencies": {
"react": "^18.0.0",
- "@shopify/ui-extensions": "2025.4.x",
- "@shopify/ui-extensions-react": "2025.4.x",
+ "@shopify/ui-extensions": "2025.7.x",
+ "@shopify/ui-extensions-react": "2025.7.x",
"react-reconciler": "0.29.0"
},
"devDependencies": {
@@ -20,7 +20,7 @@
}
{%- else %}
"dependencies": {
- "@shopify/ui-extensions": "2025.4.x"
+ "@shopify/ui-extensions": "2025.7.x"
}
{%- endif %}
}
\ No newline at end of file
diff --git a/order-routing-location-rule/shopify.extension.toml.liquid b/order-routing-location-rule/shopify.extension.toml.liquid
index 4aaff920..d7b6c3ad 100644
--- a/order-routing-location-rule/shopify.extension.toml.liquid
+++ b/order-routing-location-rule/shopify.extension.toml.liquid
@@ -1,7 +1,7 @@
{%- if flavor contains "preact" -%}
api_version = "2025-10"
{% else %}
-api_version = "2025-04"
+api_version = "2025-07"
{% endif -%}
[[extensions]]
diff --git a/product-configuration-extension/package.json.liquid b/product-configuration-extension/package.json.liquid
index 1baabd21..c9038039 100644
--- a/product-configuration-extension/package.json.liquid
+++ b/product-configuration-extension/package.json.liquid
@@ -11,8 +11,8 @@
{%- elsif flavor contains "react" -%}
"dependencies": {
"react": "^18.0.0",
- "@shopify/ui-extensions": "2025.4.x",
- "@shopify/ui-extensions-react": "2025.4.x",
+ "@shopify/ui-extensions": "2025.7.x",
+ "@shopify/ui-extensions-react": "2025.7.x",
"react-reconciler": "0.29.0"
},
"devDependencies": {
@@ -20,7 +20,7 @@
}
{%- else -%}
"dependencies": {
- "@shopify/ui-extensions": "2025.4.x"
+ "@shopify/ui-extensions": "2025.7.x"
}
{%- endif -%}
}
\ No newline at end of file
diff --git a/product-configuration-extension/shopify.extension.toml.liquid b/product-configuration-extension/shopify.extension.toml.liquid
index e38886f5..de5e991f 100644
--- a/product-configuration-extension/shopify.extension.toml.liquid
+++ b/product-configuration-extension/shopify.extension.toml.liquid
@@ -1,7 +1,7 @@
{%- if flavor contains "preact" -%}
api_version = "2025-10"
{% else %}
-api_version = "2025-04"
+api_version = "2025-07"
{% endif -%}
[[extensions]]
diff --git a/validation-settings/package.json.liquid b/validation-settings/package.json.liquid
index c3ebba68..6d3ea4ea 100644
--- a/validation-settings/package.json.liquid
+++ b/validation-settings/package.json.liquid
@@ -1,27 +1,26 @@
-{%- if flavor contains "react" -%}
{
"name": "{{ handle }}",
"private": true,
"version": "1.0.0",
"license": "UNLICENSED",
+{%- if flavor contains "preact" %}
+ "dependencies": {
+ "preact": "^10.10.x",
+ "@shopify/ui-extensions": "~2025.10.0-rc"
+ }
+{%- elsif flavor contains "react" %}
"dependencies": {
"react": "^18.0.0",
- "@shopify/ui-extensions": "2024.10.x",
- "@shopify/ui-extensions-react": "2024.10.x",
+ "@shopify/ui-extensions": "2025.7.x",
+ "@shopify/ui-extensions-react": "2025.7.x",
"react-reconciler": "0.29.0"
},
"devDependencies": {
"@types/react": "^18.0.0"
}
-}
-{%- else -%}
-{
- "name": "{{ handle }}",
- "private": true,
- "version": "1.0.0",
- "license": "UNLICENSED",
+{%- else %}
"dependencies": {
- "@shopify/ui-extensions": "2024.10.x"
+ "@shopify/ui-extensions": "2025.7.x"
}
-}
-{%- endif -%}
+{%- endif %}
+}
\ No newline at end of file
diff --git a/validation-settings/shopify.extension.toml.liquid b/validation-settings/shopify.extension.toml.liquid
index 4f87e28e..c2b71909 100644
--- a/validation-settings/shopify.extension.toml.liquid
+++ b/validation-settings/shopify.extension.toml.liquid
@@ -1,4 +1,8 @@
-api_version = "2024-10"
+{%- if flavor contains "preact" -%}
+api_version = "2025-10"
+{% else %}
+api_version = "2025-07"
+{% endif -%}
[[extensions]]
# change the merchant-facing name of the extension in locales/en.default.json
@@ -14,6 +18,4 @@ type = "ui_extension"
[[extensions.targeting]]
module = "./src/ValidationSettings.{{ srcFileExtension }}"
-target = "admin.settings.validation.render"
-
-
+target = "admin.settings.validation.render"
\ No newline at end of file
diff --git a/validation-settings/src/ValidationSettings.liquid b/validation-settings/src/ValidationSettings.liquid
index e3067a53..5e9dacae 100644
--- a/validation-settings/src/ValidationSettings.liquid
+++ b/validation-settings/src/ValidationSettings.liquid
@@ -1,56 +1,44 @@
-{%- if flavor contains 'react' -%}
-import React, { useState } from "react";
-import {
- reactExtension,
- useApi,
- Text,
- Box,
- FunctionSettings,
- Section,
- NumberField,
- BlockStack,
- Banner,
- InlineStack,
- Image,
-} from "@shopify/ui-extensions-react/admin";
+{%- if flavor contains "preact" -%}
+import {render} from 'preact';
+import {useEffect, useState} from 'preact/hooks';
const TARGET = "admin.settings.validation.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");
- }
- }
-
- // Read existing persisted data about product limits from the associated metafield
- const configuration = JSON.parse(
- api.data.validation?.metafields?.[0]?.value ?? "{}",
- );
+export default function() {
+ render(, document.body);
+}
- // Query product data needed to render the settings UI
- const products = await getProducts(api.query);
+function ValidationSettings() {
+ const [errors, setErrors] = useState([]);
+ const [settings, setSettings] = useState({});
+ const [products, setProducts] = useState([]);
+ const [initialized, setInitialized] = useState(false);
+
+ useEffect(() => {
+ const initializeSettings = async () => {
+ const existingDefinition = await getMetafieldDefinition();
+ if (!existingDefinition) {
+ const metafieldDefinition = await createMetafieldDefinition();
+ if (!metafieldDefinition) {
+ throw new Error("Failed to create metafield definition");
+ }
+ }
- return (
-
- );
-});
+ const configuration = JSON.parse(
+ shopify.data.validation?.metafields?.[0]?.value ?? "{}",
+ );
-function ValidationSettings({ configuration, products }) {
- const [errors, setErrors] = useState([]);
- // State to keep track of product limit settings, initialized to any persisted metafield value
- const [settings, setSettings] = useState(
- createSettings(products, configuration),
- );
+ const fetchedProducts = await getProducts();
+ setProducts(fetchedProducts);
+ setSettings(createSettings(fetchedProducts, configuration));
+ setInitialized(true);
+ };
- const { applyMetafieldChange } = useApi(TARGET);
+ initializeSettings();
+ }, []);
const onError = (error) => {
- setErrors(errors.map((e) => e.message));
+ setErrors([error.message]);
};
const onChange = async (variant, value) => {
@@ -61,9 +49,7 @@ function ValidationSettings({ configuration, products }) {
};
setSettings(newSettings);
- // On input change, commit updated product variant limits to memory.
- // Caution: the changes are only persisted on save!
- const result = await applyMetafieldChange({
+ const result = await shopify.applyMetafieldChange({
type: "updateMetafield",
namespace: "$app:product-limits",
key: "product-limits-values",
@@ -75,68 +61,70 @@ function ValidationSettings({ configuration, products }) {
}
};
+ if (!initialized) {
+ return Loading...;
+ }
+
return (
- // Note: FunctionSettings must be rendered for the host to receive metafield updates
-
+
-
+
);
}
function ProductQuantitySettings({ products, settings, onChange }) {
function Header() {
return (
-
-
-
- Variant Name
-
-
- Limit
-
-
+
+
+
+ Variant Name
+
+
+ Limit
+
+
);
}
- // Render table of product variants and inputs to assign limits
return products.map(({ title, variants }) => (
-
-
+
+
{variants.map((variant) => {
const limit = settings[variant.id];
return (
-
-
+
+
{variant.imageUrl ? (
-
+
) : (
- No image
+ No image
)}
-
-
- {variant.title}
-
-
-
+
+ {variant.title}
+
+
+ onChange(variant, value)}
- >
-
-
+ />
+
+
);
})}
-
-
+
+
));
}
@@ -144,17 +132,17 @@ function ErrorBanner({ errors }) {
if (errors.length === 0) return null;
return (
-
+
{errors.map((error, i) => (
-
+
{error}
-
+
))}
-
+
);
}
-async function getProducts(adminApiQuery) {
+async function getProducts() {
const query = `#graphql
query FetchProducts {
products(first: 5) {
@@ -173,7 +161,7 @@ async function getProducts(adminApiQuery) {
}
}`;
- const result = await adminApiQuery(query);
+ const result = await shopify.query(query);
return result?.data?.products.nodes.map(({ title, variants }) => {
return {
@@ -190,7 +178,7 @@ async function getProducts(adminApiQuery) {
const METAFIELD_NAMESPACE = "$app:product-limits";
const METAFIELD_KEY = "product-limits-values";
-async function getMetafieldDefinition(adminApiQuery) {
+async function getMetafieldDefinition() {
const query = `#graphql
query GetMetafieldDefinition {
metafieldDefinitions(first: 1, ownerType: VALIDATION, namespace: "${METAFIELD_NAMESPACE}", key: "${METAFIELD_KEY}") {
@@ -201,12 +189,12 @@ async function getMetafieldDefinition(adminApiQuery) {
}
`;
- const result = await adminApiQuery(query);
+ const result = await shopify.query(query);
return result?.data?.metafieldDefinitions?.nodes[0];
}
-async function createMetafieldDefinition(adminApiQuery) {
+async function createMetafieldDefinition() {
const definition = {
access: {
admin: "MERCHANT_READ_WRITE",
@@ -229,7 +217,7 @@ async function createMetafieldDefinition(adminApiQuery) {
`;
const variables = { definition };
- const result = await adminApiQuery(query, { variables });
+ const result = await shopify.query(query, { variables });
return result?.data?.metafieldDefinitionCreate?.createdDefinition;
}
@@ -239,7 +227,6 @@ function createSettings(products, configuration) {
products.forEach(({ variants }) => {
variants.forEach(({ id }) => {
- // Read existing product limits from metafield
const limit = configuration[id];
if (limit) {
@@ -251,7 +238,7 @@ function createSettings(products, configuration) {
return settings;
}
-{%- elsif flavor contains 'typescript-react' -%}
+{%- elsif flavor contains "react" -%}
import React, { useState } from "react";
import {
reactExtension,
@@ -265,59 +252,44 @@ import {
Banner,
InlineStack,
Image,
- type FunctionSettingsError,
} from "@shopify/ui-extensions-react/admin";
-import { type ValidationSettingsApi } from "@shopify/ui-extensions/admin";
const TARGET = "admin.settings.validation.render";
-export default reactExtension(
- TARGET,
- async (api: ValidationSettingsApi) => {
- 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);
+export default reactExtension(TARGET, async (api) => {
+ const existingDefinition = await getMetafieldDefinition(api.query);
+ if (!existingDefinition) {
+ const metafieldDefinition = await createMetafieldDefinition(api.query);
- if (!metafieldDefinition) {
- throw new Error("Failed to create metafield definition");
- }
+ if (!metafieldDefinition) {
+ throw new Error("Failed to create metafield definition");
}
+ }
- // Read existing persisted data about product limits from the associated metafield
- const configuration = JSON.parse(
- api.data.validation?.metafields?.[0]?.value ?? "{}",
- );
+ const configuration = JSON.parse(
+ api.data.validation?.metafields?.[0]?.value ?? "{}",
+ );
- // Query product data needed to render the settings UI
- const products = await getProducts(api.query);
+ const products = await getProducts(api.query);
- return (
-
- );
- },
-);
-
-function ValidationSettings({
- configuration,
- products,
-}: {
- configuration: Object;
- products: Product[];
-}) {
- const [errors, setErrors] = useState([]);
- // State to keep track of product limit settings, initialized to any persisted metafield value
- const [settings, setSettings] = useState>(
+ return (
+
+ );
+});
+
+function ValidationSettings({ configuration, products }) {
+ const [errors, setErrors] = useState([]);
+ const [settings, setSettings] = useState(
createSettings(products, configuration),
);
const { applyMetafieldChange } = useApi(TARGET);
- const onError = (errors: FunctionSettingsError[]) => {
+ const onError = (error) => {
setErrors(errors.map((e) => e.message));
};
- const onChange = async (variant: ProductVariant, value: number) => {
+ const onChange = async (variant, value) => {
setErrors([]);
const newSettings = {
...settings,
@@ -325,8 +297,6 @@ function ValidationSettings({
};
setSettings(newSettings);
- // On input change, commit updated product variant limits to memory.
- // Caution: the changes are only persisted on save!
const result = await applyMetafieldChange({
type: "updateMetafield",
namespace: "$app:product-limits",
@@ -340,7 +310,6 @@ function ValidationSettings({
};
return (
- // Note: FunctionSettings must be rendered for the host to receive metafield updates
;
- onChange: (variant: ProductVariant, value: number) => Promise;
-}) {
+function ProductQuantitySettings({ products, settings, onChange }) {
function Header() {
return (
@@ -375,7 +336,6 @@ function ProductQuantitySettings({
);
}
- // Render table of product variants and inputs to assign limits
return products.map(({ title, variants }) => (
@@ -402,7 +362,7 @@ function ProductQuantitySettings({
label="Set a limit"
defaultValue={String(limit)}
onChange={(value) => onChange(variant, value)}
- >
+ />
);
@@ -412,7 +372,7 @@ function ProductQuantitySettings({
));
}
-function ErrorBanner({ errors }: { errors: string[] }) {
+function ErrorBanner({ errors }) {
if (errors.length === 0) return null;
return (
@@ -426,20 +386,7 @@ function ErrorBanner({ errors }: { errors: string[] }) {
);
}
-type Product = {
- title: string;
- variants: ProductVariant[];
-};
-
-type ProductVariant = {
- id: string;
- title: string;
- imageUrl?: string;
-};
-
-async function getProducts(
- adminApiQuery: ValidationSettingsApi["query"],
-): Promise {
+async function getProducts(adminApiQuery) {
const query = `#graphql
query FetchProducts {
products(first: 5) {
@@ -458,392 +405,24 @@ async function getProducts(
}
}`;
- type ProductQueryData = {
- products: {
- nodes: Array<{
- title: string;
- variants: {
- nodes: Array<{
- id: string;
- title: string;
- image?: {
- url: string;
- };
- }>;
- };
- }>;
- };
- };
-
- const results = await adminApiQuery(query);
-
- return (
- results?.data?.products.nodes.map(({ title, variants }) => {
- return {
- title,
- variants: variants.nodes.map((variant) => ({
- title: variant.title,
- id: variant.id,
- imageUrl: variant?.image?.url,
- })),
- };
- }) ?? []
- );
-}
-
-const METAFIELD_NAMESPACE = "$app:product-limits";
-const METAFIELD_KEY = "product-limits-values";
-
-async function getMetafieldDefinition(
- adminApiQuery: ValidationSettingsApi["query"],
-) {
- const query = `#graphql
- query GetMetafieldDefinition {
- metafieldDefinitions(first: 1, ownerType: VALIDATION, namespace: "${METAFIELD_NAMESPACE}", key: "${METAFIELD_KEY}") {
- nodes {
- id
- }
- }
- }
- `;
-
- type MetafieldDefinitionsQueryData = {
- metafieldDefinitions: {
- nodes: Array<{
- id: string;
- }>;
- };
- };
-
- const result = await adminApiQuery(query);
-
- return result?.data?.metafieldDefinitions?.nodes[0];
-}
-
-async function createMetafieldDefinition(
- adminApiQuery: ValidationSettingsApi["query"],
-) {
- const definition = {
- access: {
- admin: "MERCHANT_READ_WRITE",
- },
- key: METAFIELD_KEY,
- name: "Validation Configuration",
- namespace: METAFIELD_NAMESPACE,
- ownerType: "VALIDATION",
- type: "json",
- };
-
- const query = `#graphql
- mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) {
- metafieldDefinitionCreate(definition: $definition) {
- createdDefinition {
- id
- }
- }
- }
- `;
+ const result = await adminApiQuery(query);
- type MetafieldDefinitionCreateData = {
- metafieldDefinitionCreate: {
- createdDefinition?: {
- id: string;
- };
+ return result?.data?.products.nodes.map(({ title, variants }) => {
+ return {
+ title,
+ variants: variants.nodes.map((variant) => ({
+ title: variant.title,
+ id: variant.id,
+ imageUrl: variant?.image?.url,
+ })),
};
- };
-
- const variables = { definition };
- const result = await adminApiQuery(query, {
- variables,
});
-
- return result?.data?.metafieldDefinitionCreate?.createdDefinition;
-}
-
-function createSettings(
- products: Product[],
- configuration: Object,
-): Record {
- const settings = {};
-
- products.forEach(({ variants }) => {
- variants.forEach(({ id }) => {
- // Read existing product limits from metafield
- const limit = configuration[id];
-
- if (limit) {
- settings[id] = limit;
- }
- });
- });
-
- return settings;
-}
-
-{%- elsif flavor contains 'typescript' -%}
-import { type RemoteRoot } from "@remote-ui/core";
-import {
- extend,
- Text,
- Box,
- FunctionSettings,
- Section,
- NumberField,
- BlockStack,
- Banner,
- InlineStack,
- Image,
- type ValidationSettingsApi,
- type FunctionSettingsError,
-} from "@shopify/ui-extensions/admin";
-
-const TARGET = "admin.settings.validation.render";
-
-export default extend(
- TARGET,
- async (root: RemoteRoot, api: ValidationSettingsApi) => {
- 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");
- }
- }
-
- // Read existing persisted data about product limits from the associated metafield
- const configuration = JSON.parse(
- api.data.validation?.metafields?.[0]?.value ?? "{}",
- );
-
- // Query product data needed to render the settings UI
- const products = await getProducts(api.query);
-
- renderValidationSettings(root, configuration, products, api);
- },
-);
-
-function renderValidationSettings(
- root: RemoteRoot,
- configuration: Object,
- products: Product[],
- api: ValidationSettingsApi,
-) {
- let errors: string[] = [];
- // State to keep track of product limit settings, initialized to any persisted metafield value
- let settings = createSettings(products, configuration);
-
- const onError = (newErrors: FunctionSettingsError[]) => {
- errors = newErrors.map((e) => e.message);
- renderContent();
- };
-
- const onChange = async (variant: ProductVariant, value: number) => {
- errors = [];
- const newSettings = {
- ...settings,
- [variant.id]: Number(value),
- };
- settings = newSettings;
-
- // On input change, commit updated product variant limits to memory.
- // Caution: the changes are only persisted on save!
- const result = await api.applyMetafieldChange({
- type: "updateMetafield",
- namespace: "$app:product-limits",
- key: "product-limits-values",
- value: JSON.stringify(newSettings),
- });
-
- if (result.type === "error") {
- errors = [result.message];
- renderContent();
- }
- };
-
- const renderErrors = (errors: string[], root: RemoteRoot) => {
- if (!errors.length) {
- return [];
- }
-
- return errors.map((error, i) =>
- root.createComponent(
- Banner,
- {
- title: "Errors were encountered",
- tone: "critical",
- },
- root.createComponent(Text, {}, error),
- ),
- );
- };
-
- const renderContent = () => {
- return root.append(
- root.createComponent(
- // Note: FunctionSettings must be rendered for the host to receive metafield updates
- FunctionSettings,
- { onError },
- ...renderErrors(errors, root),
- ...products.map((product) =>
- renderProductQuantitySettings(root, product, settings, onChange),
- ),
- ),
- );
- };
-
- renderContent();
-}
-
-function renderProductQuantitySettings(
- root: RemoteRoot,
- product: Product,
- settings: Record,
- onChange: (variant: ProductVariant, value: number) => Promise,
-) {
- const heading = root.createComponent(
- InlineStack,
- {},
- root.createComponent(Box, { minInlineSize: "5%" }),
- root.createComponent(
- Box,
- { minInlineSize: "5%" },
- root.createComponent(Text, { fontWeight: "bold" }, "Variant Name"),
- ),
- root.createComponent(
- Box,
- { minInlineSize: "50%" },
- root.createComponent(Text, { fontWeight: "bold" }, "Limit"),
- ),
- );
-
- const renderVariant = (
- variant: ProductVariant,
- settings: Record,
- root: RemoteRoot,
- ) => {
- const limit = settings[variant.id];
-
- return root.createComponent(
- InlineStack,
- { columnGap: "none" },
- root.createComponent(
- Box,
- { minInlineSize: "5%" },
- variant.imageUrl
- ? root.createComponent(Image, {
- source: variant.imageUrl,
- alt: variant.title,
- })
- : null,
- ),
- root.createComponent(
- Box,
- { minInlineSize: "5%" },
- root.createComponent(Text, {}, variant.title),
- ),
- root.createComponent(
- Box,
- { minInlineSize: "50%" },
- root.createComponent(NumberField, {
- label: "Set a limit",
- value: limit,
- min: 0,
- max: 99,
- defaultValue: String(limit),
- onChange: (value: number) => onChange(variant, value),
- }),
- ),
- );
- };
-
- // Render table of product variants and inputs to assign limits
- return root.createComponent(
- Section,
- { heading: product.title },
- root.createComponent(
- BlockStack,
- { paddingBlock: "large" },
- heading,
- ...product.variants.map((variant) =>
- renderVariant(variant, settings, root),
- ),
- ),
- );
-}
-
-type Product = {
- title: string;
- variants: ProductVariant[];
-};
-
-type ProductVariant = {
- id: string;
- title: string;
- imageUrl?: string;
-};
-
-async function getProducts(
- adminApiQuery: ValidationSettingsApi["query"],
-): Promise {
- const query = `#graphql
- query FetchProducts {
- products(first: 5) {
- nodes {
- title
- variants(first: 5) {
- nodes {
- id
- title
- image {
- url
- }
- }
- }
- }
- }
- }`;
-
- type ProductQueryData = {
- products: {
- nodes: Array<{
- title: string;
- variants: {
- nodes: Array<{
- id: string;
- title: string;
- image?: {
- url: string;
- };
- }>;
- };
- }>;
- };
- };
-
- const result = await adminApiQuery(query);
-
- return (
- result?.data?.products.nodes.map(({ title, variants }) => {
- return {
- title,
- variants: variants.nodes.map((variant) => ({
- title: variant.title,
- id: variant.id,
- imageUrl: variant?.image?.url,
- })),
- };
- }) ?? []
- );
}
const METAFIELD_NAMESPACE = "$app:product-limits";
const METAFIELD_KEY = "product-limits-values";
-async function getMetafieldDefinition(
- adminApiQuery: ValidationSettingsApi["query"],
-) {
+async function getMetafieldDefinition(adminApiQuery) {
const query = `#graphql
query GetMetafieldDefinition {
metafieldDefinitions(first: 1, ownerType: VALIDATION, namespace: "${METAFIELD_NAMESPACE}", key: "${METAFIELD_KEY}") {
@@ -854,22 +433,12 @@ async function getMetafieldDefinition(
}
`;
- type MetafieldDefinitionsQueryData = {
- metafieldDefinitions: {
- nodes: Array<{
- id: string;
- }>;
- };
- };
-
- const result = await adminApiQuery(query);
+ const result = await adminApiQuery(query);
return result?.data?.metafieldDefinitions?.nodes[0];
}
-async function createMetafieldDefinition(
- adminApiQuery: ValidationSettingsApi["query"],
-) {
+async function createMetafieldDefinition(adminApiQuery) {
const definition = {
access: {
admin: "MERCHANT_READ_WRITE",
@@ -891,31 +460,17 @@ async function createMetafieldDefinition(
}
`;
- type MetafieldDefinitionCreateData = {
- metafieldDefinitionCreate: {
- createdDefinition?: {
- id: string;
- };
- };
- };
-
const variables = { definition };
- const result = await adminApiQuery(query, {
- variables,
- });
+ const result = await adminApiQuery(query, { variables });
return result?.data?.metafieldDefinitionCreate?.createdDefinition;
}
-function createSettings(
- products: Product[],
- configuration: Object,
-): Record {
+function createSettings(products, configuration) {
const settings = {};
products.forEach(({ variants }) => {
variants.forEach(({ id }) => {
- // Read existing product limits from metafield
const limit = configuration[id];
if (limit) {
@@ -927,9 +482,9 @@ function createSettings(
return settings;
}
-{%- elsif flavor contains 'vanilla-js' -%}
+{%- else -%}
import {
- extend,
+ extension,
Text,
Box,
FunctionSettings,
@@ -943,10 +498,9 @@ import {
const TARGET = "admin.settings.validation.render";
-export default extend(TARGET, async (root, api) => {
+export default extension(TARGET, async (root, 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) {
@@ -954,12 +508,10 @@ export default extend(TARGET, async (root, api) => {
}
}
- // Read existing persisted data about product limits from the associated metafield
const configuration = JSON.parse(
api.data.validation?.metafields?.[0]?.value ?? "{}",
);
- // Query product data needed to render the settings UI
const products = await getProducts(api.query);
renderValidationSettings(root, configuration, products, api);
@@ -967,7 +519,6 @@ export default extend(TARGET, async (root, api) => {
function renderValidationSettings(root, configuration, products, api) {
let errors = [];
- // State to keep track of product limit settings, initialized to any persisted metafield value
let settings = createSettings(products, configuration);
const onError = (newErrors) => {
@@ -983,8 +534,6 @@ function renderValidationSettings(root, configuration, products, api) {
};
settings = newSettings;
- // On input change, commit updated product variant limits to memory.
- // Caution: the changes are only persisted on save!
const result = await api.applyMetafieldChange({
type: "updateMetafield",
namespace: "$app:product-limits",
@@ -1018,7 +567,6 @@ function renderValidationSettings(root, configuration, products, api) {
const renderContent = () => {
return root.append(
root.createComponent(
- // Note: FunctionSettings must be rendered for the host to receive metafield updates
FunctionSettings,
{ onError },
...renderErrors(errors, root),
@@ -1085,7 +633,6 @@ function renderProductQuantitySettings(root, product, settings, onChange) {
);
};
- // Render table of product variants and inputs to assign limits
return root.createComponent(
Section,
{ heading: product.title },
@@ -1185,7 +732,6 @@ function createSettings(products, configuration) {
products.forEach(({ variants }) => {
variants.forEach(({ id }) => {
- // Read existing product limits from metafield
const limit = configuration[id];
if (limit) {