diff --git a/package.json b/package.json
index e3946e05cb..188ab88eaa 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"packages/sdk/react/examples/vercel-edge",
"packages/sdk/react-native",
"packages/sdk/react-native/example",
+ "packages/sdk/react-native/example-fdv2",
"packages/sdk/react-native/contract-tests/entity",
"packages/sdk/vercel",
"packages/sdk/svelte",
diff --git a/packages/sdk/react-native/example-fdv2/.env.example b/packages/sdk/react-native/example-fdv2/.env.example
new file mode 100644
index 0000000000..476c2b4e79
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/.env.example
@@ -0,0 +1 @@
+LAUNCHDARKLY_MOBILE_KEY=
diff --git a/packages/sdk/react-native/example-fdv2/.gitignore b/packages/sdk/react-native/example-fdv2/.gitignore
new file mode 100644
index 0000000000..017aa78aa2
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/.gitignore
@@ -0,0 +1,44 @@
+# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
+.env
+
+# dependencies
+node_modules/
+
+# Expo
+.expo/
+dist/
+web-build/
+
+# Native
+*.orig.*
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+
+# Metro
+.metro-health-check*
+
+# debug
+npm-debug.*
+yarn-debug.*
+yarn-error.*
+
+# macOS
+.DS_Store
+*.pem
+
+# local env files
+.env*.local
+
+# typescript
+*.tsbuildinfo
+
+ios
+android
+
+!yarn.lock
+
+# detox
+artifacts
diff --git a/packages/sdk/react-native/example-fdv2/App.tsx b/packages/sdk/react-native/example-fdv2/App.tsx
new file mode 100644
index 0000000000..b7d9afefec
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/App.tsx
@@ -0,0 +1,27 @@
+import { LAUNCHDARKLY_MOBILE_KEY } from '@env';
+
+import {
+ AutoEnvAttributes,
+ LDProvider,
+ ReactNativeLDClient,
+} from '@launchdarkly/react-native-client-sdk';
+
+import Welcome from './src/welcome';
+
+const featureClient = new ReactNativeLDClient(LAUNCHDARKLY_MOBILE_KEY, AutoEnvAttributes.Enabled, {
+ debug: true,
+ applicationInfo: {
+ id: 'ld-rn-fdv2-test-app',
+ version: '0.0.1',
+ },
+ // @ts-ignore dataSystem is @internal
+ dataSystem: {},
+});
+
+const App = () => (
+
+
+
+);
+
+export default App;
diff --git a/packages/sdk/react-native/example-fdv2/README.md b/packages/sdk/react-native/example-fdv2/README.md
new file mode 100644
index 0000000000..6df96da2ea
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/README.md
@@ -0,0 +1,57 @@
+# LaunchDarkly React Native SDK - FDv2 Example App
+
+This is a minimal example app that demonstrates the experimental FDv2 data system
+for the React Native SDK.
+
+> **Note:** FDv2 support is `@internal` and experimental. It is not ready for
+> production use and may change or be removed without notice.
+
+## Features Demonstrated
+
+- SDK initialization with the `dataSystem` option (FDv2 protocol)
+- Connection mode switching for all FDv2 modes:
+ - **Streaming** - real-time flag updates with polling fallback
+ - **Polling** - periodic polling only
+ - **Offline** - cached flags only, no network
+ - **One-Shot** - initialize then stop (no persistent synchronizer)
+ - **Background** - low-frequency polling for background state
+ - **Automatic** - clear the override and use automatic mode selection
+- Context identification
+- Boolean flag evaluation
+
+## Quickstart
+
+1. At the js-core repo root, install dependencies and build:
+
+```shell
+yarn && yarn build
+```
+
+2. Create an `.env` file in this directory (`example-fdv2/`) with your mobile key:
+
+```shell
+LAUNCHDARKLY_MOBILE_KEY=mob-your-mobile-key-here
+```
+
+3. Update the flag key in `src/welcome.tsx` if needed (defaults to `sample-feature`).
+
+4. Run the app:
+
+```shell
+# iOS
+yarn ios
+
+# Android
+yarn android
+```
+
+> **Note:** You may need to run `npx expo prebuild` before the first iOS or
+> Android build.
+
+## Caveats
+
+- **Network-based automatic mode switching** is not yet implemented. The wiring
+ is in place, but `RNStateDetector` does not yet emit network state changes.
+ Lifecycle-based switching (foreground/background) works.
+- The `dataSystem` option and `setConnectionMode()` are marked `@internal` and
+ require `@ts-ignore` to use from TypeScript.
diff --git a/packages/sdk/react-native/example-fdv2/app.json b/packages/sdk/react-native/example-fdv2/app.json
new file mode 100644
index 0000000000..293e8da78f
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/app.json
@@ -0,0 +1,20 @@
+{
+ "expo": {
+ "name": "react-native-example-fdv2",
+ "slug": "react-native-example-fdv2",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "userInterfaceStyle": "light",
+ "assetBundlePatterns": ["**/*"],
+ "ios": {
+ "supportsTablet": true,
+ "bundleIdentifier": "com.anonymous.reactnativeexamplefdv2"
+ },
+ "android": {
+ "adaptiveIcon": {
+ "backgroundColor": "#ffffff"
+ },
+ "package": "com.anonymous.reactnativeexamplefdv2"
+ }
+ }
+}
diff --git a/packages/sdk/react-native/example-fdv2/babel.config.js b/packages/sdk/react-native/example-fdv2/babel.config.js
new file mode 100644
index 0000000000..2b897a7d65
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/babel.config.js
@@ -0,0 +1,15 @@
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ plugins: [
+ [
+ 'module:react-native-dotenv',
+ {
+ safe: true,
+ allowUndefined: false,
+ },
+ ],
+ ],
+ };
+};
diff --git a/packages/sdk/react-native/example-fdv2/index.js b/packages/sdk/react-native/example-fdv2/index.js
new file mode 100644
index 0000000000..202e3f47d8
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/index.js
@@ -0,0 +1,10 @@
+// We have to use a custom entrypoint for monorepo workspaces to work.
+// https://docs.expo.dev/guides/monorepos/#change-default-entrypoint
+import { registerRootComponent } from 'expo';
+
+import App from './App';
+
+// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
+// It also ensures that whether you load the app in Expo Go or in a native build,
+// the environment is set up appropriately
+registerRootComponent(App);
diff --git a/packages/sdk/react-native/example-fdv2/metro.config.js b/packages/sdk/react-native/example-fdv2/metro.config.js
new file mode 100644
index 0000000000..9d52aa2665
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/metro.config.js
@@ -0,0 +1,26 @@
+// We need to use a custom metro config for monorepo workspaces to work.
+// https://docs.expo.dev/guides/monorepos/#modify-the-metro-config
+/**
+ * @type {import('expo/metro-config')}
+ */
+const { getDefaultConfig } = require('expo/metro-config');
+const path = require('path');
+
+// Find the project and workspace directories
+const projectRoot = __dirname;
+// This can be replaced with `find-yarn-workspace-root`
+const workspaceRoot = path.resolve(projectRoot, '../../../..');
+
+const config = getDefaultConfig(projectRoot);
+
+// 1. Watch all files within the monorepo
+config.watchFolders = [workspaceRoot];
+// 2. Let Metro know where to resolve packages and in what order
+config.resolver.nodeModulesPaths = [
+ path.resolve(projectRoot, 'node_modules'),
+ path.resolve(workspaceRoot, 'node_modules'),
+];
+// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
+config.resolver.disableHierarchicalLookup = true;
+
+module.exports = config;
diff --git a/packages/sdk/react-native/example-fdv2/package.json b/packages/sdk/react-native/example-fdv2/package.json
new file mode 100644
index 0000000000..f4f36c32da
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@launchdarkly/react-native-example-fdv2",
+ "private": true,
+ "version": "0.0.1",
+ "main": "index.js",
+ "scripts": {
+ "start": "expo start --reset-cache",
+ "android": "expo run:android",
+ "ios": "expo run:ios",
+ "web": "expo start --web --clear"
+ },
+ "dependencies": {
+ "@launchdarkly/react-native-client-sdk": "10.16.0",
+ "@react-native-async-storage/async-storage": "^2.0.0",
+ "expo": "52.0.14",
+ "expo-status-bar": "~1.11.1",
+ "react": "18.3.1",
+ "react-native": "0.76.3",
+ "react-native-dotenv": "^3.4.9"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.20.0",
+ "@types/react": "~18.2.55",
+ "@types/react-native-dotenv": "^0.2.1",
+ "typescript": "^5.2.2"
+ },
+ "packageManager": "yarn@3.4.1",
+ "installConfig": {
+ "hoistingLimits": "workspaces"
+ }
+}
diff --git a/packages/sdk/react-native/example-fdv2/src/welcome.tsx b/packages/sdk/react-native/example-fdv2/src/welcome.tsx
new file mode 100644
index 0000000000..e142655631
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/src/welcome.tsx
@@ -0,0 +1,160 @@
+import { useState } from 'react';
+import { ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
+
+import {
+ type FDv2ConnectionMode,
+ useBoolVariation,
+ useLDClient,
+} from '@launchdarkly/react-native-client-sdk';
+
+const connectionModes: { label: string; mode?: FDv2ConnectionMode }[] = [
+ { label: 'Streaming', mode: 'streaming' },
+ { label: 'Polling', mode: 'polling' },
+ { label: 'Offline', mode: 'offline' },
+ { label: 'One-Shot', mode: 'one-shot' },
+ { label: 'Background', mode: 'background' },
+ { label: 'Automatic', mode: undefined },
+];
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 20,
+ },
+ title: {
+ fontSize: 22,
+ fontWeight: 'bold',
+ marginBottom: 10,
+ },
+ sectionTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginTop: 10,
+ marginBottom: 5,
+ },
+ modeText: {
+ fontSize: 16,
+ marginBottom: 10,
+ fontStyle: 'italic',
+ },
+ connectionModeContainer: {
+ display: 'flex',
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ gap: 8,
+ },
+ logBox: {
+ flexGrow: 0,
+ backgroundColor: '#1a1a1a',
+ maxHeight: 150,
+ width: '100%',
+ borderRadius: 8,
+ padding: 10,
+ marginVertical: 10,
+ },
+ logText: {
+ color: '#ffa500',
+ fontFamily: 'monospace',
+ },
+ input: {
+ height: 40,
+ width: '80%',
+ margin: 8,
+ borderWidth: 1,
+ borderColor: '#ccc',
+ borderRadius: 8,
+ padding: 10,
+ },
+ buttonContainer: {
+ elevation: 4,
+ backgroundColor: '#009688',
+ borderRadius: 8,
+ paddingVertical: 8,
+ paddingHorizontal: 14,
+ marginBottom: 8,
+ },
+ activeButton: {
+ backgroundColor: '#00695c',
+ borderWidth: 2,
+ borderColor: '#ffffff',
+ },
+ buttonText: {
+ fontSize: 14,
+ color: '#fff',
+ fontWeight: 'bold',
+ alignSelf: 'center',
+ textTransform: 'uppercase',
+ },
+});
+
+export default function Welcome() {
+ const [flagKey, setFlagKey] = useState('sample-feature');
+ const [userKey, setUserKey] = useState('');
+ const [currentMode, setCurrentMode] = useState('streaming');
+ const flagValue = useBoolVariation(flagKey, false);
+ const ldc = useLDClient();
+
+ const onIdentify = () => {
+ ldc
+ .identify({ kind: 'user', key: userKey }, { timeout: 5 })
+ // eslint-disable-next-line no-console
+ .catch((e: any) => console.error(`error identifying ${userKey}: ${e}`));
+ };
+
+ const onSetConnectionMode = (mode?: FDv2ConnectionMode) => {
+ // @ts-ignore setConnectionMode is @internal - experimental FDv2 opt-in
+ ldc.setConnectionMode(mode);
+ setCurrentMode(mode ?? 'automatic');
+ };
+
+ const context = ldc.getContext() ?? 'No context identified.';
+
+ return (
+
+ LaunchDarkly FDv2 Demo
+ Mode: {currentMode}
+
+ {flagKey}: {`${flagValue}`}
+
+
+ Context: {JSON.stringify(context, null, 2)}
+
+
+
+ Identify
+
+
+ Connection Modes
+
+ {connectionModes.map(({ label, mode }) => (
+ onSetConnectionMode(mode)}
+ >
+ {label}
+
+ ))}
+
+
+ );
+}
diff --git a/packages/sdk/react-native/example-fdv2/tsconfig.json b/packages/sdk/react-native/example-fdv2/tsconfig.json
new file mode 100644
index 0000000000..cfbd925faf
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "strict": true,
+ "typeRoots": ["./types"]
+ }
+}
diff --git a/packages/sdk/react-native/example-fdv2/types/env.d.ts b/packages/sdk/react-native/example-fdv2/types/env.d.ts
new file mode 100644
index 0000000000..0d39ec4bf4
--- /dev/null
+++ b/packages/sdk/react-native/example-fdv2/types/env.d.ts
@@ -0,0 +1,4 @@
+declare module '@env' {
+ // eslint-disable-next-line import/prefer-default-export
+ export const LAUNCHDARKLY_MOBILE_KEY: string;
+}
diff --git a/packages/sdk/react-native/tsconfig.json b/packages/sdk/react-native/tsconfig.json
index afbca6d198..c63ab18317 100644
--- a/packages/sdk/react-native/tsconfig.json
+++ b/packages/sdk/react-native/tsconfig.json
@@ -27,6 +27,7 @@
"dist",
"docs",
"example",
+ "example-fdv2",
"node_modules",
"babel.config.js",
"jest.config.ts",
diff --git a/release-please-config.json b/release-please-config.json
index 8aada61d70..75cfd1a6d9 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -122,6 +122,11 @@
"type": "json",
"path": "example/package.json",
"jsonpath": "$.dependencies['@launchdarkly/react-native-client-sdk']"
+ },
+ {
+ "type": "json",
+ "path": "example-fdv2/package.json",
+ "jsonpath": "$.dependencies['@launchdarkly/react-native-client-sdk']"
}
]
},