React Native (V2)
How to install, initialize, and integrate the Recurly Engage React Native SDK to render prompts and track user interaction events in iOS and Android applications.
Prerequisites
- A Recurly Engage account with a valid App ID
- A React Native project targeting iOS and/or Android
Limitations
- Inline prompts scale to fit within their container, so size your container accordingly
- It may take several seconds for prompts to refresh after calling
setUserId() <PromptOverlay />must be placed at the bottom of your app node to ensure correct Z-order
Definition
Key benefits
Key details
The Recurly Engage React Native SDK provides:
- A prompt manager for initialization and user ID management
- Hooks and components for displaying modal prompts and inline prompts
- APIs for reporting user interactions (impression, goal, decline, dismiss, timeout, holdout)
Install the SDK
The SDK is published on the public npm registry. No registry configuration or authentication token is required.
npm
npm install @recurly/engage-core
npm install @recurly/engage-react-nativeYarn
yarn add @recurly/engage-core
yarn add @recurly/engage-react-native@react-native-async-storage/async-storage. @recurly/engage-react-native depends on it, but React Native's autolinking only picks up native modules declared directly in your app's package.json, not transitive dependencies. Add it to your own package.json as well (matching the version range @recurly/engage-react-native depends on), or you'll see NativeModule: AsyncStorage is null at runtime.npm install @react-native-async-storage/async-storageInitialize Engage
// Initialize the SDK at the top of your app node
export default function App() {
return (
<PromptProvider appId="YOUR_APP_ID" userId="INITIAL_USER_ID">
<AppRoot />
</PromptProvider>
);
}
// pull the SDK to check it has been initialized
const AppRoot: React.FC = () => {
const {
dispatch,
state: { promptMgr },
} = usePrompt();
const [isReady, setReady] = React.useState(false);
React.useEffect(() => {
if (!promptMgr) return;
const intervalId = setInterval(() => {
if (promptMgr.isInitialized()) {
setReady(true);
clearInterval(intervalId);
}
}, 1000);
return () => clearInterval(intervalId);
}, [promptMgr]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<NavigationContainer>
{isReady && (
<Stack.Navigator screenOptions={{ headerShown: true }}>
<Stack.Screen />
<Stack.Screen />
...
</Stack.Navigator>
)}
<PromptOverlay
onEvent={(result: PromptResult) => {
// TODO: handle the result
}}
/>
</NavigationContainer>
);
}
// (optional) You can load the SDK with specific fonts for various parts of the prompts
const AppRoot: React.FC = () => {
...
useFonts({
buttonFont: require('../assets/fonts/AllProDisplayC-Bold.ttf'),
otherFont: require('../assets/fonts/AllProDisplayC-Regular.ttf'),
});
React.useEffect(() => {
if (!promptMgr) return;
const intervalId = setInterval(() => {
if (promptMgr.isInitialized()) {
dispatch({
type: PromptAction_Font_Button,
data: 'buttonFont',
});
dispatch({
type: PromptAction_Font_Timer,
data: 'otherFont',
});
dispatch({
type: PromptAction_Font_LegalText,
data: 'otherFont',
});
setReady(true);
clearInterval(intervalId);
}
}, 1000);
return () => clearInterval(intervalId);
}, [promptMgr]); // eslint-disable-line react-hooks/exhaustive-deps
...
}Set the user ID
You can change the user ID after the SDK has been initialized, for example, when the user authenticates mid-session. It may take several seconds for the user's prompts to refresh.
promptMgr.setUserId(userId);Privacy consent categories
If your app gates data collection behind a consent banner or preference center, you can restrict which prompts are eligible to show based on the consent categories the user has granted. Prompts configured in Pulse with consent_categories are only shown once the categories you set match exactly.
import { PrivacyConsentCategory } from '@recurly/engage-core';
// Set the categories the user has consented to (e.g. from your consent banner)
promptMgr.setPrivacyConsentCategories([
PrivacyConsentCategory.strictlyNecessary,
PrivacyConsentCategory.performance,
]);
// Read back the categories currently set
promptMgr.getPrivacyConsentCategories();
/*
Available categories:
PrivacyConsentCategory.strictlyNecessary // 'strictly_necessary'
PrivacyConsentCategory.performance // 'performance'
PrivacyConsentCategory.functional // 'functional'
PrivacyConsentCategory.targeting // 'targeting'
*/Matching behavior:
- Before
setPrivacyConsentCategoriesis called, no filtering is applied. All prompts remain eligible regardless of theirconsent_categories. - Once the categories are set, a prompt is only eligible when its
consent_categoriesare an exact match (same categories, order doesn't matter) to the categories you set. A prompt configured withoutconsent_categoriesnever matches once any categories have been set. - The filter applies everywhere prompts are resolved:
screenChanged/buttonClickedtriggering, inline zones (getInlines/<RecurlyInline>), and custom rendering (getPrompts,getTriggerablePrompts). - Whenever consent changes (for example, the user updates their preferences), call
setPrivacyConsentCategoriesagain and re-trigger the current screen (promptMgr.screenChanged(currentScreen)) so eligibility is re-evaluated.
Render modal prompts
Interstitial, Popup, and Bottom Banner modals can be triggered when the user enters a screen and/or registers a click on an element. Add the following code to screens that are eligible to show a modal.
Use the promptMgr.screenChanged('home') method for entering a screen with a screen name (a customer-defined string, for example, "home").
Use the promptMgr.buttonClicked('clickId') method for registering a click on an element (a customer-defined string, for example, a button with an id of "clickId").
// Example a screen
import {
usePrompt, // Prompt state management
} from '@recurly/engage-react-native';
// Example: trigger when entering the "home" screen
export default function HomeScreen() {
const {
dispatch,
state: { promptMgr },
} = usePrompt();
React.useEffect(() => {
if (promptMgr) {
promptMgr.screenChanged('home');
}
}, [promptMgr]);
return (
// Example: trigger when a button is clicked
<TouchableOpacity
onPress={async () => {
if (promptMgr) {
promptMgr.buttonClicked('clickId');
}
}}
>Hello</TouchableOpacity>
);
}Render inline prompts
Use the RecurlyInline view to render an inline prompt if one is available for the current user. The inline prompt scales to fit within its container.
<RecurlyInline
zoneId="myZoneId" // ZoneID as specified in Pulse
closeButtonColor="#000000" // Hex color for close button, if enabled
closeButtonBgColor="#FFFFFF" // Hex background color for close button
closeButtonSize="20" // Close button height and width, in pixels
timerFontSize="14" // Countdown timer font size, if enabled
timerFontColor="#FFFFFF" // Countdown timer font hex color
focusStyle={{
borderWidth: 2,
borderColor: '#ff0000',
borderRadius: 5,
}}
onEvent={(result) => {}}
/>Custom prompt rendering
If you want to render prompts differently from what the SDK produces by default, you can render them using the prompt metadata.
Your app should report prompt interactions through the provided functions on the prompt object.
[TODO: Dev/PO review — possible issue: the example below uses promot.deeplink and promot.holdout() ("promot" instead of "prompt"), and its comments mention homeScreen while the code uses 'home_screen'. Left verbatim.]
// Example: Retrieve all available prompts of specified type. See PathType values below. Use this if trigger criteria is to be ignored.
let prompts = promptMgr.getPrompts(PathType.ALL);
// Example: Retrieve all available prompts of specified type and trigger criteria (screenName `homeScreen`)
let prompts = promptMgr.getTriggerablePrompts('home_screen','*', PathType.ALL );
// Example: Retrieve all available prompts of specified type and trigger criteria (screenName `homeScreen` and clickId `add_to_watchlist`)
let prompts = promptMgr.getTriggerablePrompts('home_screen','add_to_watchlist', PathType.ALL );
// Access prompt properties (See below for Prompt interface details)
prompt.button1
prompt.button2
prompt.button3
prompt.inAppSku
promot.deeplink
prompt.deviceMeta
// Report prompt interactions
prompt.impression() // prompt shown to user
prompt.goal() // user clicks on primary CTA
prompt.goal2() // user clicks on secondary CTA
prompt.decline() // user clicks on decline (3rd) button
prompt.dismiss() // user dismisses prompt by clicking on "x" close button
prompt.timeout() // prompt is dismissed via countdown timer
promot.holdout() // prompt is triggered, however the user is in the Control group so prompt should not be shown
/*
PathType values:
PathType.ALL = -1
PathType.MODAL = 2 // Referenced as Popup within Pulse
PathType.HORIZONTAL = 5
PathType.TEXT = 7
PathType.VERTICAL = 8
PathType.TILE = 9
PathType.INTERSTITIAL = 10
PathType.BOTTOM_BANNER = 13
PromptResultCode values:
// Success codes
OK = 1,
// Error codes
ERROR = -100,
NOT_APPLICABLE = -101,
DISABLED = -102,
SUPPRESSED = -103,
// Interactions
IMPRESSION = 100,
BUTTON1 = 101, // CLICK
BUTTON2 = 102, // CLICK2
BUTTON3 = 103, // DECLINE
DISMISS = 110,
TIMEOUT = 111,
HOLDOUT = 120
interface Prompt {
id: string;
type: PathType;
actions: Action;
actionGroupId?: string;
inAppSku?: string;
deviceMeta?: { [key: string]: any };
deeplink?: { [key: string]: any };
button1?: ModalButton;
button2?: ModalButton;
button3?: ModalButton;
buttonBorderRadius: number;
buttonBorderColor: string;
buttonBorderThickness: number;
countDownPrompt: string;
countDownPromptColor: string;
countDownPromptFontSize: number;
countDownPromptInvisible: boolean;
countDown: number;
horizontalPoster?: string;
impression: () => Promise<PromptResult>;
dismiss: () => Promise<PromptResult>;
timeout: () => Promise<PromptResult>;
holdout: () => Promise<PromptResult>;
goal: () => Promise<PromptResult>;
goal2: () => Promise<PromptResult>;
decline: () => Promise<PromptResult>;
}
interface ModalButton {
label?: string;
textColor?: string;
textHightlightColor?: string;
bgColor?: string;
}
*/Actions
When a user interacts with the primary prompt call-to-action (CTA), a result callback includes various metadata associated with the prompt. Use it to determine the client-side action that should take place.
// Data schema of the result callback
interface PromptResult {
code: PromptResultCode;
value?: { [key: string]: any };
meta?: { [key: string]: any };
promptMeta?: {
promptName: string;
promptID: string;
promptVariationName: string;
promptVariationID: string;
promptExperimentName: string;
promptExperimentID: string;
buttonLabel: string;
};
}Analytics callback example
<RecurlyInline
zoneId="myZoneId" // ZoneID as specified in Pulse
closeButtonColor="#000000" // Hex color for close button, if enabled
closeButtonBgColor="#FFFFFF" // Hex background color for close button
closeButtonSize="20" // Close button height and width, in pixels
timerFontSize="14" // Countdown timer font size, if enabled
timerFontColor="#FFFFFF" // Countdown timer font hex color
onEvent={(result) => {
const getEventName = (code: PromptResultCode) => {
switch (code) {
case PromptResultCode.IMPRESSION:
return 'Engage Impression';
case PromptResultCode.BUTTON1:
return 'Engage Click';
case PromptResultCode.BUTTON2:
return 'Engage Click2';
case PromptResultCode.BUTTON3:
return 'Engage Decline';
case PromptResultCode.DISMISS:
return 'Engage Dismiss';
case PromptResultCode.TIMEOUT:
return 'Engage Timeout';
case PromptResultCode.HOLDOUT:
return 'Engage Holdout';
default:
return 'Engage Event';
}
};
const analyticsData = {
name: getEventName(result.code),
data: {
...result.promptMeta,
timestamp: new Date().toISOString()
}
};
console.log('ANALYTICS:', JSON.stringify(analyticsData, null, 2));
/* Example:
ANALYTICS: {
"name": "Engage Click",
"data": {
"promptName": "My Prompt Name",
"promptID": "438c8eec-1111-1111-1111-2222",
"promptVariationName": "Varation 2",
"promptVariationID": "438c8eec-1111-1111-1111-2222",
"promptExperimentName": "My Experiment",
"promptExperimentID": "438c8eec-1111-1111-1111-3333",
"promptType": 5,
"buttonLabel": "Sign me up",
"timestamp": "2025-02-12T05:41:28.365Z"
}
}
*/
// Send Payload to Analytics
}}
/>
// Modal prompts (interstitial, popup, bottom banner) are rendered automatically
// by <PromptOverlay />. Use the same analytics code inside its onEvent callback:
<PromptOverlay
onEvent={(result: PromptResult) => {
// utilize same analytics code above
}}
/>Deep link
You can add a deep link to a prompt in Pulse. When the user selects the CTA, you can use the deep link to send the user to a specific location within the app.
{
"code": 0,
"meta": {
"meta": {},
"deeplink": "redflix://test123"
},
}Custom metadata
You can add custom key-value pairs to an item in Pulse. You can use these values to perform an action other than the typical media asset deep link, such as sending the user to a registration screen or performing an operation on behalf of the user.
{
"code": 0,
"meta": {
"meta": {
"keyName1": "foo",
"keyName2": "bar",
"differentKey": "baz"
},
},
}Send a usage tracking event
Your app can send custom track events using the SDK. If you configure a tracker in Pulse, you can use these custom events to target prompts at specific sets of users.
promptMgr.customTrack(customFieldId);Debugging
You can reset the current user's prompt status so that previously suppressed prompts become available again.
promptMgr.resetGoal();Migrate from the redfast-scoped packages
If you integrated this SDK before it moved to public npm, update the following:
| Before | After |
@recurly/react-native-redfast | @recurly/engage-react-native |
@recurly/redfast-core | @recurly/engage-core |
RedfastInline | RecurlyInline |
.npmrc/.yarnrc.yml with an AUTHTOKEN for npm.pkg.github.com | Remove entirely. No registry config needed |
You also need to add @react-native-async-storage/async-storage as a direct dependency in your own package.json if you haven't already. See the note under Install the SDK above.
Claude skill
A Claude skill for this SDK is available for download at SKILL.md. This skill enables Claude to assist with SDK integration, prompt configuration, and event handling in your React Native application.
Updated 13 days ago