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,
},
});
29 changes: 26 additions & 3 deletions example-new-architecture/flags/flagsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,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 @@ -37,10 +41,29 @@ export const setFlagsProvider = async (source: FlagsSource): Promise<void> => {
});
provider.setConfiguration(configuration);
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,
);
};
105 changes: 73 additions & 32 deletions example-new-architecture/flags/sampleOfflineConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,89 @@
// 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 `03cde21` and restores
// 32-byte SHA digest validation. Safe integer conversion no longer calls
// global `BigInt`; retain tests for unsafe integers and shard values.
// The `03cde21` tree is identical to the previous `939da97` tree. Commits
// `1db13d4` and `03cde21` only refresh generated Node-server declarations
// and isolate browser provider tests. They do not change the portable wire
// or runtime contract. 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 work without the
// global `BigInt` function.
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
}
});
35 changes: 27 additions & 8 deletions example/src/flags/flagsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,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 @@ -24,10 +27,7 @@ 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 configuration = configurationFromString(
buildSampleWire(offlineContext)
Expand All @@ -43,10 +43,29 @@ export const setFlagsProvider = async (
});
provider.setConfiguration(configuration);
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