This page explains how to display App Frames content in your React Native app with the InsiderAppFrame component. The component renders the native iOS and Android App Frames views, so the content, analytics, and refresh behavior are the same as in a native app.
This component is available on React Native SDK 9.0.0 or higher.
iOS: Requires React Native 0.77 or higher with the New Architecture enabled. With the legacy architecture the component renders nothing, and a warning is logged in development builds.
Overview
App Frames renders marketer-managed content inline, as a natural part of your screen: a banner on the home page, an offer inside a product list, an announcement above the checkout button. Unlike push notifications or in-app messages, a frame is not an overlay. It is a component you place in your own layout, and you manage its content from the Insider One panel without a new app release.
Automatic refresh: Frames refresh when the app returns to the foreground and when the user identity changes.
In-memory caching: Already loaded content re-renders instantly, without a network request.
Automatic analytics: The SDK tracks impressions, clicks, and dismissals.
Marketer-side control: The panel manages targeting and up to 8 campaign variants per placement.
During a successful load, a frame moves through these statuses. READY is the only status in which content is visible; every other status keeps the frame collapsed.
RESOLVING → DOWNLOADING → RENDERING → READY
Before you start
Opt-in: App Frames is off by default. Until you enable it (Step 1), every frame stays empty.
Fail-silent: An empty placement renders nothing. Do not reserve fixed space for a frame.
No manual loading: A frame loads when it is mounted and refreshes automatically. There is no load or refresh method.
1. Enable App Frames
Add the opt-in flag to both native projects of your React Native app.
iOS: In ios/<YourApp>/Info.plist, add the App Frames Enabled key inside the Insider dictionary. If your Info.plist already has an Insider dictionary, add the key to it.
Android: In android/app/src/main/AndroidManifest.xml, add the meta-data inside the <application> tag.
Info.plist
<key>Insider</key>
<dict>
<key>App Frames Enabled</key>
<true/>
</dict>AndroidManifest.xml
<application ...>
<meta-data
android:name="com.useinsider.insider.APP_FRAMES_ENABLED"
android:value="true" />
</application>If the flag is missing or misspelled, frames report the DISABLED status and render nothing. In development builds, a one-time warning explains how to fix it.
2. Create a placement
Define your placement in the panel under Mobile App Settings › App Frames Placement. Note the placement ID; you pass the same value to the component.
3. Add the component
Import InsiderAppFrame as a named export from react-native-insider and place it where the content should appear. The component accepts all standard View props, such as style and testID.
The following table shows the data type for the component's props.
Prop | Data Type | Description |
|---|---|---|
placementId | string (required) | The placement ID defined in the panel. |
onStatusChange | (status, previousStatus) => void | Called on every status change. See Track the frame status. |
onLoadError | (error: InsiderAppFramesError) => void | Called when the content fails to load. Diagnostic only. |
onHeightChange | (height: number) => void | Called when the content reports a new height, in dp. |
onDismissRequest | () => void | Called when the user closes the content. |
onAction | (data: Record<string, unknown>) => void | Called with the custom data of an action defined in the campaign. |
ref | InsiderAppFrameHandle | Read the current status and error. See Read the status with a ref. |
Method Signature
import type { ViewProps } from 'react-native';
interface InsiderAppFrameProps extends ViewProps {
placementId: string;
onStatusChange?: (
status: InsiderAppFrameStatusType,
previousStatus: InsiderAppFrameStatusType,
) => void;
onLoadError?: (error: InsiderAppFramesError) => void;
onHeightChange?: (height: number) => void;
onDismissRequest?: () => void;
onAction?: (data: Record<string, unknown>) => void;
}
interface InsiderAppFrameHandle {
getStatus(): InsiderAppFrameStatusType;
getError(): InsiderAppFramesError | null;
}
declare const InsiderAppFrame: ForwardRefExoticComponent<
InsiderAppFrameProps & RefAttributes<InsiderAppFrameHandle>
>;Method Example
import React from 'react';
import { ScrollView, Text } from 'react-native';
import { InsiderAppFrame } from 'react-native-insider';
export default function HomeScreen() {
return (
<ScrollView>
<Text>Welcome</Text>
{/* Renders nothing when there is no active campaign for "home_page". */}
<InsiderAppFrame placementId="home_page" />
<Text>Recommended for you</Text>
</ScrollView>
);
}4. Track the frame status
Use onStatusChange to observe the frame's lifecycle. It is called with the new status and the previous one. The isLoadingStatus and isErrorStatus helpers let you group statuses without matching each case.
Method Signature
type InsiderAppFrameStatusType =
| 'DETACHED' | 'NO_PLACEMENT' | 'DISABLED'
| 'RESOLVING' | 'DOWNLOADING' | 'RENDERING'
| 'READY' | 'UNAVAILABLE' | 'DISMISSED'
| 'ERROR_RESOLVING' | 'ERROR_DOWNLOADING' | 'ERROR_RENDERING'
| 'UNRECOGNIZED';
function isLoadingStatus(status: InsiderAppFrameStatusType): boolean;
function isErrorStatus(status: InsiderAppFrameStatusType): boolean;Method Example
import {
InsiderAppFrame,
InsiderAppFrameStatus,
isErrorStatus,
isLoadingStatus,
} from 'react-native-insider';
<InsiderAppFrame
placementId="home_page"
onStatusChange={(status, previousStatus) => {
if (status === InsiderAppFrameStatus.READY) {
// Content is visible.
} else if (isLoadingStatus(status)) {
// RESOLVING, DOWNLOADING, or RENDERING.
} else if (isErrorStatus(status)) {
// ERROR_RESOLVING, ERROR_DOWNLOADING, or ERROR_RENDERING.
}
}}
/>Status | Description |
|---|---|
DETACHED | The frame is not on screen. Initial status. |
NO_PLACEMENT | The frame has no placement ID, or the value is not a non-empty string. |
DISABLED | App Frames is not enabled for the app. Check Step 1. Not an error. |
RESOLVING | Finding the campaign that targets the placement. |
DOWNLOADING | Downloading the campaign content. |
RENDERING | Rendering the content. The frame is not visible yet. |
READY | The content is rendered. The only visible status. |
UNAVAILABLE | No active campaign targets the placement. Not an error. |
DISMISSED | The user closed the content. |
ERROR_RESOLVING | The campaign could not be resolved. |
ERROR_DOWNLOADING | The content could not be downloaded, or its URL failed validation. |
ERROR_RENDERING | The content was downloaded but could not be displayed. |
UNRECOGNIZED | A status from a newer native SDK that this plugin version does not recognize. |
Read the status with a ref
To read the current status or error outside the callbacks, attach a ref. getError() returns a value only while the status is one of the ERROR_* statuses.
import React, { useRef } from 'react';
import { InsiderAppFrame } from 'react-native-insider';
export default function Banner() {
const frameRef = useRef(null);
const logFrameState = () => {
const status = frameRef.current?.getStatus();
const error = frameRef.current?.getError();
console.log('home_page', status, error?.code);
};
return <InsiderAppFrame ref={frameRef} placementId="home_page" />;
}5. Handle dismissal
When the user closes the content (for example, with the close button of the template), the frame collapses to zero height and onDismissRequest is called. If the placement gets new content later, the frame can show it again. To keep it closed, remove the component in onDismissRequest.
const [showBanner, setShowBanner] = useState(true);
return (
<View>
{showBanner && (
<InsiderAppFrame
placementId="home_page"
onDismissRequest={() => setShowBanner(false)}
/>
)}
</View>
);6. Handle actions
A campaign can attach custom data to a button. When the user taps it, onAction is called with that data as an object.
<InsiderAppFrame
placementId="home_page"
onAction={(data) => {
if (typeof data.coupon === 'string') {
// Apply the coupon in your app.
}
}}
/>The SDK has already opened any deep link on the action before this callback is called. The callback is a notification; you cannot cancel the navigation from it.
7. Height
By default the component sizes itself to the content. It grows to the content height when the content is ready and collapses to zero when the content goes away (for example, UNAVAILABLE, DISMISSED, or an error).
To use a fixed height, set height in style. An explicit height in style always wins over the content height.
Until the content reports a height, your own flex or aspectRatio styles apply.
onHeightChange is called with every height the content reports, in dp.
Do not set a fixed height to reserve space for a frame. When there is no content, the reserved space stays empty.
8. Handle errors
onLoadError is called with an InsiderAppFramesError when a frame fails to load. Use it for logging. Do not show an error to the end user; the frame already collapses.
Method Signature
class InsiderAppFramesError extends Error {
readonly code: InsiderAppFramesErrorCodeType;
readonly message: string;
readonly dismissCode: number; // NO_DISMISS_CODE (-1) when the template reported none
readonly cause: string | null;
}
type InsiderAppFramesErrorCodeType =
| 'UNKNOWN'
| 'RESOLUTION_FAILED'
| 'RESPONSE_MALFORMED'
| 'DOWNLOADING_FAILED'
| 'PLACEMENT_UNTRUSTED'
| 'CONTENT_DISPLAY_FAILED'
| 'RENDERING_FAILED';Property | Data Type | Description |
|---|---|---|
code | string | The error code. See the table below. Compare it with InsiderAppFramesErrorCode. |
message | string | A description of the error code. |
dismissCode | number | The dismiss code reported by the template, or NO_DISMISS_CODE (-1). |
cause | string | null | The underlying native error message, if any. For diagnostics only. |
Error code | Status | Description |
|---|---|---|
RESOLUTION_FAILED | ERROR_RESOLVING | The request that resolves placements failed (network or server error). |
RESPONSE_MALFORMED | ERROR_RESOLVING | The placements response could not be read. |
DOWNLOADING_FAILED | ERROR_DOWNLOADING | The content could not be downloaded. |
PLACEMENT_UNTRUSTED | ERROR_DOWNLOADING | The content URL is not a valid http(s) URL, so it was not loaded. |
CONTENT_DISPLAY_FAILED | ERROR_RENDERING | The content was downloaded but could not be displayed. |
RENDERING_FAILED | ERROR_RENDERING | The template reported an error while running. |
UNKNOWN | N/A | An unclassified error. |
Method Example
import { InsiderAppFrame, InsiderAppFramesErrorCode } from 'react-native-insider';
<InsiderAppFrame
placementId="home_page"
onLoadError={(error) => {
if (error.code === InsiderAppFramesErrorCode.RESOLUTION_FAILED) {
// Network or server error; the frame retries on the next refresh.
}
console.log('App Frame failed:', error.code, error.cause);
}}
/>Full example
The following screen shows two placements in a scroll view and uses every callback.
import React, { useState } from 'react';
import { ScrollView, Text } from 'react-native';
import { InsiderAppFrame } from 'react-native-insider';
export default function HomeScreen() {
const [showTopBanner, setShowTopBanner] = useState(true);
return (
<ScrollView>
{showTopBanner && (
<InsiderAppFrame
placementId="home_top_banner"
onStatusChange={(status, previousStatus) => {
console.log('home_top_banner:', previousStatus, '->', status);
}}
onLoadError={(error) => {
console.log('home_top_banner failed:', error.code);
}}
onHeightChange={(height) => {
console.log('home_top_banner height:', height);
}}
onDismissRequest={() => setShowTopBanner(false)}
onAction={(data) => {
console.log('home_top_banner action:', data);
}}
/>
)}
<Text>Categories</Text>
<Text>Best sellers</Text>
<InsiderAppFrame placementId="home_mid_offer" />
<Text>Recommended for you</Text>
</ScrollView>
);
}Limitations
A placement can have at most 10 active campaigns.
Each image asset can be at most 500 KB.
Content is cached in memory only; it is not persisted across app launches.
A new placement requires a developer to add a component with its placement ID to the app.
Frames load only when GDPR consent and mobile app access are granted. When consent is revoked, frames move to DISABLED.
Troubleshooting
Symptom | Cause | Fix |
|---|---|---|
Nothing is shown; status is DISABLED | The opt-in flag is missing, or consent is not granted. | Check Step 1 on both platforms (the key must be inside the Insider dictionary on iOS). Check GDPR consent and mobile app access. |
Nothing is shown; status is UNAVAILABLE | No active campaign targets the placement. | Check that the placement ID matches the panel exactly and that the campaign is active and targets this user. |
Nothing is shown on iOS only; no callbacks | The New Architecture is disabled, or React Native is older than 0.77. | Enable the New Architecture and use React Native 0.77 or higher. Check the development warning in the console. |
Status stays NO_PLACEMENT | placementId is empty or not a string. | Pass the placement ID as a non-empty string. |
Content is cut off or has extra space | A fixed height in style. | Remove the fixed height so the component sizes itself. |
A closed frame shows up again | New content arrived for the placement after the dismissal. | Remove the component in onDismissRequest. See Handle dismissal. |