Skip to content
Draft
41 changes: 33 additions & 8 deletions example-new-architecture/flags/FlagsSourceToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import React, {useState} from 'react';
import {
View,
Text,
Switch,
ActivityIndicator,
StyleSheet,
} from 'react-native';
import {View, Text, Switch, ActivityIndicator, StyleSheet} from 'react-native';

import {setFlagsProvider} from './flagsProvider';
import {setFlagsProvider, setOfflineExampleContext} from './flagsProvider';
import type {FlagsSource} from './flagsProvider';

/**
Expand All @@ -20,13 +14,27 @@ export const FlagsSourceToggle = ({
initialSource?: FlagsSource;
}) => {
const [offline, setOffline] = useState(initialSource === 'offline');
const [included, setIncluded] = useState(true);
const [busy, setBusy] = useState(false);

const onToggle = async (nextOffline: boolean) => {
setBusy(true);
try {
await setFlagsProvider(nextOffline ? 'offline' : 'online');
setOffline(nextOffline);
if (nextOffline) {
setIncluded(true);
}
} finally {
setBusy(false);
}
};

const onAudienceToggle = async (nextIncluded: boolean) => {
setBusy(true);
try {
await setOfflineExampleContext(nextIncluded);
setIncluded(nextIncluded);
} finally {
setBusy(false);
}
Expand All @@ -43,6 +51,19 @@ export const FlagsSourceToggle = ({
onValueChange={onToggle}
disabled={busy}
/>
{offline ? (
<>
<Text style={styles.audienceLabel}>
Rules match: {included ? 'yes' : 'no'}
</Text>
<Switch
accessibilityLabel="offline_rules_context_toggle"
value={included}
onValueChange={onAudienceToggle}
disabled={busy}
/>
</>
) : null}
{busy ? <ActivityIndicator style={styles.spinner} /> : null}
</View>
);
Expand All @@ -60,4 +81,8 @@ const styles = StyleSheet.create({
spinner: {
marginLeft: 10,
},
audienceLabel: {
marginLeft: 16,
marginRight: 10,
},
});
33 changes: 27 additions & 6 deletions example-new-architecture/flags/flagsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import {
} from '@datadog/mobile-react-native-openfeature';
import {OpenFeature} from '@openfeature/react-sdk';

import {buildSampleWire} from './sampleOfflineConfiguration';
import {
buildSampleWire,
DYNAMIC_OFFLINE_CONTEXTS,
} 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.
* - `offline`: loads a complete bundled portable `ConfigurationWire` into
* `DatadogOfflineOpenFeatureProvider` **before** setting it, so flags resolve immediately
* with no network request. The provider does not fetch a UFC response or build this wire.
* - `online`: the standard `DatadogOpenFeatureProvider`, which fetches assignments from the CDN.
*
* The two providers use distinct `clientName`s so each is backed by its own `FlagsClient`.
Expand All @@ -27,14 +31,31 @@ export const setFlagsProvider = async (source: FlagsSource): Promise<void> => {
const provider = new DatadogOfflineOpenFeatureProvider({
clientName: 'offline',
});
provider.setConfiguration(
configurationFromString(buildSampleWire()),
);
provider.setConfiguration(configurationFromString(buildSampleWire()));
await OpenFeature.setProviderAndWait(provider);
await setOfflineExampleContext(true);
return;
}

await OpenFeature.setProviderAndWait(
new DatadogOpenFeatureProvider({clientName: 'online'}),
);
};

/**
* Change the dynamic offline subject without a network request.
*
* Try both calls:
*
* `await setOfflineExampleContext(true);`
* `await setOfflineExampleContext(false);`
*/
export const setOfflineExampleContext = async (
included: boolean,
): Promise<void> => {
await OpenFeature.setContext(
included
? DYNAMIC_OFFLINE_CONTEXTS.included
: DYNAMIC_OFFLINE_CONTEXTS.excluded,
);
};
102 changes: 70 additions & 32 deletions example-new-architecture/flags/sampleOfflineConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,86 @@
// 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',
export const DYNAMIC_OFFLINE_CONTEXTS = {
included: {
targetingKey: 'example-offline-user-a',
country: 'US',
},
excluded: {
targetingKey: 'example-offline-user-b',
country: 'CA',
},
};

/**
* Build a bundled `ConfigurationWire` v1 string for the offline example.
* Build a complete bundled portable rules `ConfigurationWire` string.
*
* 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.
* The example is fully offline. It evaluates the same rules for each new
* OpenFeature context. It does not fetch a UFC response or build a wire at runtime.
*/
export const buildSampleWire = (
context: OfflineWireContext = DEFAULT_OFFLINE_CONTEXT,
variationValue = true,
): string =>
export const buildSampleWire = (): string =>
JSON.stringify({
version: 1,
precomputed: {
context,
// TODO(FFL-2837): Replace this complete legacy `rulesBased` JSON branch
// after a published flagging-core release contains
// DataDog/openfeature-js-client#344 through `9f794c7`, restores
// 32-byte SHA digest validation, and finalizes the runtime contract for
// integer and shard evaluation without global `BigInt`. Reuse the
// production-derived client
// fixture from the integration test: one base64 encoding of the canonical
// dd-source#34959 protobuf bytes in a version 1 `rules.response` envelope.
// Record dd-source#40304 commit `071c4ad` as its schema revision. Let the
// upstream configuration subpath decode it. Do not use the protobuf-free
// precomputed subpath, raw protobuf, the legacy service JSON response, or a
// local strict base64 validator here.
// The final fixture must preserve protobuf integers as `bigint`, return
// `PARSE_ERROR` for unsafe number conversion, tolerate unknown fields,
// preserve them through `configurationToString`, return flag-scoped
// `PARSE_ERROR` for an unsupported feature level, and follow the final
// global-`BigInt` runtime requirement.
rulesBased: {
response: JSON.stringify({
data: {
attributes: {
obfuscated: false,
flags: {
[OFFLINE_FLAG_KEY]: {
variationType: 'boolean',
variationValue,
variationKey: String(variationValue),
allocationKey: 'offline-example-alloc',
reason: 'STATIC',
createdAt: '2026-07-23T12:00:00.000Z',
format: 'SERVER',
environment: {name: 'example'},
flags: {
[OFFLINE_FLAG_KEY]: {
key: OFFLINE_FLAG_KEY,
enabled: true,
variationType: 'BOOLEAN',
variations: {
enabled: {key: 'enabled', value: true},
},
allocations: [
{
key: 'offline-example-alloc',
rules: [
{
conditions: [
{
operator: 'ONE_OF',
attribute: 'country',
value: ['US'],
},
],
},
],
splits: [
{
variationKey: 'enabled',
serialId: 1,
shards: [
{
salt: 'offline-example-salt',
ranges: [{start: 0, end: 100}],
totalShards: 100,
},
],
},
],
doLog: true,
extraLogging: {},
},
},
],
},
},
}),
Expand Down
44 changes: 42 additions & 2 deletions example/src/components/FlagsSourceToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import React, { useState } from 'react';
import { View, Text, Switch, ActivityIndicator, StyleSheet } from 'react-native';
import {
View,
Text,
Switch,
ActivityIndicator,
StyleSheet
} from 'react-native';

import { setFlagsProvider } from '../flags/flagsProvider';
import {
setFlagsProvider,
setOfflineExampleContext
} from '../flags/flagsProvider';
import type { FlagsSource } from '../flags/flagsProvider';

/**
Expand All @@ -14,13 +23,27 @@ export const FlagsSourceToggle = ({
initialSource?: FlagsSource;
}) => {
const [offline, setOffline] = useState(initialSource === 'offline');
const [included, setIncluded] = useState(true);
const [busy, setBusy] = useState(false);

const onToggle = async (nextOffline: boolean) => {
setBusy(true);
try {
await setFlagsProvider(nextOffline ? 'offline' : 'online');
setOffline(nextOffline);
if (nextOffline) {
setIncluded(true);
}
} finally {
setBusy(false);
}
};

const onAudienceToggle = async (nextIncluded: boolean) => {
setBusy(true);
try {
await setOfflineExampleContext(nextIncluded);
setIncluded(nextIncluded);
} finally {
setBusy(false);
}
Expand All @@ -37,6 +60,19 @@ export const FlagsSourceToggle = ({
onValueChange={onToggle}
disabled={busy}
/>
{offline ? (
<>
<Text style={styles.audienceLabel}>
Rules match: {included ? 'yes' : 'no'}
</Text>
<Switch
accessibilityLabel="offline_rules_context_toggle"
value={included}
onValueChange={onAudienceToggle}
disabled={busy}
/>
</>
) : null}
{busy ? <ActivityIndicator style={styles.spinner} /> : null}
</View>
);
Expand All @@ -53,5 +89,9 @@ const styles = StyleSheet.create({
},
spinner: {
marginLeft: 10
},
audienceLabel: {
marginLeft: 16,
marginRight: 10
}
});
39 changes: 28 additions & 11 deletions example/src/flags/flagsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import {
} from '@datadog/mobile-react-native-openfeature';
import { OpenFeature } from '@openfeature/react-sdk';

import { buildSampleWire } from './sampleOfflineConfiguration';
import type { OfflineWireContext } from './sampleOfflineConfiguration';
import {
buildSampleWire,
DYNAMIC_OFFLINE_CONTEXTS
} 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.
* - `offline`: loads a complete bundled portable `ConfigurationWire` into
* `DatadogOfflineOpenFeatureProvider` **before** setting it, so flags resolve immediately
* with no network request. The provider does not fetch a UFC response or build this wire.
* - `online`: the standard `DatadogOpenFeatureProvider`, which fetches assignments from the CDN.
*
* The two providers use distinct `clientName`s so each is backed by its own `FlagsClient`.
Expand All @@ -23,22 +26,36 @@ export type FlagsSource = 'online' | 'offline';
*
* `DdFlags.enable()` must have been called once before this (it enables the native feature).
*/
export const setFlagsProvider = async (
source: FlagsSource,
offlineContext?: OfflineWireContext
): Promise<void> => {
export const setFlagsProvider = async (source: FlagsSource): Promise<void> => {
if (source === 'offline') {
const provider = new DatadogOfflineOpenFeatureProvider({
clientName: 'offline'
});
provider.setConfiguration(
configurationFromString(buildSampleWire(offlineContext))
);
provider.setConfiguration(configurationFromString(buildSampleWire()));
await OpenFeature.setProviderAndWait(provider);
await setOfflineExampleContext(true);
return;
}

await OpenFeature.setProviderAndWait(
new DatadogOpenFeatureProvider({ clientName: 'online' })
);
};

/**
* Change the dynamic offline subject without a network request.
*
* Try both calls:
*
* `await setOfflineExampleContext(true);`
* `await setOfflineExampleContext(false);`
*/
export const setOfflineExampleContext = async (
included: boolean
): Promise<void> => {
await OpenFeature.setContext(
included
? DYNAMIC_OFFLINE_CONTEXTS.included
: DYNAMIC_OFFLINE_CONTEXTS.excluded
);
};
Loading