Build a pre-purchase Shopify checkout UI extension
Expected time: 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
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.
Now we will set up a custom Shopify application in the Partners dashboard.
- Go to the Shopify Partner 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
- 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 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.
- Select the scopes and models listed below and click Confirm to connect to the custom Shopify app
- Enable the read scope for the Shopify Products API, and select the underlying Product model that we want to import into Gadget
Now we want to connect our Gadget app to our custom app in the Partner dashboard.
- In your Shopify app in the Partner 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.
- Go back to the Shopify Partner 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
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 sync Shopify data into our Gadget app database.
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
api/models/shopifyProduct/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
api/models/shopifyShop/schema
- Click + in the FIELDS section of the schema page and give the field the identifier
prePurchaseProduct
- Check the Store data from Shopify Metafield option
- Enter the metafield namespace:
gadget-tutorial
- Click the Register Namespace button
- Enter your metafield 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 + in the
api/models/shopifyShop/actions
folder to create a new action and name the new filesavePrePurchaseProduct.js
- Paste the following code snippet into
api/models/shopifyShop/actions/savePrePurchaseProduct.js
to update theonSuccess
function of the action:
api/models/shopifyShop/actions/savePrePurchaseProduct.jsJavaScript1import {2 applyParams,3 ActionOptions,4 SavePrePurchaseProductShopifyShopActionContext,5} from "gadget-server";6import { preventCrossShopDataAccess } from "gadget-server/shopify";78/**9 * @param { SavePrePurchaseProductShopifyShopActionContext } context10 */11export async function run({ params, record, logger, connections }) {12 applyParams(params, record);13 await preventCrossShopDataAccess(params, record);1415 // get the product id passed in as a custom param16 const { productId } = params;1718 // save the selected pre-purchase product in a SHOP-owned metafield19 // https://www.npmjs.com/package/shopify-api-node#metafields20 const response = await connections.shopify.current?.metafield.create({21 key: "pre-purchase-product",22 namespace: "gadget-tutorial",23 owner_id: record.id,24 type: "product_reference",25 value: productId,26 });2728 // print to the Gadget Logs29 logger.info({ response }, "add metafields response");30}3132// define a productId custom param for this action33export const params = {34 productId: { type: "string" },35};3637/** @type { ActionOptions } */38export const options = {39 actionType: "custom",40 triggers: { api: true },41};
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 modify the access control settings for your new action:
- Check the
shopify-app-users
box in the ACCESS CONTROL section to the right of the editor
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
web/routes/index.jsx
:
web/routes/index.jsxjsx1import { useFindFirst, useFindMany, useActionForm, Controller } from "@gadgetinc/react";2import { Banner, FooterHelp, Layout, Link, Page, Select, Spinner, Form, Button, FormLayout, SkeletonDisplayText } from "@shopify/polaris";3import { api } from "../api";4import { useEffect, useState } from "react";56const PrePurchaseForm = ({ products, shop }) => {7 // useActionForm used to handle form state and submission8 const { submit, control, formState, error, setValue, watch } = useActionForm(api.shopifyShop.savePrePurchaseProduct, {9 findBy: shop.id,10 select: {11 id: true,12 prePurchaseProduct: true,13 },14 // send productId as a custom param15 send: ["id", "productId"],16 });1718 // use watch to listen for updates to the form state19 const updateProductId = watch("shopifyShop.prePurchaseProduct");20 // save as productId value in form state to send custom param21 useEffect(() => {22 setValue("productId", updateProductId);23 }, [updateProductId]);2425 return (26 <Form onSubmit={submit}>27 <FormLayout>28 {formState?.isSubmitSuccessful && <Banner title="Pre-purchase product saved!" tone="success" />}29 {error && (30 <Banner title="Error saving selection" tone="critical">31 {error.message}32 </Banner>33 )}34 {formState?.isLoading ? (35 <SkeletonDisplayText size="large" />36 ) : (37 <Controller38 name="shopifyShop.prePurchaseProduct"39 control={control}40 required41 render={({ field }) => {42 const { ref, ...fieldProps } = field;43 return (44 <Select45 label="Product for pre-purchase offer"46 placeholder="-No product selected-"47 options={products}48 disabled={formState.isSubmitting}49 {...fieldProps}50 />51 );52 }}53 />54 )}5556 <Button submit disabled={formState.isSubmitting} variant="primary">57 Save58 </Button>59 </FormLayout>60 </Form>61 );62};6364export default function () {65 // use React state to handle selected product and options66 const [productOptions, setProductOptions] = useState([]);6768 // use the Gadget React hooks to fetch products as options for Select component69 const [{ data: products, fetching: productsFetching, error: productsFetchError }] = useFindMany(api.shopifyProduct);70 // get the current shop id (shop tenancy applied automatically, only one shop available)71 const [{ data: shopData, fetching: shopFetching, error: shopFetchError }] = useFindFirst(api.shopifyShop, {72 select: {73 id: true,74 },75 });7677 // a React useEffect hook to build product options for the Select component78 useEffect(() => {79 if (products) {80 const options = products.map((product) => ({81 value: `gid://shopify/Product/${product.id}`,82 label: product.title,83 }));84 setProductOptions(options);85 }86 }, [products]);8788 return (89 <Page title="Select product for pre-purchase offer">90 {productsFetching || shopFetching || productOptions.length === 0 ? (91 <Spinner size="large" />92 ) : (93 <Layout>94 {(productsFetchError || shopFetchError) && (95 <Layout.Section>96 <Banner title="Error loading data" tone="critical">97 {productsFetchError?.message || shopFetchError?.message}98 </Banner>99 </Layout.Section>100 )}101 <Layout.Section>102 <PrePurchaseForm shop={shopData} products={productOptions} />103 </Layout.Section>104 <Layout.Section>105 <FooterHelp>106 <p>107 Powered by{" "}108 <Link url="https://gadget.dev" external>109 gadget.dev110 </Link>111 </p>112 </FooterHelp>113 </Layout.Section>114 </Layout>115 )}116 </Page>117 );118}
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 web/api.js
and handles auth and session token management for you. This client is imported into index.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 requestfetching
is a boolean that can be used to display a loading state while data is being retrievederror
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:
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.
More docs on useActionForm
and the Controller
component can be found in the @gadgetinc/react
reference docs.
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
Now you can build a checkout UI extension to offer the product to buyers during checkout. Shopify hosts all extensions on their infrastructure, so you only need to write the extension code and deploy it. Thankfully, you can build your extensions in your Gadget project.
- Open the Gadget command palette with P or Ctrl P
- Type
>
to enable terminal mode - Run the following command:
Run in the Gadget command paletteyarn add -D @shopify/cli
Once the install is finished, modify your package.json
file:
- Add a
workspaces
field with the value["extensions/*"]
, and atrustedDependencies
field with the value["@shopify/plugin-cloudflare"]
to the root of yourpackage.json
file:
package.jsonjson{"workspaces": ["extensions/*"],"trustedDependencies": ["@shopify/plugin-cloudflare"]}
- Create an empty
shopify.app.toml
file at the root level of your project - Add additional scripts to your
package.json
file. You can paste this over the existingscripts
:
package.jsonjson1{2 "scripts": {3 "vite:build": "NODE_ENV=production vite build",4 "shopify": "shopify",5 "dev": "shopify app dev",6 "info": "shopify app info",7 "generate": "shopify app generate",8 "deploy": "shopify app deploy"9 }10}
Your package.json should look something like this:
package.jsonjson1{2 "name": "<your-app-package>",3 "version": "0.1.0",4 "description": "Internal package for Gadget app <your-app-package> (Development environment)",5 "license": "UNLICENSED",6 "private": true,7 "scripts": {8 "vite:build": "NODE_ENV=production vite build",9 "shopify": "shopify",10 "dev": "shopify app dev",11 "info": "shopify app info",12 "generate": "shopify app generate",13 "deploy": "shopify app deploy"14 },15 "dependencies": {16 "@gadget-client/<your-app-package>": "link:.gadget/client",17 "@gadgetinc/react": "^0.15.4",18 "@gadgetinc/react-shopify-app-bridge": "^0.14.2",19 "@shopify/app-bridge-react": "4.0.0",20 "@shopify/polaris": "^12.0.0",21 "@shopify/polaris-icons": "^8.1.0",22 "gadget-server": "link:.gadget/server",23 "react": "^18.2.0",24 "react-dom": "^18.2.0",25 "react-router-dom": "6.15.0",26 "shopify-api-node": "^3.12.6"27 },28 "devDependencies": {29 "@shopify/app": "^3.57.1",30 "@shopify/cli": "^3.57.1",31 "@types/node": "^20.8.4",32 "@types/react": "^18.0.28",33 "@types/react-dom": "^18.0.11",34 "@vitejs/plugin-react-swc": "3.2.0",35 "typescript": "^5.4.5",36 "vite": "^4.4.9"37 },38 "workspaces": ["extensions/*"],39 "trustedDependencies": ["@shopify/plugin-cloudflare"]40}
Now pull down your Gadget project to your local machine to start building your extension. You can use ggt
, Gadget's CLI to work on your Gadget app locally.
- In your local terminal, run the following command replacing
<YOUR APP DOMAIN>
:
terminalnpx ggt@latest dev ./<YOUR APP DOMAIN> --app=<YOUR APP DOMAIN> --env=development
You can also click the cloud icon next to your environment selector in the Gadget editor to get your ggt command. See the ggt guide for more info on working locally.
ggt dev
will run in the background. It pushes any code changes you make locally to your hosted Gadget environment, and pulls down any changes you make in the Gadget editor to your local machine. Make sure you leave it running while you work on your extension in your local editor.
cd
into your project, and open it in an editor- Add a
.ignore
file to the root of your project and add the following:
.ignoreextensions/*/distextensions/*/node_modules
- Use
yarn
to generate your checkout UI extension:
terminalyarn generate extension --template checkout_ui --name=pre-purchase-ext
- Select the same Partner app and development store you used to connect to Shopify when prompted by Shopify's CLI
- Select JavaScript React when prompted for the extension language
This command will generate an extensions
folder at your project root, and your extension will be generated by Shopify.
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.
- Paste the following into
extensions/pre-purchase-ext/shopify.extension.toml
:
extensions/pre-purchase-ext/shopify.extension.tomltoml1# Learn more about configuring your checkout UI extension:2# https://shopify.dev/api/checkout-extensions/checkout/configuration34# The version of APIs your extension will receive. Learn more:5# https://shopify.dev/docs/api/usage/versioning6api_version = "2024-01"78[[extensions]]9type = "ui_extension"10name = "pre-purchase-ext"11handle = "pre-purchase-ext"1213# Controls where in Shopify your extension will be injected,14# and the file that contains your extension's source code. Learn more:15# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/targets-overview1617[[extensions.targeting]]18module = "./src/Checkout.jsx"19target = "purchase.checkout.block.render"2021[extensions.capabilities]22# Gives your extension access to directly query Shopify's storefront API.23# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#api-access24api_access = true2526# Loads metafields on checkout resources, including the cart,27# products, customers, and more. Learn more:28# https://shopify.dev/docs/api/checkout-ui-extensions/unstable/configuration#metafields2930[[extensions.metafields]]31namespace = "gadget-tutorial"32key = "pre-purchase-product"
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 of 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
extensions/pre-purchase-ext/src/Checkout.jsx
file:
extensions/pre-purchase-ext/src/Checkout.jsxjsx1import {2 Banner,3 BlockStack,4 Button,5 Divider,6 Heading,7 Image,8 InlineLayout,9 SkeletonImage,10 SkeletonText,11 Text,12 reactExtension,13 useApi,14 useAppMetafields,15 useApplyCartLinesChange,16 useCartLines,17} from "@shopify/ui-extensions-react/checkout";18import { useEffect, useState } from "react";1920export default reactExtension("purchase.checkout.block.render", () => <Extension />);2122function Extension() {23 // Use `query` for fetching product data from the Storefront API, and use `i18n` to format24 // currencies, numbers, and translate strings25 const { query, i18n } = useApi();26 // Get a reference to the function that will apply changes to the cart lines from the imported hook27 const applyCartLinesChange = useApplyCartLinesChange();2829 // get passed in metafield30 const [prePurchaseProduct] = useAppMetafields();3132 // Set up the states33 const [product, setProduct] = useState(null);34 const [loading, setLoading] = useState(false);35 const [adding, setAdding] = useState(false);36 const [showError, setShowError] = useState(false);3738 // On initial load, fetch the product variants39 useEffect(() => {40 if (prePurchaseProduct) {41 // Set the loading state to show some UI if you're waiting42 setLoading(true);43 // Use `query` api method to send graphql queries to the Storefront API44 query(45 `query ($id: ID!) {46 product(id: $id) {47 id48 title49 images(first:1){50 nodes {51 url52 }53 }54 variants(first: 1) {55 nodes {56 id57 price {58 amount59 }60 }61 }62 }63 }`,64 {65 variables: { id: prePurchaseProduct.metafield.value },66 }67 )68 .then(({ data }) => {69 // Set the `product` so that you can reference it70 setProduct(data.product);71 })72 .catch((error) => console.error(error))73 .finally(() => setLoading(false));74 }75 }, [prePurchaseProduct]);7677 // If an offer is added and an error occurs, then show some error feedback using a banner78 useEffect(() => {79 if (showError) {80 const timer = setTimeout(() => setShowError(false), 3000);81 return () => clearTimeout(timer);82 }83 }, [showError]);8485 // Access the current cart lines and subscribe to changes86 const lines = useCartLines();8788 // Show a loading UI if you're waiting for product variant data89 // Use Skeleton components to keep placement from shifting when content loads90 if (loading) {91 return (92 <BlockStack spacing="loose">93 <Divider />94 <Heading level={2}>You might also like</Heading>95 <BlockStack spacing="loose">96 <InlineLayout spacing="base" columns={[64, "fill", "auto"]} blockAlignment="center">97 <SkeletonImage aspectRatio={1} />98 <BlockStack spacing="none">99 <SkeletonText inlineSize="large" />100 <SkeletonText inlineSize="small" />101 </BlockStack>102 <Button kind="secondary" disabled={true}>103 Add104 </Button>105 </InlineLayout>106 </BlockStack>107 </BlockStack>108 );109 }110 // If product variants can't be loaded, then show nothing111 if (!loading && !product) {112 return null;113 }114115 // Get the IDs of all product variants in the cart116 const cartLineProductVariantIds = lines.map((item) => item.merchandise.id);117118 // check to see if the product is already in the cart119 const productInCart = !!product.variants.nodes.some(({ id }) => cartLineProductVariantIds.includes(id));120121 // If the product is in the cart, then don't show the offer122 if (productInCart) {123 return null;124 }125126 // Choose the first available product variant on offer127 const { images, title, variants } = product;128129 // Localize the currency for international merchants and customers130 const renderPrice = i18n.formatCurrency(variants.nodes[0].price.amount);131132 // Use the first product image or a placeholder if the product has no images133 const imageUrl =134 images.nodes[0]?.url ??135 "https://cdn.shopify.com/s/files/1/0533/2089/files/placeholder-images-image_medium.png?format=webp&v=1530129081";136137 return (138 <BlockStack spacing="loose">139 <Divider />140 <Heading level={2}>You might also like</Heading>141 <BlockStack spacing="loose">142 <InlineLayout143 spacing="base"144 // Use the `columns` property to set the width of the columns145 // Image: column should be 64px wide146 // BlockStack: column, which contains the title and price, should "fill" all available space147 // Button: column should "auto" size based on the intrinsic width of the elements148 columns={[64, "fill", "auto"]}149 blockAlignment="center"150 >151 <Image border="base" borderWidth="base" borderRadius="loose" source={imageUrl} description={title} aspectRatio={1} />152 <BlockStack spacing="none">153 <Text size="medium" emphasis="strong">154 {title}155 </Text>156 <Text appearance="subdued">{renderPrice}</Text>157 </BlockStack>158 <Button159 kind="secondary"160 loading={adding}161 accessibilityLabel={`Add ${title} to cart`}162 onPress={async () => {163 setAdding(true);164 // Apply the cart lines change165 const result = await applyCartLinesChange({166 type: "addCartLine",167 merchandiseId: variants.nodes[0].id,168 quantity: 1,169 });170 setAdding(false);171 if (result.type === "error") {172 // An error occurred adding the cart line173 // Verify that you're using a valid product variant ID174 // For example, 'gid://shopify/ProductVariant/123'175 setShowError(true);176 console.error(result.message);177 }178 }}179 >180 Add181 </Button>182 </InlineLayout>183 </BlockStack>184 {showError && <Banner status="critical">There was an issue adding this product. Please try again.</Banner>}185 </BlockStack>186 );187}
Shopify goes through a step-by-step build of the extension in their tutorial. The differences will be highlighted here. 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.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/Checkout.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 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.
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.
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
yarn dev
from your CLI app's root - When prompted, 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
You should be able to see the extension in the checkout UI.
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 runyarn deploy
to publish your extension to Shopify
terminalyarn deploy
- When prompted, select Yes, always to include the
shopify.app.toml
configuration ondeploy
- Select Yes when prompted to release a new version of the extension
Now you need to place your extension in the checkout so it is visible for shoppers!
- Go to your development store
- Click Settings in the store admin
- Click on the Checkout option
- Click the Customize button for the current theme
- Click Add app block in the bottom of the Information 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 checkout editor
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!
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: