Skip to main content

Building a great merchant-facing experience for customer account UI extensions

In this workshop you'll learn all about how to use the editor extension API, extension collections, and querying the storefront API to build a great merchant-facing experience for your extensions.

This workshop covers the following topics:

  • Using the extension.editor extension API to implement a preview that only appears for merchants in the editor preview
  • Querying the Storefront API from an extension to get products from the merchant's shop to display in the editor preview
  • Using extension collections and deep linking to them from an embedded app

  • A Shopify Partner account
  • An organization on Dev Dashboard
  • A dev store populated with test data (can be created during the workshop)

Anchor to Development EnvironmentDevelopment Environment


Anchor to Setting Up Your EnvironmentSetting Up Your Environment

Anchor to Install the latest Shopify CLIInstall the latest Shopify CLI

You'll need the latest version of Shopify CLI for this workshop.

Terminal

# Check the version you have installed
shopify version

# Install the latest
npm install -g @shopify/cli@latest

Anchor to Create a new development storeCreate a new development store

Create a new development store with demo data using the Dev Dashboard:

  1. Open the Dev Dashboard.
  2. Navigate to Dev stores.
  3. Click Add dev store.
  4. Fill out the details for your dev store.
    • Enter a name, such as masterclass-customer-accounts-<your initials>.
    • IMPORTANT: Build version should be Checkout and accounts extensibility.
    • IMPORTANT: Check the box to Generate test data for store.
    • Click Create store.

Anchor to Initialize a new appInitialize a new app

  1. In a terminal, create a new app with unified components enabled. Follow the prompts to create your app:

Terminal

# Enter this command and follow the prompts
shopify app init --template https://github.com/Shopify/editions-dev-2025-workshop-customer-account

IMPORTANT: make sure you select your Dev Dashboard org, if you have multiple orgs.

If you're asked whether you want to link it to an existing app or create a new app, create a new app

  1. Open the Dev Dashboard and verify your app is present.

In this section, we'll make some adjustments and get our app running.

Anchor to Request protected customer data accessRequest protected customer data access

  1. In the legacy Partner dashboard, go to Apps > Dev Dashboard apps > Your app > API access requests.
  2. Scroll down to the "Protected customer data access" section and click on "Request access"
  3. Select any reason for using protected custom data and click "Save"

Anchor to Add product tags to a few of the products on your storeAdd product tags to a few of the products on your store

Navigate to some products on your store and assign them a product tag called "wishlist_suggestions".

Anchor to Run the app using localhostRun the app using localhost

Now get the app running in your store:

Terminal

shopify app dev --use-localhost

Anchor to Enable the full page and inline extensionsEnable the full page and inline extensions

Navigate to the checkout and accounts editor using this link.

Select the "Apps" tab on the left panel, and click the "+" buttons to enable the extensions. Then, click "Save".

The inline extension will have no preview, and the full page extension will have a empty state preview.


Anchor to Setting a mock product in the wishlist when in the editorSetting a mock product in the wishlist when in the editor

In your code editor, navigate to FullPageExtension.tsx, and update the code at the top of the component to create an isInEditor variable:

FullPageExtension.tsx

function WishlistedItemsPage() {
// Add the following two lines of code, leave the rest of the file as-is

const { editor } = shopify.extension;
const isInEditor = editor?.type === "checkout";

const { id: customerId } = useAuthenticatedAccountCustomer();
// etc.
}

Next, make use of isInEditor and set mock data for the recommended products in this case in the useEffect hook.

FullPageExtension.tsx

useEffect(() => {
async function run() {
const shopDataPromise = fetchShopData();

const products = isInEditor
? [
{
id: "1",
title: "Product 1",
handle: "product-1",
priceRange: {
minVariantPrice: {
amount: 100,
currencyCode: "USD",
},
maxVariantPrice: {
amount: 100,
currencyCode: "USD",
},
},
images: {
nodes: [
{
url: "https://placehold.co/150",
},
],
},
},
]
: await fetchProducts(await fetchWishlistedProductIds());

setShopData(await shopDataPromise);
setWishlist(products);
setLoading(false);
}
run();
}, [isInEditor]);

Next, reload the Editor, and navigate to the full page extension using the page selector.

You should now see a preview in the editor for the mock product we added. While this is better than an empty state, it makes it difficult for merchants to visualize what the experience would look like for a customer on their store, since this isn't reflective of their store's products.

Let's improve this by querying the Storefront API to get some real products.

Anchor to Querying the Storefront API to get real preview productsQuerying the Storefront API to get real preview products

Create a function called fetchPreviewProducts at the bottom of the page:

FullPageExtension.tsx

async function fetchPreviewProducts() {
const response = await fetch(
"shopify://storefront/api/unstable/graphql.json",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(getFirst3ProductsQuery()),
},
);

const data = await response.json();
return data?.data?.products?.nodes.filter((node) => node !== null);
}

getFirst3ProductsQuery already exists in a the shared graphql.ts file. It queries three products from the merchant's shop.

FullPageExtension.tsx

export const getFirst3ProductsQuery = () => {
return {
query: `
${productFragment}
query {
products(first: 3) {
nodes {
... on Product {
...ProductFields
}
}
}
}`,
};
};

Next, update the useEffect hook to make use of fetchPreviewProducts

FullPageExtension.tsx

useEffect(() => {
async function run() {
const shopDataPromise = fetchShopData();
const shopData = await shopDataPromise;
setShopData(shopData);

const products = isInEditor
? await fetchPreviewProducts()
: await fetchProducts(await fetchWishlistedProductIds());

setWishlist(products);
setLoading(false);
}
run();
}, [isInEditor]);

Reload the Editor, and you'll see that the preview is now much more representative to what a real customer would see on their shop, as we now show products from the shop itself in the preview.

Anchor to Show configuration warnings to merchants in the editorShow configuration warnings to merchants in the editor

In our example, we have an inline extension to recommend products to wishlist. The product recommendations are queried using a product tag, which needs to be provided by the merchant for the extension to function. In this section, we will provide contextual information to the merchant to ensure they are aware of what they're missing for the extension to be configured correctly.

In our code, we have a default value for the product tag, navigate to OrderListPageExtension.tsx and remove that default value:

OrderListPageExtension.tsx

function OrderListPageExtension({
initialProducts,
}: {
initialProducts: Product[];
}) {
// Before
// const { product_tag: productTag = "wishlist_suggestions" } = useSettings();

// After
const { product_tag: productTag } = useSettings();

// etc
}

Next, update the code at the top of the component to create an isInEditor variable:

OrderListPageExtension.tsx

function OrderListPageExtension({
initialProducts,
}: {
initialProducts: Product[];
}) {
// Add the following two lines of code, leave the rest of the file as-is
const { editor } = shopify.extension;
const isInEditor = editor?.type === "checkout";

const { product_tag: productTag } = useSettings();
// etc.
}

Right above if (suggestedProducts.length === 0) return null, add a condition to render a banner if we are in the editor and no product tag has been provided by the merchant.

OrderListPageExtension.tsx

if (isInEditor && !productTag) {
return (
<s-banner tone="critical">
Please set a product tag in the extension settings to display products.
This message will only be shown in the editor.
</s-banner>
);
}
if (suggestedProducts.length === 0) return null;

Reload the Editor. You should now see a banner appear, warning the merchant that they need to set a value for the product tag in the extension settings.

Another potential issue is that the merchant could have a typo in the product tag, or they could forget to associate products to that product tag. Let's add a condition to warn them in that case as well.

OrderListPageExtension.tsx

if (isInEditor && !productTag) {
return (
<s-banner tone="critical">
Please set a product tag in the extension settings to display products.
This message will only be shown in the editor.
</s-banner>
);
}

if (suggestedProducts.length === 0 && isInEditor && productTag) {
return (
<s-banner tone="warning">
No products found for the selected tag. This message will only be shown
in the editor.
</s-banner>
);
}

if (suggestedProducts.length === 0) return null;

Reload the Editor, add a typo to the product tag supplied to the extension, and you'll see this banner appear.

Anchor to Disable actions in the editorDisable actions in the editor

Merchants will likely to try and click on the different buttons and links in an extension, but in the context of the editor, most actions won't work. It's better to mimic the behaviour of other pages in the editor, and disable actions that can't work in the editor.

In our case, that's the onClick action for the "Add to wishlist" button in our inline extension - in the OrderListPageExtension.tsx file.

OrderListPageExtension.tsx

<s-button
inline-size="fill"
variant="secondary"
onClick={() => {
if (isInEditor) return;
addToWishlist(product.id);
}}
>
Add to Wishlist
</s-button>

And the onClick action for the "Remove" button in our full page extension - in the "FullPageExtension.tsx file.

FullPageExtension.tsx

<WishlistItem
key={product.id}
product={product}
shopUrl={shopData.url}
onRemoveClick={() => {
// Add the following line
if (isInEditor) return;
removeItemFromWishlist(wishlistedProductIds, product.id);
}}
/>

Anchor to Using extension collectionsUsing extension collections

Extension collections offer a way to visually group your extensions in the checkout and accounts editor, and to deep link to them.

In this section, we will create an extension collection for our two extensions, and we will update our embedded app to dink link to it.

Anchor to Create an extension collectionCreate an extension collection

Run the following Shopify CLI command:

Terminal

shopify app generate extension

Select "Editor extension collection" as the extension type, and then provide a name for your collection, for example wishlist-collection.

# Choose Editor extension collection
? Type of extension?
Admin action
Admin block
Admin print action
Admin purchase options action
App Support link
Conditional admin action
Discount Function Settings
> Editor extension collection

Next, navigate to your extension collection's toml file, and add a reference to the inline and full page extensions, using the extension handles:

shopify.extension.toml

[[extensions]]
name = "t:collection_name"
type = "editor_extension_collection"
uid = "74c9a40e-bb7b-f223-0351-97ccb441b212432b45b0"
handle = "wishlist-collection"
# Add the handles of the extensions you want in this collection here
includes=["wishlist-suggestions-inline", "wishlist-page"]

Reload the Editor, and you'll now see the extensions are grouped in the editor.

Anchor to Deep linking to the extension collections from an embedded appDeep linking to the extension collections from an embedded app

In your code editor, navigate to app/routes/app._index.tsx. Then, update the value of APP_ID to be the id of your app, which you can find in the dev console, or in the Dev dashboard.

app/routes/app._index.tsx

const APP_ID = "242741477377";

Next, update actionLink in the activated-extension step:

app/routes/app._index.tsx

const steps = [
{
handle: "new-customer-accounts",
title: "Upgrade to new customer accounts",
description:
"You are still using legacy customer accounts. Please upgrade to the new version to use this app.",
actionTitle: "Upgrade",
actionLink: "shopify://admin/settings/customer_accounts",
isComplete: isUsingCustomerAccounts,
expandableWhenComplete: false,
},
{
handle: "activated-extension",
title: "Add to customer accounts",
description:
"Allow buyers to manage their wishlist. Add it now to customer accounts.",
actionTitle: "Add in the editor",
// Update the following to deep link to the extension collection. `wishlist-collection` below is the extension collection's handle, update it to the handle you gave your collection.
actionLink: `shopify://admin/settings/checkout/editor?page=order-list&context=apps&app=${APP_ID}&collection=wishlist-collection`,
isComplete: completeOverrides["activated-extension"],
onNavigate: () => {
setCompleteOverrides((prev) => ({
...prev,
"activated-extension": true,
}));
},
},
];

Navigate to your embedded app, and click on "Add in the editor" - you will now be deep-linked to the editor, and the extension collection will be highlighted.


In this workshop, you've learned how to build a good merchant experience for your extensions by:

  • Using the extension.editor extension API to implement a preview that only appears for merchants in the editor preview
  • Querying the Storefront API from an extension to get products from the merchant's shop to display in the editor preview
  • Using extension collections and deep linking to them from an embedded app

As Shopify builds more and more merchant-facing experiences that will drive them straight to the Checkout and accounts editor, using these tools to build a great merchant experience for your extensions will become even more essential to ensuring a smooth onboarding process.


Was this page helpful?