The Uptick Shopify Extensibility repository is designed to integrate seamlessly with your Shopify UI Extensibility project. It allows developers to render offers on Shopify’s Order Status and Thank You pages with minimal modifications.
To include Uptick Shopify Extensibility in your Shopify CLI project, choose one of the following methods:
Add the submodule directly to your Shopify CLI Extension project. Replace {extension-name}
in the path below with your actual extension name. Then, run the following commands from your projects root directory:
git submodule add https://github.com/uptick-ads/shopify-extensibility.git extensions/{extension-name}/src/uptick-extension git submodule update --init --recursive
Copy or reference the Api and Generator classes from the Uptick Shopify Extensibility repository in your codebase.
Note: All relative paths in the examples below assume installation was done as a submodule.
The Api service handles server communication and response processing. It can be used independently or alongside the Generator class.
Import and initialize the API service with your integration ID in the main entry component of your extension, as configured in your shopify.extension.toml
file under [[extensions.targeting]]
:
// Other imports ... import { Api as UptickApi } from "./extraction/index.js"; // Api installation const uptickApi = new UptickApi({ integrationId: "{Your Integration Id}", apiVersion: "v2" }); // Shopify extension initialization for context const orderStatusBlockRender = reactExtension("customer-account.order-status.block.render", () => <Extension />); export { orderStatusBlockRender }; function Extension() { // Checkout ui component code
Optionally, initialize the API with captureWarning
and captureException
callbacks to log errors to services like Sentry or Bugsnag. If not explicitly initialized, these will default logging to the console. These callbacks have the following signatures:
// Method signatures let captureWarning = (warningMessage, context) => {...}; let captureException = (error, context) => {...}; // Example using Sentry new UptickApi({ captureError: Sentry.captureException });
Prepare the Api by calling its setup
method and pass in any variables that can only be created within the context of the component. Here’s an example:
// ... Previous imports and initialization import { useApi } from "@shopify/ui-extensions-react/checkout"; import { useState, useEffect } from "react"; function Extension() { const shopApi = useApi(); // Shopify's api initialization const [loading, setLoading] = useState(true); // Used in your components to track the status of the Api calls. const [offer, setOffer] = useState(null); uptickApi.setup({ shopApi, setLoading });
Use the getInitialOffer
method to fetch the first offer. Pass the appropriate placement
parameter depending on where the component is rendered:
order_confirmation
: For the Thank You page.
order_status
: For the Order Status page.
function Extension() { const shopApi = useApi(); const [loading, setLoading] = useState(true); const [offer, setOffer] = useState(null); uptickApi.setup({ shopApi, setLoading }); useEffect(() => { (async () => { // Note: Only call this method once let offer = await uptickApi.getInitialOffer("order_confirmation"); setOffer(offer); })(); }, []);
After presenting the initial offer to a customer, if the offer is rejected, the next offer must be retrieved. This is done using the rejection URL provided in the Api response. Below are examples of the Api response formats indicating where the rejection URL can be found:
The Api response in V1 provides a straightforward, flat data structure. The rejection URL is located within attributes -> actions
, specifically in the array item that includes a url
property under attributes
. If other items in the actions
array do not contain a url
property, they are not considered reject buttons.
{ "attributes": { "actions": [ // Accept button without a url property { "type": "button", "text": "Redeem $40 Bonus", "attributes": { "kind": "primary", "to": "https://app.uptick.com/i/offers/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE/accept" } }, // Reject button with a url property { "type": "button", "text": "No, thanks", "attributes": { "kind": "secondary" }, "url": "https://app.uptick.com/i/offers/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE/reject" } ] } }
The Api response in V2 is designed to be fully managed by the Generator
function. If you choose to render it manually using the response, you will need to traverse the data structure to locate the required nested information. Typically, this data is found approximately 10 levels deep in the object hierarchy, under an object named "button-reject"
. The url
property within the "button-reject"
object is the URL you will pass into the getNextOffer
callback. Below is a simplified example of the nested JSON structure:
// Many nested children deep { "type": "grid", "name": "actions", "children": [ { "type": "pressable", "name": "button-reject", "url": "https://app.uptick.com/i/offers/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE/reject", } ] }
The Generator class facilitates the complete rendering of an Uptick Offer for Shopify Checkout UI Extensions. It dynamically generates all the necessary offer components based on the result returned by the API service.
The Generator class accepts the following method parameters:
offer?.attributes?.header
).
offer?.children
.
false
).
true
.
Below is a complete example of generating an Uptick offer using a response from the V2 API on the Order Status page:
import { useState, useEffect } from "react"; import { useApi, reactExtension, } from "@shopify/ui-extensions-react/checkout"; // Generated import { Generator } from "./extraction/index.js"; // Services import { Api as UptickApi } from "./extraction/index.js"; import { View, InlineLayout, BlockSpacer, Spinner, SkeletonTextBlock } from "@shopify/ui-extensions-react/checkout"; const uptickApi = new UptickApi({ integrationId: "1b630eb1-30af-46bd-bdb9-8b97093e2998", apiVersion: "v2" }); const thankYouHeaderRenderAfter = reactExtension("purchase.thank-you.header.render-after", () => <Extension />); export { thankYouHeaderRenderAfter }; function Extension() { const shopApi = useApi(); const [loading, setLoading] = useState(true); const [offer, setOffer] = useState(null); uptickApi.setup({ shopApi, setLoading }); useEffect(() => { (async () => { let offer = await uptickApi.getInitialOffer("order_confirmation"); setOffer(offer); })(); }, []); function rejectOffer(rejectURL) { (async () => { let offer = await uptickApi.getNextOffer(rejectURL); setOffer(offer); })(); } // Show loading skeleton if loading is true and we've never had an offer if (loading === true && offer == null) { return ( <InlineLayout columns={["auto", 40, "fill"]}> <View> <BlockSpacer spacing="base" /> <Spinner size="large" /> </View> <View></View> <View> <SkeletonTextBlock lines={3} /> </View> </InlineLayout> ); } // If we aren't loading and offer is null or false, don't render anything if (offer == null || offer === false) { return ( <></> ); } // Generate uptick offer return ( <> { Generator({ defaultKeyName: "default", items: offer?.children, options: { button: { rejected: loading, rejectOffer: rejectOffer }, pressable: { rejected: loading, rejectOffer: rejectOffer } }, allowEmpty: true }) } </> ); }