React Native and Expo guide
The React Native SDK is not a separate package. It is the @contentflow/sdk/react-native entry point of the same JavaScript SDK that serves Web and React, so one archive covers every JavaScript platform. The SDK is plain JavaScript with no native module of its own: the native code in your app comes from the packages listed under Dependencies, whose versions you choose.
This page covers which React Native and Expo versions work, how to set each one up, and what behaves differently from the web. The API itself (identity, consent, blocks, campaigns, events) is the same on every platform and is documented in the JavaScript SDK reference.
Compatibility at a glance
| React Native | Expo SDK | Status | What you need to do |
|---|---|---|---|
| 0.79 and later | 53 and later | Supported | Nothing beyond installing. |
| 0.72 to 0.78 | 50 to 52 | Supported with one Metro setting | Turn on package exports in metro.config.js (see Configure Metro). |
| 0.71 and earlier | 49 and earlier | Not supported | Upgrade React Native. |
Why the line falls there: the SDK publishes its entry points (/react-native, /expo, /react, …) through the exports map in its package.json. Metro, the React Native bundler, only reads that map by default from Metro 0.82, which ships with React Native 0.79 and Expo SDK 53. React Native 0.72 to 0.78 can read it when you opt in. Earlier Metro releases cannot read it at all, and the import fails with Unable to resolve module @contentflow/sdk/react-native.
The rest of the platform requirements are the same for every supported version:
| Requirement | Supported |
|---|---|
| React | 17 or later (whatever your React Native version ships with) |
| JavaScript engine | Hermes and JSC |
| React Native architecture | New Architecture and the legacy architecture (the SDK has no native module, so it does not depend on either) |
| Expo workflow | Expo Go, development builds and bare apps. Push notifications need a development build: Expo Go does not support push for your own app. |
| iOS and Android minimums | Set by your React Native version and the native packages you add, not by the SDK |
The React Native sample in the release (examples/react-native-app) is built and checked with Expo SDK 57, React Native 0.86 and React 19.2 for each release. The Metro behaviour above was checked by bundling an app on React Native 0.72.17 with and without the setting.
Dependencies
The SDK lists these as optional peer dependencies. Nothing is bundled into it, so you install the version that fits your React Native release and never get a second copy of React or React Native.
| Package | Needed for | Versions |
|---|---|---|
@react-native-async-storage/async-storage |
Required: the default storage for the device id, consent, segments and cached content | 1.x, 2.x or 3.x. The SDK only calls getItem, setItem and removeItem, which all three keep. Use the release your React Native version supports (on Expo, npx expo install picks it). |
react-native-sse |
Optional: live updates over a stream. Without it the SDK polls. | Any. It is plain JavaScript. |
@react-native-community/geolocation |
Optional: device location (requestLocation, startLocationTracking). React Native core no longer ships a location API. |
Any module with the navigator.geolocation shape works. |
expo-notifications |
Optional: the Expo push helper, @contentflow/sdk/expo |
Any. You pass your own copy of the module in, so the SDK never imports it. |
Install
- Download the SDK archive and its checksum file from Releases and downloads, and verify the archive as described in the JavaScript SDK install steps.
- Install the archive and AsyncStorage, plus any optional packages you need.
Bare React Native:
npm install ./vendor/contentflow-sdk-<version>.tgz
npm install @react-native-async-storage/async-storage
npm install react-native-sse # optional: live updates
npm install @react-native-community/geolocation # optional: device location
cd ios && pod install && cd ..Expo:
npm install ./vendor/contentflow-sdk-<version>.tgz
npx expo install @react-native-async-storage/async-storage
npx expo install react-native-sse # optional: live updates
npx expo install expo-notifications # optional: push- Configure Metro if your version needs it (next section).
- Rebuild the native app (
npx react-native run-ios/run-android, ornpx expo run:ios/run:androidfor a development build). AsyncStorage adds native code, so a JavaScript reload alone is not enough the first time.
Configure Metro
React Native 0.79+ and Expo SDK 53+
Nothing to do. Package exports are on by default. If your metro.config.js sets resolver.unstable_enablePackageExports = false (some apps added this to work around another library), the SDK import will fail; remove that line or set it to true.
React Native 0.72 to 0.78 (bare)
// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const config = {
resolver: {
unstable_enablePackageExports: true,
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);Expo SDK 50 to 52
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
config.resolver.unstable_enablePackageExports = true;
module.exports = config;After changing the file, restart Metro with a clean cache: npx react-native start --reset-cache, or npx expo start -c.
This setting applies to every package in the app, not only ContentFlow. It is the same behaviour React Native turned on for everyone in 0.79, so most libraries are ready for it, but run through your app once after enabling it. If another library breaks, the practical fix is to upgrade that library or React Native itself.
Set up the provider
Wrap your app once in CFProvider. It creates the client, starts it, and disposes it when the provider unmounts.
import EventSource from 'react-native-sse'; // optional: live updates
import Geolocation from '@react-native-community/geolocation'; // optional: device location
import { CFProvider, CFBanner } from '@contentflow/sdk/react-native';
export default function App() {
return (
<CFProvider
config={{ publicKey: '<workspace-id>_test', locale: 'ar' }}
eventSource={EventSource}
geolocation={Geolocation}
>
<Home />
</CFProvider>
);
}
function Home() {
return <CFBanner blockKey="home_banner" onPress={(b) => openLink(String(b.get('cta_url') ?? ''))} />;
}@contentflow/sdk/react-native exports the same hooks as the React entry (useCF, useCFBlock, useCFBlocks, useCFCampaigns, useCFString, useCFTranslations, useCFTheme, useLiveStatus) plus two ready-made components, CFBanner and CFCampaignBanner. Both are optional: pass render to draw a block with your own components, or use the hooks directly.
To configure the platform yourself (for example, a different storage backend), build the client and hand it to the provider:
import { CFClient, CFProvider, createRNPlatform } from '@contentflow/sdk/react-native';
const config = { publicKey: '<workspace-id>_test' };
const client = new CFClient(
config,
createRNPlatform({ EventSource, geolocation: Geolocation, storage: myKeychainStorage }),
);
<CFProvider config={config} client={client}>…</CFProvider>;Push registration with Expo is covered in Expo push. Bare apps register a native APNs/FCM token with client.registerPushWithReceipt({ token, platform: 'ios', provider: 'apns' }) (or 'android' / 'fcm'), as in the SDK guide.
What is different on React Native
- Storage. Data is kept in AsyncStorage instead of
localStorage. PassstoragetocreateRNPlatformto use something else. - Secure storage is not applied. On the web,
enableSecureStorage(on by default) obfuscates consent and segment data. That needscrypto.subtle, which React Native does not provide, so on React Native the SDK detects this and stores those values in plain AsyncStorage. It is obfuscation, not encryption, even on the web. If you need the data protected at rest, supply astorageadapter backed by the iOS Keychain or Android Keystore. - Request signing and certificate pinning. The optional JavaScript HMAC and pinning flags fail explicitly on every JavaScript platform, React Native included. Leave them off.
- App lifecycle. The SDK listens to
AppState: it flushes queued events and pauses live updates when the app goes to the background, and refreshes when it comes back. - Live updates. With
react-native-ssethe SDK streams changes. Without it, it polls.useLiveStatus()shows which one is active. - Location. Without a
geolocationmodule,requestLocation()rejects immediately andstartLocationTracking()logs a warning. The OS permission prompt only appears when you call one of them, never at start. - SDK version header. Requests identify themselves as
react-native/<version>inX-CF-SDK-Version, so the server can tell which SDK build and platform is calling.
Upgrading the SDK
- Download and verify the new archive, then
npm install ./vendor/contentflow-sdk-<new-version>.tgz. - Read that version's Action needed notes in the changelog.
- Restart Metro with a clean cache and rebuild the app.
Upgrading React Native itself does not require a new SDK. If you move to React Native 0.79 or Expo SDK 53 or later, you can delete the unstable_enablePackageExports line from metro.config.js.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Unable to resolve module @contentflow/sdk/react-native |
Metro is not reading package exports: React Native 0.72 to 0.78 without the setting, or the setting forced to false |
Follow Configure Metro, then restart Metro with --reset-cache. On 0.71 or earlier, upgrade React Native. |
Unable to resolve module @react-native-async-storage/async-storage |
AsyncStorage is not installed | Install it, then rebuild the native app. |
NativeModule: AsyncStorage is null |
AsyncStorage is installed but the native app was not rebuilt | Run pod install (iOS) and rebuild, or create a new development build on Expo. |
| Content arrives, but only every few seconds | No eventSource was passed, so the SDK polls |
Install react-native-sse and pass it to CFProvider. |
requestLocation() rejects immediately |
No geolocation module was passed | Pass geolocation={Geolocation} to CFProvider. |
Push registration returns unsupported_token |
An Expo relay token (ExponentPushToken[...]) was used |
Use registerExpoPush, which requests the native APNs/FCM token. ContentFlow does not deliver through Expo's relay. |