diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 190667007..679d3a93a 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -12,12 +12,15 @@ import { DdFlags, PropagatorType, } from '@datadog/mobile-react-native'; -import {DatadogOpenFeatureProvider} from '@datadog/mobile-react-native-openfeature'; import { OpenFeature, OpenFeatureProvider, + useBooleanFlagDetails, useObjectFlagDetails, } from '@openfeature/react-sdk'; +import {setFlagsProvider} from './flags/flagsProvider'; +import {OFFLINE_CONTEXT} from './flags/sampleOfflineConfiguration'; +import {FlagsSourceToggle} from './flags/FlagsSourceToggle'; import React, {Suspense} from 'react'; import type {PropsWithChildren} from 'react'; import { @@ -75,9 +78,10 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; // Enable Datadog Flags feature. await DdFlags.enable(); - // Set the provider with OpenFeature. - const provider = new DatadogOpenFeatureProvider(); - OpenFeature.setProvider(provider); + // Set the flags provider. This app defaults to the offline provider (a bundled + // ConfigurationWire, no network); the "Flags source" switch flips to the online provider + // (CDN) at runtime. + await setFlagsProvider('offline'); // Datadog SDK usage examples. await DdRum.startView('main', 'Main'); @@ -94,15 +98,10 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; function AppWithProviders() { React.useEffect(() => { - const user = { - id: 'user-123', - favoriteFruit: 'apple', - }; - - OpenFeature.setContext({ - targetingKey: user.id, - favoriteFruit: user.favoriteFruit, - }); + // Set the same context the bundled offline configuration was precomputed for. Context + // matching is exact (targetingKey AND attributes), so this MUST equal OFFLINE_CONTEXT — + // otherwise the offline provider reports a mismatch and flags fall back to their defaults. + OpenFeature.setContext(OFFLINE_CONTEXT); }, []); return ( @@ -123,6 +122,8 @@ function App(): React.JSX.Element { const greetingFlag = useObjectFlagDetails('rn-sdk-test-json-flag', { greeting: 'Default greeting', }); + // The boolean flag carried by the bundled offline configuration. + const offlineFlag = useBooleanFlagDetails('rn-sdk-test-boolean-flag', false); const isDarkMode = useColorScheme() === 'dark'; const backgroundStyle = { @@ -138,6 +139,27 @@ function App(): React.JSX.Element {
+ + + Offline Feature Flag + + + {offlineFlag.value + ? 'Greetings from the offline Feature Flags!' + : 'Welcome!'} + + + `{offlineFlag.flagKey}` ={' '} + {String(offlineFlag.value)}, + reason {offlineFlag.reason}. + + + +
The title of this section is based on the{' '} diff --git a/example-new-architecture/flags/FlagsSourceToggle.tsx b/example-new-architecture/flags/FlagsSourceToggle.tsx new file mode 100644 index 000000000..5920c0da5 --- /dev/null +++ b/example-new-architecture/flags/FlagsSourceToggle.tsx @@ -0,0 +1,63 @@ +import React, {useState} from 'react'; +import { + View, + Text, + Switch, + ActivityIndicator, + StyleSheet, +} from 'react-native'; + +import {setFlagsProvider} from './flagsProvider'; +import type {FlagsSource} from './flagsProvider'; + +/** + * A runtime switch between the offline (bundled, no network) and online (CDN) flags + * providers. Toggling re-sets the OpenFeature provider, which re-renders any ``. + */ +export const FlagsSourceToggle = ({ + initialSource = 'offline', +}: { + initialSource?: FlagsSource; +}) => { + const [offline, setOffline] = useState(initialSource === 'offline'); + const [busy, setBusy] = useState(false); + + const onToggle = async (nextOffline: boolean) => { + setBusy(true); + try { + await setFlagsProvider(nextOffline ? 'offline' : 'online'); + setOffline(nextOffline); + } finally { + setBusy(false); + } + }; + + return ( + + + Flags source: {offline ? 'offline (bundled)' : 'online (CDN)'} + + + {busy ? : null} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 8, + }, + label: { + marginRight: 10, + }, + spinner: { + marginLeft: 10, + }, +}); diff --git a/example-new-architecture/flags/flagsProvider.ts b/example-new-architecture/flags/flagsProvider.ts new file mode 100644 index 000000000..a3c5b1c1a --- /dev/null +++ b/example-new-architecture/flags/flagsProvider.ts @@ -0,0 +1,32 @@ +import { + DatadogOpenFeatureProvider, + DatadogOfflineOpenFeatureProvider, + configurationFromString, +} from '@datadog/mobile-react-native-openfeature'; +import {OpenFeature} from '@openfeature/react-sdk'; + +import {buildSampleWire, OFFLINE_CONTEXT} from './sampleOfflineConfiguration'; + +export type FlagsSource = 'online' | 'offline'; + +/** + * Select which OpenFeature provider backs flag evaluations, and (re)set it at runtime. + * + * - `offline`: loads a bundled `ConfigurationWire` into `DatadogOfflineOpenFeatureProvider` + * **before** setting it, so flags resolve immediately with no network request. + * - `online`: the standard `DatadogOpenFeatureProvider`, which fetches assignments from the CDN. + * + * `DdFlags.enable()` must have been called once before this (it enables the native feature). + */ +export const setFlagsProvider = async (source: FlagsSource): Promise => { + if (source === 'offline') { + const provider = new DatadogOfflineOpenFeatureProvider(); + provider.setConfiguration( + configurationFromString(buildSampleWire(OFFLINE_CONTEXT)), + ); + await OpenFeature.setProviderAndWait(provider); + return; + } + + await OpenFeature.setProviderAndWait(new DatadogOpenFeatureProvider()); +}; diff --git a/example-new-architecture/flags/sampleOfflineConfiguration.ts b/example-new-architecture/flags/sampleOfflineConfiguration.ts new file mode 100644 index 000000000..fd010e2b6 --- /dev/null +++ b/example-new-architecture/flags/sampleOfflineConfiguration.ts @@ -0,0 +1,54 @@ +// The boolean flag key demonstrated by the offline example. +export const OFFLINE_FLAG_KEY = 'rn-sdk-test-boolean-flag'; + +// Value type kept to JSON primitives so this is assignable to OpenFeature's +// `EvaluationContext` when passed to `OpenFeature.setContext`. +export type OfflineWireContext = {targetingKey?: string} & Record< + string, + string | number | boolean +>; + +// The evaluation context the bundled configuration is precomputed for. This app also calls +// `OpenFeature.setContext` (see App.tsx), so the two MUST be identical — context matching is +// exact (targetingKey AND attributes). A mismatch surfaces PROVIDER_ERROR and the flag falls +// back to its default. +export const OFFLINE_CONTEXT: OfflineWireContext = { + targetingKey: 'example-offline-user', + favoriteFruit: 'apple', +}; + +/** + * Build a bundled `ConfigurationWire` v1 string for the offline example. + * + * Mirrors the shape the Datadog Flags CDN returns, but is bundled with the app so the demo + * is fully offline — it never hits the network. Flip `variationValue` to `false` to confirm + * the flag's fallback renders. + */ +export const buildSampleWire = ( + context: OfflineWireContext = OFFLINE_CONTEXT, + variationValue = true, +): string => + JSON.stringify({ + version: 1, + precomputed: { + context, + response: JSON.stringify({ + data: { + attributes: { + obfuscated: false, + flags: { + [OFFLINE_FLAG_KEY]: { + variationType: 'boolean', + variationValue, + variationKey: String(variationValue), + allocationKey: 'offline-example-alloc', + reason: 'STATIC', + doLog: true, + extraLogging: {}, + }, + }, + }, + }, + }), + }, + }); diff --git a/example/src/App.tsx b/example/src/App.tsx index e6e4ac694..11926f907 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -9,11 +9,11 @@ import style from './screens/styles'; import { navigationRef } from './NavigationRoot'; import { DdRumReactNavigationTracking, NavigationTrackingOptions, ParamsTrackingPredicate, ViewNamePredicate, ViewTrackingPredicate } from '@datadog/mobile-react-navigation'; import { DatadogProvider, TrackingConsent, DdFlags } from '@datadog/mobile-react-native' -import { DatadogOpenFeatureProvider } from '@datadog/mobile-react-native-openfeature'; -import { OpenFeature, OpenFeatureProvider } from '@openfeature/react-sdk'; +import { OpenFeatureProvider } from '@openfeature/react-sdk'; import { Route } from "@react-navigation/native"; import { NestedNavigator } from './screens/NestedNavigator/NestedNavigator'; import { getDatadogConfig, onDatadogInitialization } from './ddUtils'; +import { setFlagsProvider } from './flags/flagsProvider'; const Tab = createBottomTabNavigator(); @@ -74,9 +74,10 @@ const handleDatadogInitialization = async () => { // Enable Datadog Flags feature. await DdFlags.enable(); - // Set the provider with OpenFeature. - const provider = new DatadogOpenFeatureProvider(); - OpenFeature.setProvider(provider); + // Set the flags provider. This example defaults to the offline provider (a bundled + // ConfigurationWire, no network); the "Flags source" switch on the Home screen flips to + // the online provider (CDN) at runtime. + await setFlagsProvider('offline'); } export default function App() { diff --git a/example/src/components/FlagsSourceToggle.tsx b/example/src/components/FlagsSourceToggle.tsx new file mode 100644 index 000000000..40f796b77 --- /dev/null +++ b/example/src/components/FlagsSourceToggle.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { View, Text, Switch, ActivityIndicator, StyleSheet } from 'react-native'; + +import { setFlagsProvider } from '../flags/flagsProvider'; +import type { FlagsSource } from '../flags/flagsProvider'; + +/** + * A runtime switch between the offline (bundled, no network) and online (CDN) flags + * providers. Toggling re-sets the OpenFeature provider, which re-renders any ``. + */ +export const FlagsSourceToggle = ({ + initialSource = 'offline' +}: { + initialSource?: FlagsSource; +}) => { + const [offline, setOffline] = useState(initialSource === 'offline'); + const [busy, setBusy] = useState(false); + + const onToggle = async (nextOffline: boolean) => { + setBusy(true); + try { + await setFlagsProvider(nextOffline ? 'offline' : 'online'); + setOffline(nextOffline); + } finally { + setBusy(false); + } + }; + + return ( + + + Flags source: {offline ? 'offline (bundled)' : 'online (CDN)'} + + + {busy ? : null} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12 + }, + label: { + marginRight: 10 + }, + spinner: { + marginLeft: 10 + } +}); diff --git a/example/src/flags/flagsProvider.ts b/example/src/flags/flagsProvider.ts new file mode 100644 index 000000000..688ba3d80 --- /dev/null +++ b/example/src/flags/flagsProvider.ts @@ -0,0 +1,36 @@ +import { + DatadogOpenFeatureProvider, + DatadogOfflineOpenFeatureProvider, + configurationFromString +} from '@datadog/mobile-react-native-openfeature'; +import { OpenFeature } from '@openfeature/react-sdk'; + +import { buildSampleWire } from './sampleOfflineConfiguration'; +import type { OfflineWireContext } from './sampleOfflineConfiguration'; + +export type FlagsSource = 'online' | 'offline'; + +/** + * Select which OpenFeature provider backs flag evaluations, and (re)set it at runtime. + * + * - `offline`: loads a bundled `ConfigurationWire` into `DatadogOfflineOpenFeatureProvider` + * **before** setting it, so flags resolve immediately with no network request. + * - `online`: the standard `DatadogOpenFeatureProvider`, which fetches assignments from the CDN. + * + * `DdFlags.enable()` must have been called once before this (it enables the native feature). + */ +export const setFlagsProvider = async ( + source: FlagsSource, + offlineContext?: OfflineWireContext +): Promise => { + if (source === 'offline') { + const provider = new DatadogOfflineOpenFeatureProvider(); + provider.setConfiguration( + configurationFromString(buildSampleWire(offlineContext)) + ); + await OpenFeature.setProviderAndWait(provider); + return; + } + + await OpenFeature.setProviderAndWait(new DatadogOpenFeatureProvider()); +}; diff --git a/example/src/flags/sampleOfflineConfiguration.ts b/example/src/flags/sampleOfflineConfiguration.ts new file mode 100644 index 000000000..e62b2b7ee --- /dev/null +++ b/example/src/flags/sampleOfflineConfiguration.ts @@ -0,0 +1,50 @@ +// The flag key shared with the online example, so the UI is comparable across providers. +export const OFFLINE_FLAG_KEY = 'rn-sdk-test-boolean-flag'; + +export type OfflineWireContext = { targetingKey?: string } & Record< + string, + string | number | boolean +>; + +// The evaluation context the bundled configuration is precomputed for. Because the wire +// carries its own context, the app does not need to call `OpenFeature.setContext` for the +// offline flow. +export const DEFAULT_OFFLINE_CONTEXT: OfflineWireContext = { + targetingKey: 'example-offline-user' +}; + +/** + * Build a bundled `ConfigurationWire` v1 string for the offline example. + * + * Mirrors the shape the Datadog Flags CDN returns, but is bundled with the app so the demo + * is fully offline — it never hits the network. Flip `variationValue` to `false` to confirm + * the flag's fallback renders. + */ +export const buildSampleWire = ( + context: OfflineWireContext = DEFAULT_OFFLINE_CONTEXT, + variationValue = true +): string => + JSON.stringify({ + version: 1, + precomputed: { + context, + response: JSON.stringify({ + data: { + attributes: { + obfuscated: false, + flags: { + [OFFLINE_FLAG_KEY]: { + variationType: 'boolean', + variationValue, + variationKey: String(variationValue), + allocationKey: 'offline-example-alloc', + reason: 'STATIC', + doLog: true, + extraLogging: {} + } + } + } + } + }) + } + }); diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index 1616e953d..37f14b8b0 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -12,6 +12,7 @@ import { import { DdLogs, DdSdkReactNative, TrackingConsent, DdFlags } from '@datadog/mobile-react-native'; import { FeatureFlag } from '@openfeature/react-sdk'; import styles from './styles'; +import { FlagsSourceToggle } from '../components/FlagsSourceToggle'; import { APPLICATION_KEY, API_KEY } from '../../src/ddCredentials'; import { getTrackingConsent, saveTrackingConsent } from '../utils'; import { ConsentModal } from '../components/consent'; @@ -114,11 +115,13 @@ export default class MainScreen extends Component { render() { return + + Welcome!}> Greetings from the Feature Flags! - The above greeting is being controlled by the{'\n'}`rn-sdk-test-boolean-flag` feature flag. + The above greeting is being controlled by the{'\n'}`rn-sdk-test-boolean-flag` feature flag,{'\n'}resolved from the source selected above.