Add Package Protection Failing API 2025-10

Topic summary

A developer’s checkout extension code that worked with API version 2025-04 is now failing with 2025-10 after creating a new framework using shopify app init and shopify app generate extension.

Core Issue:

  • Build errors indicate missing exports from @shopify/ui-extensions/build/esm/surfaces/checkout.mjs
  • Components failing to import: Button, SkeletonImage, SkeletonText
  • The extension attempts to add package protection functionality to checkout

Technical Details:

  • Uses Preact for rendering
  • Targets a specific product variant (package-protection-free-replacements-returns)
  • Code includes state management for product loading and image handling
  • Error occurs during the bundling phase

Key Questions:

  • How to make the code compatible with API 2025-10?
  • Why can’t the developer recreate working production code on their staging store?
  • Whether it’s possible to revert to the older framework

The discussion remains open with no resolution provided yet.

Summarized with AI on October 23. AI used: claude-sonnet-4-5-20250929.

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?

Let me add, that I’ve gone through all sorts of iterations, including moving down to API version 2025–04, and the following Checkout.jsx:

export default reactExtension("purchase.post.purchase.render", () => (
  <BlockStack>
    <Text>✅ Post-purchase render works</Text>
  </BlockStack>
));

I get the flash and then disappears when adding to the Summary Block or the Main block. My store is a Shopify Plus, verified with Graphql. Very frustrating. Same with my console Dev Store.

Is something broken on Shopify?