Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
},
});
28 changes: 24 additions & 4 deletions example-new-architecture/flags/flagsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ 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';

Expand All @@ -27,14 +30,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,
);
};
87 changes: 55 additions & 32 deletions example-new-architecture/flags/sampleOfflineConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,71 @@
// 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 bundled 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 assignments.
*/
export const buildSampleWire = (
context: OfflineWireContext = DEFAULT_OFFLINE_CONTEXT,
variationValue = true,
): string =>
export const buildSampleWire = (): string =>
JSON.stringify({
version: 1,
precomputed: {
context,
rulesBased: {
// TODO(FFL-2837): Replace this legacy rulesBased JSON fixture with
// a protobuf rules wire after flagging-core publishes PR #344.
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
}
});
34 changes: 25 additions & 9 deletions example/src/flags/flagsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ 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';

Expand All @@ -23,22 +25,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