Shopify UI Extensions Integration Guide

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.


Features

  • Dynamic Offer Rendering: Display offers using generated Shopify UI Checkout components.
  • Error Handling and Callbacks: Includes hooks for handling errors and warning events.
  • Dynamic Content Support: Renders a variety of offer types, such as buttons, text, disclaimers, and more, based on the API response.
  • API Flow Management: Fetches responses and returns data to dynamically render offers.

Installation

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

Option 2:

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.

Api Usage

The Api service handles server communication and response processing. It can be used independently or alongside the Generator class.

1. Initialize the Api

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 });

2. Setup Api and Render Calls

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 });

3. Fetch the Initial Offer

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);
    })();
  }, []);

4. Handle Offer Rejections

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:

V1 Api Response:

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"
      }
    ]
  }
}

V2 Api Response:

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",
    }
  ]
}

Generator

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:

  • defaultKeyName: Specifies the default key name to use when an item lacks a name.
    • For V1, it is recommended to use the name of the section being rendered.
    • For V2, you can use any key name, such as “default” or “root”.
  • items: An array of items to generate elements for.
    • For V1, this is the property to be rendered (e.g., offer?.attributes?.header).
    • For V2, this corresponds to the single parameter offer?.children.
  • options: An object containing configuration options for generating elements. This is useful for passing state and callbacks to the generated components.
  • allowEmpty: A boolean that determines whether empty elements are allowed (default is false).
    • For V2, this should be set to 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
        })
      }
    </>
  );
}

Troubleshooting Tips

  • Support: If issues persist, reach out to our technical support team for further assistance.