Build a pre-purchase Shopify checkout UI extension
Topics covered: Shopify connections, React frontends
Time to build: ~20 minutes
Checkout UI extensions are an important part of the Shopify developer ecosystem. Since Shopify announced the deprecation of checkout.liquid
, it has become more important than ever for developers to be able to swiftly migrate existing Plus-merchant checkout functionality over to checkout UI extensions.
In this tutorial, you will build a simple pre-purchase upsell extension that allows merchants to select a product in an embedded admin app that will then be offered to shoppers during checkout. Gadget will take care of your embedded app frontend, backend, and database, and the Shopify CLI will be used to manage the checkout UI extension.

To get the most out of this tutorial, you will need:
- A Shopify Partners account
- A development store with the checkout extensibility developer preview enabled
You can fork this Gadget project to quickly preview this app. There are two things you need to do after forking:
- Set up a Shopify Partners Connection after forking
- After setting up your connection, you also need to build, test, and deploy your checkout UI extension
Follow along and build a pre-purchase checkout extension. Learn how to connect to Shopify, build an embedded admin frontend, write data to metafields from Gadget, and set up a checkout UI extension.
Create a Gadget app and connect to Shopify
Your first step will be to set up a Gadget project and connect to a Shopify store via the Shopify connection. Create a new Gadget application at gadget.new.
Because we are adding an embedded frontend, we are going to connect to Shopify using the Partners connection.
Connect to Shopify through the Partners dashboard
To complete this connection, you will need a Shopify Partners account as well as a store or development store
Our first step is going to be setting up a custom Shopify application in the Partners dashboard.
- Go to the Shopify Partners dashboard
- Click on the link to the Apps page
Both the Shopify store Admin and the Shopify Partner Dashboard have an Apps section. Ensure that you are on the Shopify Partner Dashboard before continuing.

- Click the Create App button

- Click the Create app manually button and enter a name for your Shopify app

- Go to the Connections page in your Gadget app

- Copy the Client ID and Client secret from your newly created Shopify app and paste the values into the Gadget Connections page
- Click Connect on the Gadget Connections page to move to scope and model selection

Now we get to select what Shopify scopes we give our application access to, while also picking what Shopify data models we want to import into our Gadget app.
- Enable the read scope for the Shopify Products API, and select the underlying Product model that we want to import into Gadget

- Click Confirm
Now we want to connect our Gadget app to our custom app in the Partners dashboard.
- In your Shopify app in the Partners dashboard, click on App setup in the side nav bar so you can edit the App URL and Allowed redirection URL(s) fields
- Copy the App URL and Allowed redirection URL from the Gadget Connections page and paste them into your custom Shopify App

Now we need to install our Shopify app on a store, and sync data.
- Go back to the Shopify Partners dashboard
- Click on Apps to go to the Apps page again
- Click on your custom app
- Click on Select store
- Click on the store we want to use to develop our app
- You may be prompted about Store transfer being disabled. This is okay, click Install anyway
- Click Install app to install your Gadget app on your Shopify store


You will be redirected to an embedded admin app that has been generated for you. The code for this app template can be found in frontend/ShopPage.jsx.

Set up is complete! We are now ready to build our Gadget application.
If we have created at least one product in our store we can test out the connection:
- Go back to our Gadget Connections page and click on the Shop Installs button for the added app in the Shopify Apps section
- Click the Sync button for the installed store
- We should see that the Sync action was a success



That's it! We have successfully set up a store and custom app in Shopify, connected our Gadget app to this store, and synced our store data with our Gadget app.
Note: For public Shopify apps, you can add a code effect to sync data on Shop install. For more information, check out our docs.
After your data sync is successful, you can view synced data in the Shopify Product Data page in Gadget. To view this page:
- Click on the Shopify Product model in the Gadget nav
- Click Data to view the Shopify Product data that has been synced to your Gadget app
Now that your app is connected to Shopify, the next step will be to start building your app frontend that will be embedded in the store admin.
Build an embedded frontend
This is all the code you need to add a product picker to an embedded app. Shopify Polaris is used to draw components and Gadget's React tooling is used to manage reading from your Gadget app's backend API. An explanation of the Gadget tooling used is found below the snippet.
- Copy and paste this snippet into
frontend/ShopPage.jsx
:
frontend/ShopPage.jsxjsx1import { useFindFirst, useFindMany } from "@gadgetinc/react";2import { Banner, FooterHelp, Layout, Link, Page, Select, Spinner } from "@shopify/polaris";3import { api } from "./api";4import { useEffect, useState, useCallback } from "react";56const ShopPage = () => {7 // use React state to handle selected product and options8 const [selectedProduct, setSelectedProduct] = useState("");9 const [productOptions, setProductOptions] = useState([]);1011 // use the Gadget React hooks to fetch products and shop data12 const [{ data, fetching, error }] = useFindMany(api.shopifyProduct);13 const [{ data: shopData, fetching: shopFetching, error: shopFetchError }] = useFindFirst(api.shopifyShop, {14 select: {15 id: true,16 },17 });1819 // a React useEffect hook to build product options for the Select component20 useEffect(() => {21 if (data) {22 const options = data.map((product) => ({23 value: product.id,24 label: product.title,25 }));26 setProductOptions(options);27 }28 }, [data]);2930 // a useCallback hook that will send the selected product id to Gadget (eventually...)31 const saveSelection = useCallback(() => {32 console.log({ selectedProduct, shopData });33 }, [selectedProduct, shopData]);3435 return (36 <Page37 title="Select product for pre-purchase offer"38 primaryAction={{ content: "Save", disabled: !selectedProduct, onAction: saveSelection }}39 >40 {fetching || shopFetching ? (41 <Spinner size="large" />42 ) : (43 <Layout>44 <Layout.Section>45 <Select46 label="Product for pre-purchase offer"47 placeholder="-No product selected-"48 options={productOptions}49 onChange={setSelectedProduct}50 value={selectedProduct}51 />52 </Layout.Section>53 <Layout.Section>54 <FooterHelp>55 <p>56 Powered by{" "}57 <Link url="https://gadget.dev" external>58 gadget.dev59 </Link>60 </p>61 </FooterHelp>62 </Layout.Section>63 </Layout>64 )}65 </Page>66 );67};6869export default ShopPage;
Most of this code is just Polaris and React - the Select
component is used to display a dropdown that merchants can use to select what product is offered to shoppers during checkout.

Your Gadget app's API client is already set up in frontend/api.js
and handles auth and session token management for you. This client is imported into ShopPage.jsx
and used alongside the useFindFirst
and useFindMany
React hooks to read product data from your app's API. Both useFindFirst
and useFindMany
are imported from the @gadgetinc/react
package. These are hooks provided by Gadget and allow for a reactive way of interacting with your Gadget app's API.
useFindMany to read product data
The useFindMany
hook is used to read Shopify product data that has been synced to Gadget:
frontend/ShopPage.jsxjsxconst [{ data, fetching, error }] = useFindMany(api.shopifyProduct);
The returned properties are then used to handle the request's response reactively. The data
property will contain product data returned from this request. Note that pagination is not implemented here, so 50 Shopify Product records will be returned by default, up to a maximum of 250 without pagination. The fetching
property is a boolean that can be used to display a loading state while data is being retrieved. And finally, error
contains any error information that can be handled or displayed to the user.
useFindFirst to read shop data
To retrieve the current shop id, a useFindFirst
hook is used. Because shop tenancy is automatically handled for Shopify models when you connect your Gadget app to Shopify, you will only read a single shop's data from inside an embedded admin. This default filter can be modified in tenancy/shopifyShop.gelly
, but it isn't recommended!
frontend/ShopPage.jsxjsxconst [{ data: shopData, fetching: shopFetching, error: shopFetchError }] = useFindFirst(api.shopifyShop. {select: {id: true,}});
Because the ID field on the Shopify Shop model is the only required field, a select query is added to the hook so only the ID field is returned.
Your embedded frontend app is almost complete. It is missing one crucial piece: writing the selected product back to Gadget.
Save product reference metafield
There is one thing missing from your embedded frontend app - you need to be able to save the selected product somewhere so that it can be fed into the checkout UI extension. The best way to do this is to store the product ID inside a metafield. To save this metafield, you will create a new action on the Shopify Shop model and write some custom code to save the product ID to a metafield in Shopify. You will also add a new field to the Shopify Shop so the metafield data is synced to Gadget.
Add save action and custom code
- Go to the Shopify Shop Model page
- Click + in the ACTIONS section of the model page and name the new action
savePrePurchaseProduct

- Click the Add Code Snippet button to add a new Success Effect
- Paste the following code snippet into the generated code file
shopifyShop/savePrePurchaseProduct/onSavePrePurchaseProduct.jsJavaScript1/**2 * Effect code for savePrePurchaseProduct on Shopify Shop3 * @param { import("gadget-server").SavePrePurchaseProductShopifyShopActionContext } context - Everything for running this effect, like the api client, current record, params, etc. More on effect context: https://docs.gadget.dev/guides/extending-with-code#effect-context4 */5module.exports = async ({ api, record, params, logger, connections }) => {6 // get product id from params7 const { productId } = params;89 // save the selected pre-purchase product in a SHOP owned metafield10 const response = await connections.shopify.current?.graphql(11 `mutation setMetafield($metafields: [MetafieldsSetInput!]!) {12 metafieldsSet(metafields: $metafields) {13 metafields {14 id15 value16 ownerType17 key18 namespace19 }20 userErrors {21 field22 message23 }24 }25 }`,26 {27 metafields: [28 {29 key: "pre-purchase-product",30 namespace: "gadget-tutorial",31 ownerId: `gid://shopify/Shop/${record.id}`,32 type: "product_reference",33 value: `gid://shopify/Product/${productId}`,34 },35 ],36 }37 );3839 // print to the Gadget Logs40 logger.info({ response }, "add metafields response");41};4243// define a custom parameter for the id of the product selected by merchants44module.exports.params = {45 productId: { type: "string" },46};
This code file is writing a metafield to Shopify using Shopify's metafieldsSet
GraphQL API. The ownerId
is set to the current Shopify shop, so each shop that installs your app can have a unique pre-purchase-product
metafield.
Input parameters, which are available in the params
arg in code effects, are defined with the module.exports.params
declaration at the bottom of the file. Here, you are declaring a new string parameter called productId
. This param is then used here: const { productId } = params;
.
Add metafield to Shopify Shop model
This metafield can also be added to your Shopify Shop model. In this app, the synced metafield is only used for testing, but it could also be used to display the currently selected product in the admin app if you want to add that on your own.
- Go to the Shopify Shop Model page
- Click + in the FIELDS section of the model page and name the new field
Pre-Purchase Product
- Check the Store data from Shopify Metafield option
- Enter the metafield's namespace:
gadget-tutorial
- Click the Register Namespace button
- Enter your metafield's key
pre-purchase-product
- Select the Product Reference metafield type

Your metafield is now set up in Gadget and you are subscribed to any metafield changes.
Update permissions on new action
You've created your new action and are almost ready to test it out! If you hook up the savePrePurchaseProduct
action to your frontend now your request will return an error: GGT_PERMISSION_DENIED
. By default, all custom actions (or CRUD actions for custom models) will not have permission granted to the Shopify App User access role, which is the default role used when making requests to Gadget from an embedded app.
To enable permission:
- Go to Settings -> Roles & Permissions
- Find the
savePrePurchaseProduct
action in the Shopify Shop model - Give the Shopify App User role permission to access
savePrePurchaseProduct
by clicking the checkbox

You can now call your new action from the frontend without errors! The next step is to wire it up to the frontend.
Call new action from frontend
Return to your frontend/ShopPage.jsx
file. To call your new action from the frontend, you can use another Gadget React hook: useAction
.
- Import the
useAction
hook from@gadgetinc/react
frontend/ShopPage.jsxjsx// add useAction to this import statementimport { useFindFirst, useFindMany, useAction } from "@gadgetinc/react";
- Paste the following line of code into
frontend/ShopPage.jsx
:
frontend/ShopPage.jsxjsx1/** imports */2const ShopPage = () => {3 /* useFindMany and useFindFirst hooks */45 // the useAction Gadget React hook is used to call the savePrePurchaseProduct action on the Shopify Shop model6 const [{ data: saveResponse, fetching: saving, error: saveError }, saveProduct] = useAction(api.shopifyShop.savePrePurchaseProduct);78 /* useEffect for setting product options */910 return (/* return block */)11}
Similar to the useFindFirst
and useFindMany
hooks, useAction
gives you a response object you can use to determine what to render in your frontend. The second returned parameter from useAction is a function that can be used to call your Gadget app's API. In this case, it is called saveProduct
.
- Modify the
saveSelection
callback so that it callssaveProduct
:
frontend/ShopPage.jsxjsx1// a useCallback hook that calls the savePrePurchaseProduct action on the Shopify Shop model to save the selected product2const saveSelection = useCallback(() => {3 if (shopData && selectedProduct) {4 void saveProduct({ id: shopData.id, productId: selectedProduct });5 }6}, [selectedProduct, shopData]);
Now when you select a product and click Save in your admin app, the product ID will be saved to a metafield!
You can also use the fetching
param, aliased as saving
from the useAction hook to disable the Page primaryAction button and Select components, and the data
param, aliased as saveResponse
to show a success banner.
- Replace the contents of the return statement with the following JSX snippet:
frontend/ShopPage.jsxjsx1return (2 <Page3 title="Select product for pre-purchase offer"4 primaryAction={{ content: "Save", disabled: !selectedProduct || saving, onAction: saveSelection }}5 >6 {fetching || shopFetching ? (7 <Spinner size="large" />8 ) : (9 <Layout>10 {saveResponse && (11 <Layout.Section>12 <Banner title="Pre-purchase product saved!" status="success" />13 </Layout.Section>14 )}15 <Layout.Section>16 <Select17 label="Product for pre-purchase offer"18 placeholder="-No product selected-"19 options={productOptions}20 onChange={setSelectedProduct}21 value={selectedProduct}22 disabled={saving}23 />24 </Layout.Section>25 <Layout.Section>26 <FooterHelp>27 <p>28 Powered by{" "}29 <Link url="https://gadget.dev" external>30 gadget.dev31 </Link>32 </p>33 </FooterHelp>34 </Layout.Section>35 </Layout>36 )}37 </Page>38);
You are done with the embedded app and backend. Now the only thing left to do is build the checkout UI extension.
Build a pre-purchase checkout UI extension
To generate any Shopify extension, you need to create a new Shopify CLI app. The Shopify CLI app will simply be used to manage your extensions, the rest of your app is built in Gadget. These setup steps can be used for any Shopify extension, including Checkout UI extensions and Functions!
Checkout UI extensions will not work unless you have the checkout extensibility developer preview enabled on your development store. To learn how to enable the checkout extensibility preview, see Shopify's documentation.
- Create a new Shopify CLI app by running the following command in your local terminal
- Once the app is generated,
cd
into the app's root - From the app's root, delete the
web
folder
terminalbashrm -rf web/
- Generate a checkout UI extension
npm run shopify app generate extension -- --type checkout_ui --name=pre-purchase-ext
yarn shopify app generate extension --type checkout_ui --name=pre-purchase-ext
Make sure you connect to the same Partners app you used to set up your Shopify connection in Gadget, and select the same development store you installed your app on.
The extension code sample below is written in JavaScript React.
This creates an extensions
folder at the root of your CLI app. A pre-purchase-ext
folder containing your extension code will also be created. Shopify checkout UI extensions have two files that you will need to edit, a configuration file: shopify.ui.extension.toml
and the extension source: src/index.jsx
. Both of these files will require some changes to pull in your metafield as input.
Checkout UI extensions have their own API and set of components, separate from the Admin API and Polaris component library. More details about different components and endpoints can be found in Shopify's Checkout UI extension API docs.
Modify the configuration file
The first thing you need to do is modify your extension's shopify.ui.extension.toml
file. You need to define your metafield as input and allow access to the Storefront API.
- Paste the following into
shopify.ui.extension.toml
:
extensions/pre-purchase-ext/shopify.ui.extension.tomltoml1type = "checkout_ui_extension"2name = "pre-purchase-ext"34extension_points = [5 'Checkout::Dynamic::Render'6]78[[metafields]]9namespace = "gadget-tutorial"10key = "pre-purchase-product"1112[capabilities]13api_access = true
The [[metafields]] definition allows you to pull in the metafield as input to your extension. The api_access
[capabilities] setting was also enabled, which allows you to query using the Storefront API.
Write checkout UI extension code
Now for the extension itself. This borrows heavily from Shopify's own pre-purchase tutorial, with a couple small modifications. The entire src/index.jsx
code file is provided, with additional details provided below the snippet.
- Paste the following into your extension's
src/index.jsx
file:
extensions/pre-purchase-ext/src/index.jsxjsx1import React, { useEffect, useState } from "react";2import {3 render,4 Divider,5 Image,6 Banner,7 Heading,8 Button,9 InlineLayout,10 BlockStack,11 Text,12 SkeletonText,13 SkeletonImage,14 useCartLines,15 useApplyCartLinesChange,16 useExtensionApi,17 useAppMetafields,18} from "@shopify/checkout-ui-extensions-react";1920// Set up the entry point for the extension21render("Checkout::Dynamic::Render", () => <App />);2223// The function that will render the app24function App() {25 // Use `query` for fetching product data from the Storefront API, and use `i18n` to format26 // currencies, numbers, and translate strings27 const { query, i18n } = useExtensionApi();28 // Get a reference to the function that will apply changes to the cart lines from the imported hook29 const applyCartLinesChange = useApplyCartLinesChange();3031 // get passed in metafield32 const [prePurchaseProduct] = useAppMetafields();3334 // Set up the states35 const [product, setProduct] = useState(null);36 const [loading, setLoading] = useState(false);37 const [adding, setAdding] = useState(false);38 const [showError, setShowError] = useState(false);3940 // On initial load, fetch the product variants41 useEffect(() => {42 if (prePurchaseProduct) {43 // Set the loading state to show some UI if you're waiting44 setLoading(true);45 // Use `query` api method to send graphql queries to the Storefront API46 query(47 `query ($id: ID!) {48 product(id: $id) {49 id50 title51 images(first:1){52 nodes {53 url54 }55 }56 variants(first: 1) {57 nodes {58 id59 price {60 amount61 }62 }63 }64 }65 }`,66 {67 variables: { id: prePurchaseProduct.metafield.value },68 }69 )70 .then(({ data }) => {71 // Set the `product` so that you can reference it72 setProduct(data.product);73 })74 .catch((error) => console.error(error))75 .finally(() => setLoading(false));76 }77 }, [prePurchaseProduct]);7879 // If an offer is added and an error occurs, then show some error feedback using a banner80 useEffect(() => {81 if (showError) {82 const timer = setTimeout(() => setShowError(false), 3000);83 return () => clearTimeout(timer);84 }85 }, [showError]);8687 // Access the current cart lines and subscribe to changes88 const lines = useCartLines();8990 // Show a loading UI if you're waiting for product variant data91 // Use Skeleton components to keep placement from shifting when content loads92 if (loading) {93 return (94 <BlockStack spacing="loose">95 <Divider />96 <Heading level={2}>You might also like</Heading>97 <BlockStack spacing="loose">98 <InlineLayout spacing="base" columns={[64, "fill", "auto"]} blockAlignment="center">99 <SkeletonImage aspectRatio={1} />100 <BlockStack spacing="none">101 <SkeletonText inlineSize="large" />102 <SkeletonText inlineSize="small" />103 </BlockStack>104 <Button kind="secondary" disabled={true}>105 Add106 </Button>107 </InlineLayout>108 </BlockStack>109 </BlockStack>110 );111 }112 // If product variants can't be loaded, then show nothing113 if (!loading && !product) {114 return null;115 }116117 // Get the IDs of all product variants in the cart118 const cartLineProductVariantIds = lines.map((item) => item.merchandise.id);119120 // check to see if the product is already in the cart121 const productInCart = !!product.variants.nodes.some(({ id }) => cartLineProductVariantIds.includes(id));122123 // If the product is in the cart, then don't show the offer124 if (productInCart) {125 return null;126 }127128 // Choose the first available product variant on offer129 const { images, title, variants } = product;130131 // Localize the currency for international merchants and customers132 const renderPrice = i18n.formatCurrency(variants.nodes[0].price.amount);133134 // Use the first product image or a placeholder if the product has no images135 const imageUrl =136 images.nodes[0]?.url ??137 "https://cdn.shopify.com/s/files/1/0533/2089/files/placeholder-images-image_medium.png?format=webp&v=1530129081";138139 return (140 <BlockStack spacing="loose">141 <Divider />142 <Heading level={2}>You might also like</Heading>143 <BlockStack spacing="loose">144 <InlineLayout145 spacing="base"146 // Use the `columns` property to set the width of the columns147 // Image: column should be 64px wide148 // BlockStack: column, which contains the title and price, should "fill" all available space149 // Button: column should "auto" size based on the intrinsic width of the elements150 columns={[64, "fill", "auto"]}151 blockAlignment="center"152 >153 <Image border="base" borderWidth="base" borderRadius="loose" source={imageUrl} description={title} aspectRatio={1} />154 <BlockStack spacing="none">155 <Text size="medium" emphasis="strong">156 {title}157 </Text>158 <Text appearance="subdued">{renderPrice}</Text>159 </BlockStack>160 <Button161 kind="secondary"162 loading={adding}163 accessibilityLabel={`Add ${title} to cart`}164 onPress={async () => {165 setAdding(true);166 // Apply the cart lines change167 const result = await applyCartLinesChange({168 type: "addCartLine",169 merchandiseId: variants.nodes[0].id,170 quantity: 1,171 });172 setAdding(false);173 if (result.type === "error") {174 // An error occurred adding the cart line175 // Verify that you're using a valid product variant ID176 // For example, 'gid://shopify/ProductVariant/123'177 setShowError(true);178 console.error(result.message);179 }180 }}181 >182 Add183 </Button>184 </InlineLayout>185 </BlockStack>186 {showError && <Banner status="critical">There was an issue adding this product. Please try again.</Banner>}187 </BlockStack>188 );189}
Shopify goes though a step-by-step build of the extension in their tutorial. The differences will be highlighted here. Notice that none of the extension code is Gadget-specific! When working with Shopify checkout UI extensions, you are largely working completely in the Shopify ecosystem.
To pull in your metafield, the useAppMetafields()
function is called. This, along with all imported components and APIs, is imported from the @shopify/checkout-ui-extensions-react
package:
extensions/pre-purchase-ext/src/index.jsxjsxconst [prePurchaseProduct] = useAppMetafields();
This metafield value, which contains the product ID, is then used to pull the first product variant and first product image available using the Storefront API. The query is wrapped in a useEffect
with prePurchaseProduct
as the input. The resulting data is then saved to the product
state.
extensions/pre-purchase-ext/src/index.jsxjsx1// On initial load, fetch the product variants2useEffect(() => {3 if (prePurchaseProduct) {4 // Set the loading state to show some UI if you're waiting5 setLoading(true);6 // Use `query` api method to send graphql queries to the Storefront API7 query(8 `query ($id: ID!) {9 product(id: $id) {10 id11 title12 images(first:1){13 nodes {14 url15 }16 }17 variants(first: 1) {18 nodes {19 id20 price {21 amount22 }23 }24 }25 }26 }`,27 {28 variables: { id: prePurchaseProduct.metafield.value },29 }30 )31 .then(({ data }) => {32 // Set the `product` so that you can reference it33 setProduct(data.product);34 })35 .catch((error) => console.error(error))36 .finally(() => setLoading(false));37 }38}, [prePurchaseProduct]);
There is a good chance you want to pick a particular variant and image in the Shopify admin app when selecting a product for pre-purchase. Or maybe the customer can pick a variant! All this can be done with additional metafields or additional code in the checkout UI extension.
There is some code to check if the product is already in the cart. If it is, the pre-purchase offer is skipped. A null
return value simply means that the extension will not be rendered in the checkout:
extensions/pre-purchase-ext/src/index.jsxjsx1// Get the IDs of all product variants in the cart2const cartLineProductVariantIds = lines.map((item) => item.merchandise.id);34// check to see if the product is already in the cart5const productInCart = !!product.variants.nodes.some(({ id }) => cartLineProductVariantIds.includes(id));67// If the product is in the cart, then don't show the offer8if (productInCart) {9 return null;10}
Finally, the last block of code is the return statement that will render the UI extension. The most interesting block of code is the onPress
function param on the Button component. It uses the applyCartLinesChange
function to add the additional product variant to the cart.
extensions/pre-purchase-ext/src/index.jsxjsx1onPress={async () => {2 setAdding(true);3 // Apply the cart lines change4 const result = await applyCartLinesChange({5 type: "addCartLine",6 merchandiseId: variants.nodes[0].id,7 quantity: 1,8 });9 setAdding(false);10 if (result.type === "error") {11 // An error occurred adding the cart line12 // Verify that you're using a valid product variant ID13 // For example, 'gid://shopify/ProductVariant/123'14 setShowError(true);15 console.error(result.message);16 }17}}
Test your extension
Now that you've built your extension, you need to test it out in a checkout.
- Start the CLI app and extension by running
npm run dev
oryarn dev
from your CLI app's root- If you haven't already, make sure you select the same development store you used to connect your Gadget app to Shopify!
- Open the Preview URL to access the Shopify Developer Console and open the provided Checkout UI extension URL
- Your extension should be previewed in the checkout!

Deploy your extension
Shopify hosts all checkout UI extension code for you. You don't need to set up hosting yourself, but you do need to deploy your extension to Shopify's infrastructure.
- Stop the
dev
command you ran in the previous step and runnpm run deploy
oryarn deploy
to push your extension to Shopify - Go to your app's Extension page in the Partners dashboard
- Click on your extension
- Click Create version to create a new version of your extension and the Create button in the modal
- You can select either Minor version or Major version for this tutorial
- Enable the development store preview
- Click Publish on your new extension version in the Version history to publish your extension

Now you need to place your extension in the checkout so it is visible for shoppers!
- Go to your development store
- Click Settings -> Checkout -> and then the Customize button for the current theme

- Click Add app in the bottom of the right panel of the checkout editor
- Select your extension, and drag and drop it in the checkout editor to place it
- Click Save in the top right corner of the screenshot

You should now be able to see your extension when you go through your development store's checkout!
Congrats! You have successfully built a fullstack pre-purchase app that includes an admin-embedded frontend and checkout UI extension!
Next steps
Have questions about the tutorial? Join Gadget's developer Discord to ask Gadget employees and join the Gadget developer community!
Want to learn more about data modeling and writing custom code effects in Gadget? Try out the product tagger tutorial:
Build an embedded Shopify application that automatically tags products in a Shopify store based on description keywords.