Skip to content

Shopify Ad Unit (React)

Overview

This guide walks you through integrating the Falcon configurable ad template into your Shopify app. The setup is straightforward and requires minimal ongoing maintenance — everything is powered by git submodules, so updates are pulled in with a single command.

1. Repository Access

Template files are distributed via a private GitHub repository. Access is managed through SSH deploy keys — no individual GitHub accounts need to be added.

  1. Request a deploy key from your Falcon contact. You will receive a private key file.

  2. Save the file as falcon_deploy_key in your project root.

  3. Add falcon_deploy_key to your .gitignore:

    text
    falcon_deploy_key
  4. Every developer who needs to pull templates should have this file in their project root.

2. Prerequisites

  • react
  • @shopify/ui-extensions-react

3. Installation

First, create two helper scripts in your project root and add them to package.json.

Important: In both scripts and in package.json, replace <your-preferred-path> with the actual path where you want the templates (e.g., src/falcon-templates).

falcon-init.sh:

bash
#!/bin/bash
set -e

# ⬇️ Change this to your preferred submodule path
SUBMODULE_PATH="<your-preferred-path>"

DEPLOY_KEY="$(pwd)/falcon_deploy_key"

if [ ! -f "$DEPLOY_KEY" ]; then
  echo "Error: falcon_deploy_key not found in project root"
  exit 1
fi

chmod 600 "$DEPLOY_KEY"
GIT_SSH_COMMAND="ssh -i $DEPLOY_KEY -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \
  git submodule add git@github.com:falcon-partners/shopify-templates.git "$SUBMODULE_PATH"

echo "Submodule added at $SUBMODULE_PATH"

falcon-sync.sh:

bash
#!/bin/bash
set -e

# ⬇️ Change this to your preferred submodule path
SUBMODULE_PATH="<your-preferred-path>"

DEPLOY_KEY="$(pwd)/falcon_deploy_key"

if [ ! -f "$DEPLOY_KEY" ]; then
  echo "Error: falcon_deploy_key not found in project root"
  exit 1
fi

chmod 600 "$DEPLOY_KEY"
GIT_SSH_COMMAND="ssh -i $DEPLOY_KEY -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \
  git submodule update --remote --merge "$SUBMODULE_PATH"

echo "Templates synced"

Add to your package.json:

json
{
  "scripts": {
    "falcon:init": "bash ./falcon-init.sh",
    "falcon:sync": "bash ./falcon-sync.sh"
  }
}

Then install the submodule:

bash
npm run falcon:init

Note: Do not create the submodule path manually before running the command — the script creates the directory for you. If the directory already exists, the command will fail.

Recommended: Add the submodule path to .prettierignore:

text
<your-preferred-path>

This prevents Prettier from reformatting submodule files. If you accidentally modified files inside the submodule, discard the changes — the submodule does not need to be pushed separately.

4. File Overview

The react/ folder contains:

FileDescription
provider.tsxFeature management provider
index.tsxTemplate21 — configurable ad template
fallback.tsxTemplate15 — fallback template
renderer.tsxTemplate router (selects 21 or 15)
skeleton.tsxLoading skeleton

provider.tsx — FeatureManagementProvider

A React context provider that must wrap the template. It handles feature delivery internally — the provider makes a request to our server and manages feature flags, A/B testing, and configuration updates. This means new features and experiments are delivered to your users without any code changes on your side.

Props (provider.tsx)

typescript
interface FeatureManagementProviderProps {
  // Required
  publicKey: string; // Falcon API public key
  apiEndpoint: string; // API endpoint URL (default: https://pr-api.falconlabs.us/api/features/evaluate)
  userContext: FeatureManagementUserContext; // User targeting context (see below)
  extensionTarget: string; // Shopify extension target (see below)
  storage: Storage; // Shopify storage object from useStorage()
  children: ReactNode; // Child components

  // Optional
  loadingElement?: JSX.Element; // Component shown during loading
  disableClientImpressions?: boolean; // Disable automatic impression beacon firing (default: false)
}

FeatureManagementUserContext

typescript
interface FeatureManagementUserContext {
  // Required
  placementId: string; // The placement ID for this extension

  // Optional — User identification
  sessionId?: string; // One-time generated session ID (uuid, format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, 36 characters)
  hashedCustomerShopifyId?: string; // Hashed* Shopify customer ID (trimmed, e.g. "1" from "gid://shopify/Customer/1")
  hashedPhone?: string; // Hashed* phone number
  hashedEmail?: string; // Hashed* email address

  // Optional — Targeting attributes
  templateId?: number; // from Falcon API
  timezone?: string; // User timezone
  amount?: number; // Order amount, from useApi()
  orderId?: string; // Order ID, from useApi()
  paymentType?: string; // Payment type, from useApi()
  age?: number; // User age
  gender?: string; // User gender
  billingZipCode?: string; // Billing zip code, from useApi()
  referrer?: string; // Referrer URL
  screenWidth?: number; // Screen width in pixels
  screenHeight?: number; // Screen height in pixels
}

// *Hashed = trim → toLowerCase → SHA-256

Privacy note: We do not store any of this data. It is used exclusively at runtime for feature management and A/B testing.

If you have questions about where to obtain any of these values, reach out to the Falcon Labs technical team.


index.tsx — Template21 (Configurable Template)

The primary configurable ad template. Its layout, element parameters, and positioning are all controlled remotely through our proxy — changes take effect without pulling updates from GitHub.

Props (index.tsx)

typescript
interface TemplateProps {
  showIcon: boolean; // Show icon flag, from Falcon API
  templateData: TemplateData; // Template configuration (84 parameters), from Falcon API
  activeOffer: Offer; // Current active offer, from Falcon API
  offers: Offer[]; // Full array of offers, from Falcon API
  activeOfferIndex: number; // Index of the current offer in the offers array
  reachedEndOfOffers: boolean; // Whether all offers have been shown
  clickOffer: () => void; // Handler for offer click (primary CTA)
  handleNoThanks: () => void; // Handler for declining an offer
  extensionTarget: ExtensionTarget; // Shopify extension target
  firstName?: string; // Customer first name, from Shopify API
  email?: string; // Customer email, from Shopify API
}

Where data comes from:

  • showIcon, templateData, activeOffer, offers — provided by the Falcon proxy API (we will supply the endpoint).
  • extensionTarget, firstName, email — obtained from Shopify APIs on your side.
  • activeOfferIndex, reachedEndOfOffers, clickOffer, handleNoThanks — handled by your application logic.

Prop details:

  • activeOffer — The current offer object to display.
  • templateData — Configuration object including templateConfig (84 parameters) that control the template layout and styling.
  • extensionTarget — Identifies the extension point:
    • "purchase.thank-you.block.render" — Thank you page
    • "customer-account.order-status.block.render" — Order status page
  • offers — The full array of offers from the Falcon API response. Used internally by the template for the Inspired tease bar feature.
  • activeOfferIndex — The index of the currently displayed offer within the offers array.
  • clickOffer — Called when the primary CTA button is clicked. See Inspired offer behavior below.
  • handleNoThanks — Called when the decline button is clicked.
  • firstName — Used for personalization (e.g., "John, thank you for your purchase").
  • email — Customer email address, displayed in the template when email feature is enabled.
  • reachedEndOfOffers — Set to true to hide the component when no more offers are available.

If you have questions about any of these props, reach out to the Falcon Labs technical team.


fallback.tsx — Template15 (Fallback Template)

A simplified fallback template with a predefined layout. It accepts the same props as Template21. The Renderer component (below) handles switching between templates automatically.


renderer.tsx — Renderer

Handles template routing — automatically selects Template21 or Template15 based on the templateId from the Falcon proxy API. You don't need to implement any switching logic yourself.

The Renderer accepts the same props as the templates, plus one additional prop:

typescript
interface RendererProps extends TemplateProps {
  templateId: number; // Template ID from Falcon API
}

templateId is provided by the Falcon proxy API alongside templateData and activeOffer.

templateIdTemplate
21Template21
15Template15
any otherTemplate15 (fallback)

skeleton.tsx — TemplateDefaultSkeleton

A loading skeleton component. No props required.

Use it in two ways:

  1. Pass it to FeatureManagementProvider via the loadingElement prop — shown while the provider fetches feature configuration.
  2. Use it directly in your code during your own internal loading states.

5. Usage Example

tsx
import { FeatureManagementProvider } from '<your-preferred-path>/react/provider';
import { Renderer } from '<your-preferred-path>/react/renderer';
import { TemplateDefaultSkeleton } from '<your-preferred-path>/react/skeleton';

function App() {
  const publicKey = 'your-public-key'; // The same public key you use for requesting odata
  const apiEndpoint = 'https://pr-api.falconlabs.us/api/features/evaluate';

  const [sessionId] = useState(generateUUID());
  const { hashedCustomerShopifyId, hashedPhone, hashedEmail, firstName } =
    useShopifyApi();
  const { templateId, showIcon, templateData, activeOffer } = useFalconApi();

  const { reachedEndOfOffers, handleClick, handleDecline } = useFalconFlow();
  // You can use handleClick and handleDecline for your own custom logic.
  // reachedEndOfOffers should be true when offers are ended
  // (offers can be found within the odata response).

  const storage = useStorage();
  // Import from Shopify extension storage API depending on your target:
  //   thank you page:   '@shopify/ui-extensions-react/checkout'
  //   order status page: '@shopify/ui-extensions-react/customer-account'

  const userContext = {
    placementId: 'extension-placement-id',
    sessionId: sessionId,
    hashedCustomerShopifyId: hashedCustomerShopifyId,
    hashedPhone: hashedPhone,
    hashedEmail: hashedEmail,
    templateId: templateId,
  };

  const extensionTarget = 'purchase.thank-you.block.render';
  // Or 'customer-account.order-status.block.render' for order status page

  return (
    <FeatureManagementProvider
      publicKey={publicKey}
      apiEndpoint={apiEndpoint}
      userContext={userContext}
      loadingElement={<TemplateDefaultSkeleton />}
      storage={storage}
      extensionTarget={extensionTarget}
    >
      <Renderer
        templateId={templateId}
        showIcon={showIcon}
        templateData={templateData}
        activeOffer={activeOffer}
        offers={offers}
        activeOfferIndex={activeOfferIndex}
        reachedEndOfOffers={reachedEndOfOffers}
        clickOffer={handleClick}
        handleNoThanks={handleDecline}
        extensionTarget={extensionTarget}
        firstName={firstName}
        email={email}
      />
    </FeatureManagementProvider>
  );
}

6. Inspired Offer Behavior

Some Falcon API responses include an "Inspired" offer — a special final offer in the carousel. When this feature is active, the API response will contain:

  • templateData.hasInspired: true — indicates the last offer in the array is an Inspired offer
  • templateData.teaseMessage — a tease message displayed as a bar above the footer (e.g., "See what's next!")

The template handles the tease bar rendering automatically — it shows the bar when teaseMessage exists, hides it on non-block targets, and hides it when the user reaches the last (Inspired) offer.

However, the offer navigation logic is your responsibility. Your clickOffer handler must implement the "jump to last offer" behavior:

typescript
function clickOffer() {
  // ... your existing click tracking logic ...

  const lastIndex = offers.length - 1;

  // If already on the last offer, mark end of offers
  if (activeOfferIndex >= lastIndex) {
    setReachedEndOfOffers(true);
    return;
  }

  // If hasInspired, jump directly to the last (Inspired) offer
  if (templateData.hasInspired) {
    setActiveOfferIndex(lastIndex);
    return;
  }

  // Default: advance to next offer
  setActiveOfferIndex(activeOfferIndex + 1);
}

Key points:

  • When hasInspired is true, clicking the CTA should skip intermediate offers and jump directly to the last offer
  • The tease bar and offer index update should happen in the same state update to avoid visual flicker
  • handleNoThanks should always advance to the next offer sequentially (no jumping)

7. Impression Tracking

The SDK fires impression beacons automatically — no action required on your side. Each time the active offer changes, the template sends a request to activeOffer.beaconUrl to register that the offer was seen.

Server-side impressions (opt-out)

If you fire impressions yourself via a server-side proxy (e.g. to forward the real end-user IP and User-Agent), pass disableClientImpressions to the provider to prevent double-counting:

tsx
<FeatureManagementProvider
  publicKey={publicKey}
  apiEndpoint={apiEndpoint}
  userContext={userContext}
  storage={storage}
  extensionTarget={extensionTarget}
  disableClientImpressions
>
  ...
</FeatureManagementProvider>

When firing the beacon server-side, you must forward the real end-user IP and User-Agent as query parameters — otherwise the backend records your server's IP and UA instead:

Query paramValue
at.clientIpReal end-user IP address
at.userAgentReal end-user User-Agent

Example server-side beacon call:

http
GET {activeOffer.beaconUrl}&at.clientIp=1.2.3.4&at.userAgent=Mozilla%2F5.0...
Authorization: Bearer {publicKey}

Important: Use exactly at.clientIp and at.userAgent as the parameter names. Other names (e.g. userIp, userAgent) are not recognized and will be silently ignored.

8. Updating Templates

Run the sync script (set up in step 3):

bash
npm run falcon:sync

This pulls the latest templates from the Falcon repository using your deploy key.

Tip: Add falcon:sync to your pre-commit hook (e.g., via Husky) to keep templates up to date automatically.

9. CI/CD Setup

Your CI/CD environment (CodeBuild, GitHub Actions, etc.) does not have access to the private template repository by default. When your pipeline clones your repo, it won't be able to fetch the submodule — you need to configure the same deploy key on the server.

The idea is simple: before your build runs git submodule update, the deploy key must be available as an SSH identity. How you do this depends on your CI/CD provider — store the key in your provider's secrets manager, write it to a file at build time, and point SSH to it.

Example: AWS CodeBuild

1. Store the key in Secrets Manager:

bash
aws secretsmanager create-secret \
  --name "falcon-deploy-key" \
  --secret-string file://falcon_deploy_key

2. Add to your buildspec.yml:

yaml
env:
  secrets-manager:
    DEPLOY_KEY: 'falcon-deploy-key'

phases:
  install:
    commands:
      - mkdir -p ~/.ssh
      - echo "$DEPLOY_KEY" > ~/.ssh/falcon_deploy
      - chmod 600 ~/.ssh/falcon_deploy
      - export GIT_SSH_COMMAND="ssh -i ~/.ssh/falcon_deploy -o IdentitiesOnly=yes -o StrictHostKeyChecking=no"
      - git submodule update --init --recursive

Example: GitHub Actions

1. Store the key as a repository secret:

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

  • Name: FALCON_DEPLOY_KEY
  • Value: paste the full contents of falcon_deploy_key

2. Add to your workflow (e.g., .github/workflows/deploy.yml):

yaml
steps:
  - name: Setup deploy key
    run: |
      mkdir -p ~/.ssh
      echo "${{ secrets.FALCON_DEPLOY_KEY }}" > ~/.ssh/falcon_deploy
      chmod 600 ~/.ssh/falcon_deploy

  - name: Checkout
    uses: actions/checkout@v4

  - name: Fetch submodules
    run: |
      GIT_SSH_COMMAND="ssh -i ~/.ssh/falcon_deploy -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \
        git submodule update --init --recursive

Note: Do not use submodules: recursive in actions/checkout — it overrides SSH with its own HTTPS authentication, which does not have access to the private template repository.

Support

For questions or issues, contact Falcon Labs.