Hi, I have code that I wrote in May, it no longer works in the new API 2025-10. The old API 2025-04 worked fine.
I created a new framework (shopify app init, shopify app generate extension) and the following code does not work:
This is my Checkout.jsx:
import '@shopify/ui-extensions/preact';
import {render} from 'preact';
import {useEffect, useState, useRef} from 'preact/hooks';
// 1. Export the extension
export default async () => {
console.log('Starting extension render');
render(<Extension />, document.body);
console.log('Completed extension render');
};
function Extension() {
console.log('Extension component initializing');
console.log('Setting up state');
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
const imgRef = useRef(null);
console.log('Initial state:', { loading, product });
// Product details we'll try to load
const productHandle = 'package-protection-free-replacements-returns';
const variantId = '45246379786418';
const gidVariant = `gid://shopify/ProductVariant/${variantId}`;
// Load product data when the extension mounts
useEffect(() => {
console.log('useEffect running');
async function loadProduct() {
try {
// Note: Since direct GraphQL queries might not be available in all extension
// environments, we'll use a simpler object with the known details
// Use the store-hosted CDN image as the primary source
// If you prefer a different file, replace this CDN URL with the public Files URL from your store
const cdnImageUrl = 'https://cdn.shopify.com/s/files/1/0019/2672/5691/files/PackageProtectionMarika.png?v=1742422040';
const productData = {
title: 'Package Protection',
description: 'Add protection to your order for replacements and returns.',
image: {
url: cdnImageUrl,
altText: 'Package Protection'
}
};
console.log('Setting product data with image (CDN):', productData.image);
setProduct(productData);
} catch (err) {
console.error('Failed to set product data', err);
} finally {
setLoading(false);
}
}
loadProduct();
}, []);
// Network check when product is set
useEffect(() => {
if (!product?.image?.url) {
console.log('Network check: No image URL available yet');
return;
}
const imageUrl = product.image.url;
console.log('Network check starting for URL:', imageUrl);
let mounted = true;
fetch(imageUrl, {
method: 'GET',
cache: 'no-store',
mode: 'no-cors'
})
.then(res => {
if (!mounted) return;
console.log('Fetch result for image:', {
ok: res.ok,
status: res.status,
type: res.type,
url: imageUrl
});
})
.catch(err => {
if (!mounted) return;
console.error('Fetch attempt for image failed:', err);
setImageError(true);
});
const checkImageElement = () => {
if (!mounted) return;
const imgEl = imgRef.current;
if (imgEl) {
console.log('IMG element check:', {
naturalWidth: imgEl.naturalWidth,
naturalHeight: imgEl.naturalHeight,
src: imgEl.src
});
if (imgEl.naturalWidth > 0 && imgEl.naturalHeight > 0) {
setImageLoaded(true);
setImageError(false);
}
}
};
// Check after a short delay
const timeoutId = setTimeout(checkImageElement, 500);
return () => {
mounted = false;
clearTimeout(timeoutId);
};
}, [product]);
// 2. Check instructions for feature availability, see https://shopify.dev/docs/api/checkout-ui-extensions/apis/cart-instructions for details
if (!shopify.instructions.value.attributes.canUpdateAttributes) {
// For checkouts such as draft order invoices, cart attributes may not be allowed
// Consider rendering a fallback UI or nothing at all, if the feature is unavailable
return (
<s-banner heading="mstaging-new-package" tone="warning">
{shopify.i18n.translate("attributeChangesAreNotSupported")}
</s-banner>
);
}
console.log('About to render UI');
// 3. Render a UI
return (
<s-stack>
<s-text>Basic Test Text</s-text>
{loading && <s-text>Loading...</s-text>}
{product && <s-text>Product loaded</s-text>}
{!loading && !product && <s-text>No product state</s-text>}
{/* Debug: render inline SVG directly (avoid <img> restrictions) */}
{product && (
<>
<s-text>Image URL: {product.image?.url}</s-text>
<s-text>Image loaded: {imageLoaded ? 'yes' : 'no'}</s-text>
<s-text>Image error: {imageError ? 'yes' : 'no'}</s-text>
{/* Primary: external CDN image */}
<div style={{ marginTop: '8px' }}>
<img
src={product.image?.url}
alt={product.image?.altText || 'Package Protection'}
width="200"
height="200"
ref={imgRef}
onLoad={() => {
console.log('Image onLoad event triggered');
if (imgRef.current) {
console.log('Image element properties:', {
src: imgRef.current.src,
complete: imgRef.current.complete,
naturalWidth: imgRef.current.naturalWidth,
naturalHeight: imgRef.current.naturalHeight
});
}
setImageLoaded(true);
setImageError(false);
}}
onError={() => {
console.error('Image onError event triggered');
setImageError(true);
setImageLoaded(false);
}}
style={{ display: 'block', maxWidth: '100%' }}
/>
</div>
{/* Fallback inline SVG (visible when image fails) */}
{imageError && (
<svg
width={200}
height={200}
viewBox="0 0 200 200"
style={{ display: 'block', marginTop: '8px' }}
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label={product.image?.altText || 'Package Protection'}
>
<rect width="100%" height="100%" fill="#f3f4f6" />
<text x="50%" y="50%" dominantBaseline="middle" textAnchor="middle" fontFamily="Arial" fontSize="16" fill="#111">Pkg Protection</text>
</svg>
)}
</>
)}
</s-stack>
);
async function handleClick() {
// Add a protection product to the checkout.
// Primary identifier: product handle (string) as the user requested.
// We also attempt using the known variant id for better success.
const productHandle = 'package-protection-free-replacements-returns';
const variantId = '45246379786418';
const gidVariant = `gid://shopify/ProductVariant/${variantId}`;
// Use a loosely typed API reference to avoid strict TS mismatch issues
/** @type {any} */ const api = shopify;
// Note: We avoid directly reading `shopify.cart` because the type for the
// checkout API available in this extension target may not expose it.
// We'll rely on the apply APIs and fallback attribute instead.
// 2) Try to add the product using various candidate payload shapes.
try {
if (shopify.instructions.value.lines?.canUpdateCartLine && typeof api.applyCartLinesChange === 'function') {
const tryPayloads = [
// Common shape: add cart line using GraphQL global id
{ type: 'addCartLine', cartLine: { merchandiseId: gidVariant, quantity: 1 } },
// Variant id numeric style
{ type: 'addCartLine', cartLine: { variantId: variantId, quantity: 1 } },
// Array-of-lines style with merchandiseId
{ type: 'addCartLine', lines: [{ merchandiseId: gidVariant, quantity: 1 }] },
// Array-of-lines style with variantId
{ type: 'addCartLine', lines: [{ variantId: variantId, quantity: 1 }] },
// Fallback try using product handle (less likely, but requested)
{ type: 'addCartLine', lines: [{ productHandle: productHandle, quantity: 1 }] },
];
for (const payload of tryPayloads) {
try {
const res = await api.applyCartLinesChange(payload);
console.log('applyCartLinesChange result', res, 'payload', payload);
return;
} catch (err) {
console.debug('applyCartLinesChange payload failed', payload, err);
}
}
}
} catch (err) {
console.warn('adding protection product via applyCartLinesChange failed', err);
}
// 3) Fallback: store request in a checkout attribute so backend or other
// processes can add the item server-side.
try {
const result = await api.applyAttributeChange({
key: 'requestedFreeGift',
type: 'updateAttribute',
value: productHandle,
});
console.log('applyAttributeChange result', result);
} catch (err) {
console.error('failed to set fallback attribute for protection product', err);
}
}
}
If I try and use my old code (works in production):
import {
extension,
Text,
InlineLayout,
BlockStack,
Divider,
Image,
Banner,
Heading,
Button,
SkeletonImage,
SkeletonText,
} from "@shopify/ui-extensions/checkout";
// Set up the entry point for the extension
export default extension(
"purchase.checkout.block.render",
(root, { lines, applyCartLinesChange, query, i18n }) => {
let products = [];
let loading = true;
let appRendered = false;
// Fetch products from the server
fetchProducts(query).then((fetchedProducts) => {
products = fetchedProducts;
loading = false;
renderApp();
});
lines.subscribe(() => renderApp());
const loadingState = createLoadingState(root);
if (loading) {
root.appendChild(loadingState);
}
const { imageComponent, titleMarkup, priceMarkup, merchandise } =
createProductComponents(root);
const addButtonComponent = createAddButtonComponent(
root,
applyCartLinesChange,
merchandise
);
const app = createApp(
root,
imageComponent,
titleMarkup,
priceMarkup,
addButtonComponent
);
function renderApp() {
if (loading) {
return;
}
if (!loading && products.length === 0) {
root.removeChild(loadingState);
return;
}
const productsOnOffer = filterProductsOnOffer(lines, products);
if (!loading && productsOnOffer.length === 0) {
if (loadingState.parent) root.removeChild(loadingState);
if (root.children) root.removeChild(root.children[0]);
return;
}
updateProductComponents(
productsOnOffer[0],
imageComponent,
titleMarkup,
priceMarkup,
addButtonComponent,
merchandise,
i18n
);
if (!appRendered) {
root.removeChild(loadingState);
root.appendChild(app);
appRendered = true;
}
}
}
);
function fetchProducts(query) {
return query(
`query {
products(first: 1, query: "handle:'package-protection-free-replacements-returns") {
nodes {
id
title
images(first:1){
nodes {
url
}
}
variants(first: 1) {
nodes {
id
price {
amount
}
}
}
}
}
}`,
)
.then(({ data }) => data.products.nodes)
.catch((err) => {
console.error(err);
return [];
});
}
function createLoadingState(root) {
return root.createComponent(BlockStack, { spacing: "loose" }, [
root.createComponent(Divider),
root.createComponent(Heading, { level: 2 }, ["Add Package Protection:"]),
root.createComponent(BlockStack, { spacing: "loose" }, [
root.createComponent(
InlineLayout,
{
spacing: "base",
columns: [64, "fill", "auto"],
blockAlignment: "center",
},
[
root.createComponent(SkeletonImage, { aspectRatio: 1 }),
root.createComponent(BlockStack, { spacing: "none" }, [
root.createComponent(SkeletonText, { inlineSize: "large" }),
root.createComponent(SkeletonText, { inlineSize: "small" }),
]),
root.createComponent(Button, { kind: "secondary", disabled: true }, [
root.createText("Add"),
]),
]
),
]),
]);
}
function createProductComponents(root) {
const imageComponent = root.createComponent(Image, {
border: "base",
borderWidth: "base",
borderRadius: "loose",
source: "",
accessibilityDescription: "",
aspectRatio: 1,
});
const titleMarkup = root.createText("");
const priceMarkup = root.createText("");
const merchandise = { id: "" };
return { imageComponent, titleMarkup, priceMarkup, merchandise };
}
function createAddButtonComponent(root, applyCartLinesChange, merchandise) {
return root.createComponent(
Button,
{
kind: "secondary",
loading: false,
onPress: async () => {
await handleAddButtonPress(root, applyCartLinesChange, merchandise);
},
},
["Add"]
);
}
async function handleAddButtonPress(root, applyCartLinesChange, merchandise) {
const result = await applyCartLinesChange({
type: "addCartLine",
merchandiseId: merchandise.id,
quantity: 1,
});
if (result.type === "error") {
displayErrorBanner(
root,
"There was an issue adding this product. Please try again."
);
}
}
function displayErrorBanner(root, message) {
const errorComponent = root.createComponent(Banner, { status: "critical" }, [
message,
]);
const topLevelComponent = root.children[0];
topLevelComponent.appendChild(errorComponent);
setTimeout(() => topLevelComponent.removeChild(errorComponent), 3000);
}
function createApp(
root,
imageComponent,
titleMarkup,
priceMarkup,
addButtonComponent
) {
return root.createComponent(BlockStack, { spacing: "loose" }, [
root.createComponent(Divider),
root.createComponent(Heading, { level: 2 }, "Add Package Protection"),
root.createComponent(BlockStack, { spacing: "loose" }, [
root.createComponent(
InlineLayout,
{
spacing: "base",
columns: [64, "fill", "auto"],
blockAlignment: "center",
},
[
imageComponent,
root.createComponent(BlockStack, { spacing: "none" }, [
root.createComponent(Text, { size: "medium", emphasis: "bold" }, [
titleMarkup,
]),
root.createComponent(Text, { appearance: "subdued" }, [
priceMarkup,
]),
]),
addButtonComponent,
]
),
]),
]);
}
function filterProductsOnOffer(lines, products) {
const cartLineProductVariantIds = lines.current.map(
(item) => item.merchandise.id
);
return products.filter((product) => {
const isProductVariantInCart = product.variants.nodes.some(({ id }) =>
cartLineProductVariantIds.includes(id)
);
return !isProductVariantInCart;
});
}
function updateProductComponents(
product,
imageComponent,
titleMarkup,
priceMarkup,
addButtonComponent,
merchandise,
i18n
) {
const { images, title, variants } = product;
const renderPrice = i18n.formatCurrency(variants.nodes[0].price.amount);
const imageUrl =
images.nodes[0]?.url ??
"https://cdn.shopify.com/s/files/1/0533/2089/files/placeholder-images-image_medium.png?format=webp&v=1530129081";
imageComponent.updateProps({ source: imageUrl });
titleMarkup.updateText(title);
addButtonComponent.updateProps({
accessibilityLabel: `Add ${title} to cart,`,
});
priceMarkup.updateText(renderPrice);
merchandise.id = variants.nodes[0].id;
}
Then this is the compile errors I get:
Releasing a new app version as part of mstaging-new-package
mstaging-new-package │ Bundling UI extension mstaging-new-package...
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "extension"
mstaging-new-package/src/Checkout.jsx:2:2:
2 │ extension,
╵ ~~~~~~~~~
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "Text"
mstaging-new-package/src/Checkout.jsx:3:2:
3 │ Text,
╵ ~~~~
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "InlineLayout"
mstaging-new-package/src/Checkout.jsx:4:2:
4 │ InlineLayout,
╵ ~~~~~~~~~~~~
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "BlockStack"
mstaging-new-package/src/Checkout.jsx:5:2:
5 │ BlockStack,
╵ ~~~~~~~~~~
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "Divider"
mstaging-new-package/src/Checkout.jsx:6:2:
6 │ Divider,
╵ ~~~~~~~
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "Image"
mstaging-new-package/src/Checkout.jsx:7:2:
7 │ Image,
╵ ~~~~~
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "Banner"
mstaging-new-package/src/Checkout.jsx:8:2:
8 │ Banner,
╵ ~~~~~~
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "Heading"
mstaging-new-package/src/Checkout.jsx:9:2:
9 │ Heading,
╵ ~~~~~~~
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "Button"
mstaging-new-package/src/Checkout.jsx:10:2:
10 │ Button,
╵ ~~~~~~
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "SkeletonImage"
mstaging-new-package/src/Checkout.jsx:11:2:
11 │ SkeletonImage,
╵ ~~~~~~~~~~~~~
✘ [ERROR] No matching export in "../node_modules/@shopify/ui-extensions/build/esm/surfaces/checkout.mjs" for import "SkeletonText"
mstaging-new-package/src/Checkout.jsx:12:2:
12 │ SkeletonText,
╵ ~~~~~~~~~~~~
╭─ error ──────────────────────────────────────────────────────────────────────╮
│ │
│ Failed to bundle extension mstaging-new-package. Please check the │
│ extension source code for errors. │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
What can I do to get my code working? It seems I cannot go back to the old framework, code that works in production I cannot recreate on my staging store. Why is that?
