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.
Requirements
To get the most out of this tutorial, you will need:
After setting up your connection, you also need to build, test, and deploy your
checkout UI extension
Step 1: 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 and select the Shopify app template.
Because we are adding an embedded frontend, we are going to connect to Shopify using the Partners connection.
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
Click on Settings in the side nav bar
Click on Plugins in the modal that opens
Select Shopify from the list of plugins and connections
Copy the Client ID and Client secret from your newly created Shopify app and paste the values into the Gadget Connections page
Click Connect 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 Configuration 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.
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
Having an issue installing?
If you are getting a permissions denied error when installing your app, try logging in to the Shopify store Admin!
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 web/routes/index.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 on the shopifyProduct Data page in Gadget. To view this page:
Click on the shopifyProduct 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 modifying your app backend and database to support adding and storing a Shopify metafield used to save the product offered to shoppers.
Step 2: Save product reference metafield
The best way to save a selected product to be offered in this extension is to store the product ID inside a metafield. You will add a new field to the shopifyShop model so the metafield data is synced to Gadget. To save this metafield, you will also create a new action on the shopifyShop model and write some custom code to save the product ID to a metafield in Shopify.
Add metafield to shopifyShop model
This metafield can also be added to your shopifyShop 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.
Click on the shopifyShop data model
Click + in the FIELDS section of the model page and give the field the identifier prePurchaseProduct
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. Setting this field as a relationship will link the metafield value to the existing record in the Gadget database.
Add save action and custom code
Click on the shopifyShop data model
Click + in the ACTIONS section of the model page and name the new action savePrePurchaseProduct
Paste the following code snippet into shopifyShop/actions/savePrePurchaseProduct.js to update the onSuccess function of the action:
shopifyShop/actions/savePrePurchaseProduct.js
JavaScript
1import{
2 applyParams,
3 preventCrossShopDataAccess,
4 save,
5ActionOptions,
6SavePrePurchaseProductShopifyShopActionContext,
7}from"gadget-server";
8
9// define a productId custom param for this action
This code file is writing a metafield to Shopify using Shopify's metafieldsSet GraphQL API. The ownerId is set to the current 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 export const params = {} declaration at the top of the file. Here, you are declaring a new string parameter called productId. This param is then used in the onSuccess function.
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 shopifyShop 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 build the frontend.
Step 3: 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:
A lot 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, useFindMany, and useAction React hooks to read and write data from your app's API.
useFindMany to read product data
The useFindMany hook is used to read Shopify product data that has been synced to Gadget:
The returned properties are then used to handle the request's response reactively:
data contains product data returned from this request
fetching is a boolean that can be used to display a loading state while data is being retrieved
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 accessControl/filters/shopify/shopifyShop.gelly, but it isn't recommended!
Because the ID field on the shopifyShop model is the only required field, a select query is added to the hook so only the ID field is returned.
useActionFrom to write metafield data
The useActionForm hook is used in the PrePurchaseForm component to manage the selected form state, and call the shopifyShop.savePrePurchaseProduct action:
frontend/ShopPage.jsx
React
1// the useActionForm Gadget React hook is used to call the savePrePurchaseProduct action on the shopifyShop model
2constPrePurchaseForm=({ products, shop })=>{
3// useActionForm used to handle form state and submission
Because we are sending a custom productId param with our action (and don't want to update prePurchaseProduct directly), we need to use the watch and setValue functions to update the productId param with the selected product ID. We also only send the id and productId fields to the action, so the prePurchaseProduct field is not updated.
You are done with the embedded app and backend. Now the only thing left to do is build the checkout UI extension.
Step 4: 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.
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.extension.toml and the extension source: src/Checkout.jsx. Both of these files will require some changes to pull in your metafield as input.
Checkout UI extension API
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.extension.toml file. You need to define your metafield as input and allow access to the Storefront API.
The [[extensions.metafields]] definition allows you to pull in the metafield as input to your extension. The api_access[extensions.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/Checkout.jsx code file is provided, with additional details provided below the snippet.
Paste the following into your extension's src/Checkout.jsx file:
173// Verify that you're using a valid product variant ID
174// For example, 'gid://shopify/ProductVariant/123'
175setShowError(true);
176console.error(result.message);
177}
178}}
179>
180 Add
181</Button>
182</InlineLayout>
183</BlockStack>
184{showError &&<Bannerstatus="critical">There was an issue adding this product. Please try again.</Banner>}
185</BlockStack>
186);
187}
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/ui-extensions-react/checkout package:
extensions/pre-purchase-ext/src/Checkout.jsx
React
const[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/Checkout.jsx
React
1// On initial load, fetch the product variants
2useEffect(()=>{
3if(prePurchaseProduct){
4// Set the loading state to show some UI if you're waiting
5setLoading(true);
6// Use `query` api method to send graphql queries to the Storefront API
32// Set the `product` so that you can reference it
33setProduct(data.product);
34})
35.catch((error)=>console.error(error))
36.finally(()=>setLoading(false));
37}
38},[prePurchaseProduct]);
Skip the Storefront API call
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/Checkout.jsx
React
1// Get the IDs of all product variants in the cart
4// check to see if the product is already in the cart
5const productInCart =!!product.variants.nodes.some(({ id })=> cartLineProductVariantIds.includes(id));
6
7// If the product is in the cart, then don't show the offer
8if(productInCart){
9returnnull;
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/Checkout.jsx
React
1onPress={async()=>{
2setAdding(true);
3// Apply the cart lines change
4const result =awaitapplyCartLinesChange({
5type:"addCartLine",
6merchandiseId: variants.nodes[0].id,
7quantity:1,
8});
9setAdding(false);
10if(result.type==="error"){
11// An error occurred adding the cart line
12// Verify that you're using a valid product variant ID
13// For example, 'gid://shopify/ProductVariant/123'
14setShowError(true);
15console.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 or yarn 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!
Extension not rendering?
Make sure the product you have selected to test the pre-purchase offer on is not already in the cart or the extension will not render!
Another possible issue: the product must have inventory available. If the product is out of stock, the extension will not render. Adjust inventory in your Shopify admin or select a different product to be offered.
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 run npm run deploy or yarn deploy to publish your extension to Shopify
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 full-stack 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 make requests to your Gadget backend from an extension?
You can install your Gadget client into Shopify extensions to make requests against your Gadget app's API. For more information, check out
our documentation.
Want to learn more about data modeling and writing custom code effects in Gadget? Try out the product tagger tutorial: