Configure App Frames

Prev Next

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.