Appearance
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.
Request a deploy key from your Falcon contact. You will receive a private key file.
Save the file as
falcon_deploy_keyin your project root.Add
falcon_deploy_keyto your.gitignore:textfalcon_deploy_keyEvery 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:initNote: 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:
| File | Description |
|---|---|
provider.tsx | Feature management provider |
index.tsx | Template21 — configurable ad template |
fallback.tsx | Template15 — fallback template |
renderer.tsx | Template router (selects 21 or 15) |
skeleton.tsx | Loading 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-256Privacy 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 includingtemplateConfig(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 theoffersarray.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 totrueto 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.
templateId | Template |
|---|---|
21 | Template21 |
15 | Template15 |
| any other | Template15 (fallback) |
skeleton.tsx — TemplateDefaultSkeleton
A loading skeleton component. No props required.
Use it in two ways:
- Pass it to
FeatureManagementProvidervia theloadingElementprop — shown while the provider fetches feature configuration. - 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 offertemplateData.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
hasInspiredis 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
handleNoThanksshould 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 param | Value |
|---|---|
at.clientIp | Real end-user IP address |
at.userAgent | Real 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.clientIpandat.userAgentas 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:syncThis pulls the latest templates from the Falcon repository using your deploy key.
Tip: Add
falcon:syncto 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_key2. 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 --recursiveExample: 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 --recursiveNote: Do not use
submodules: recursiveinactions/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.