Skip to content

Commit 6f8f006

Browse files
committed
feat: React Native FDv2 support.
1 parent d766f39 commit 6f8f006

3 files changed

Lines changed: 207 additions & 63 deletions

File tree

packages/sdk/react-native/src/RNOptions.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { ConnectionMode, LDOptions } from '@launchdarkly/js-client-sdk-common';
1+
import {
2+
ConnectionMode,
3+
LDClientDataSystemOptions,
4+
LDOptions,
5+
} from '@launchdarkly/js-client-sdk-common';
26

37
import { LDPlugin } from './LDPlugin';
48

@@ -60,6 +64,23 @@ export interface RNStorage {
6064
clear: (key: string) => Promise<void>;
6165
}
6266

67+
/**
68+
* Data system options for the React Native SDK.
69+
*
70+
* React Native supports the full range of automatic mode switching options,
71+
* including lifecycle-based (foreground/background) and network-based switching.
72+
*
73+
* Note: Network-based automatic mode switching is not yet supported.
74+
* Lifecycle-based switching (foreground/background) is fully functional.
75+
*
76+
* This interface is not stable, and not subject to any backwards compatibility
77+
* guarantees or semantic versioning. It is in early access. If you want access
78+
* to this feature please join the EAP.
79+
* https://launchdarkly.com/docs/sdk/features/data-saving-mode
80+
*/
81+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
82+
export interface RNDataSystemOptions extends LDClientDataSystemOptions {}
83+
6384
export interface RNSpecificOptions {
6485
/**
6586
* Some platforms (windows, web, mac, linux) can continue executing code
@@ -118,6 +139,22 @@ export interface RNSpecificOptions {
118139
* Plugin support is currently experimental and subject to change.
119140
*/
120141
plugins?: LDPlugin[];
142+
143+
/**
144+
* @internal
145+
*
146+
* This feature is experimental and should NOT be considered ready for
147+
* production use. It may change or be removed without notice and is not
148+
* subject to backwards compatibility guarantees.
149+
*
150+
* Configuration for the FDv2 data system. When present, the SDK uses
151+
* the FDv2 protocol for flag delivery instead of the default FDv1
152+
* protocol.
153+
*
154+
* Note: Network-based automatic mode switching is not yet supported.
155+
* Lifecycle-based switching (foreground/background) is fully functional.
156+
*/
157+
dataSystem?: RNDataSystemOptions;
121158
}
122159

123160
export default interface RNOptions extends LDOptions, RNSpecificOptions {}

packages/sdk/react-native/src/ReactNativeLDClient.ts

Lines changed: 167 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import {
44
BasicLogger,
55
type Configuration,
66
ConnectionMode,
7+
createDefaultSourceFactoryProvider,
8+
createFDv2DataManagerBase,
9+
FDv2ConnectionMode,
10+
type FDv2DataManagerControl,
711
FlagManager,
812
internal,
913
LDClientImpl,
@@ -12,13 +16,21 @@ import {
1216
LDHeaders,
1317
LDPluginEnvironmentMetadata,
1418
MOBILE_DATA_SYSTEM_DEFAULTS,
19+
MOBILE_TRANSITION_TABLE,
1520
mobileFdv1Endpoints,
21+
MODE_TABLE,
22+
resolveForegroundMode,
1623
} from '@launchdarkly/js-client-sdk-common';
1724

1825
import MobileDataManager from './MobileDataManager';
1926
import validateOptions, { filterToBaseOptions } from './options';
2027
import createPlatform from './platform';
21-
import { ConnectionDestination, ConnectionManager } from './platform/ConnectionManager';
28+
import {
29+
ApplicationState,
30+
ConnectionDestination,
31+
ConnectionManager,
32+
NetworkState,
33+
} from './platform/ConnectionManager';
2234
import LDOptions from './RNOptions';
2335
import RNStateDetector from './RNStateDetector';
2436

@@ -36,7 +48,8 @@ import RNStateDetector from './RNStateDetector';
3648
* ```
3749
*/
3850
export default class ReactNativeLDClient extends LDClientImpl {
39-
private _connectionManager: ConnectionManager;
51+
private _connectionManager?: ConnectionManager;
52+
private _stateDetector?: RNStateDetector;
4053
/**
4154
* Creates an instance of the LaunchDarkly client.
4255
*
@@ -72,62 +85,114 @@ export default class ReactNativeLDClient extends LDClientImpl {
7285
const platform = createPlatform(logger, options, validatedRnOptions.storage);
7386
const endpoints = mobileFdv1Endpoints();
7487

88+
const dataManagerFactory = (
89+
flagManager: FlagManager,
90+
configuration: Configuration,
91+
baseHeaders: LDHeaders,
92+
emitter: LDEmitter,
93+
diagnosticsManager?: internal.DiagnosticsManager,
94+
) => {
95+
if (configuration.dataSystem) {
96+
return createFDv2DataManagerBase({
97+
platform,
98+
flagManager,
99+
credential: sdkKey,
100+
config: configuration,
101+
baseHeaders,
102+
emitter,
103+
transitionTable: MOBILE_TRANSITION_TABLE,
104+
foregroundMode: resolveForegroundMode(
105+
configuration.dataSystem,
106+
MOBILE_DATA_SYSTEM_DEFAULTS,
107+
),
108+
backgroundMode: configuration.dataSystem.backgroundConnectionMode ?? 'background',
109+
modeTable: MODE_TABLE,
110+
sourceFactoryProvider: createDefaultSourceFactoryProvider(),
111+
fdv1Endpoints: mobileFdv1Endpoints(),
112+
buildQueryParams: () => [], // Mobile uses Authorization header, not query params
113+
});
114+
}
115+
116+
return new MobileDataManager(
117+
platform,
118+
flagManager,
119+
sdkKey,
120+
configuration,
121+
validatedRnOptions,
122+
endpoints.polling,
123+
endpoints.streaming,
124+
baseHeaders,
125+
emitter,
126+
diagnosticsManager,
127+
);
128+
};
129+
75130
super(
76131
sdkKey,
77132
autoEnvAttributes,
78133
platform,
79134
{ ...filterToBaseOptions(options), logger },
80-
(
81-
flagManager: FlagManager,
82-
configuration: Configuration,
83-
baseHeaders: LDHeaders,
84-
emitter: LDEmitter,
85-
diagnosticsManager?: internal.DiagnosticsManager,
86-
) =>
87-
new MobileDataManager(
88-
platform,
89-
flagManager,
90-
sdkKey,
91-
configuration,
92-
validatedRnOptions,
93-
endpoints.polling,
94-
endpoints.streaming,
95-
baseHeaders,
96-
emitter,
97-
diagnosticsManager,
98-
),
135+
dataManagerFactory,
99136
internalOptions,
100137
);
101138

102-
this.setEventSendingEnabled(!this.isOffline(), false);
103-
104-
const dataManager = this.dataManager as MobileDataManager;
105-
const destination: ConnectionDestination = {
106-
setNetworkAvailability: (available: boolean) => {
107-
dataManager.setNetworkAvailability(available);
108-
},
109-
setEventSendingEnabled: (enabled: boolean, flush: boolean) => {
110-
this.setEventSendingEnabled(enabled, flush);
111-
},
112-
setConnectionMode: async (mode: ConnectionMode) => {
113-
// Pass the connection mode to the base implementation.
114-
// The RN implementation will pass the connection mode through the connection manager.
115-
dataManager.setConnectionMode(mode);
116-
},
117-
};
139+
const isFDv2 = !!options.dataSystem;
140+
141+
if (isFDv2) {
142+
const fdv2DataManager = this.dataManager as FDv2DataManagerControl;
143+
144+
this.setEventSendingEnabled(true, false);
145+
fdv2DataManager.setFlushCallback(() => this.flush());
146+
147+
// Wire state detection directly to FDv2 data manager.
148+
const stateDetector = new RNStateDetector();
149+
this._stateDetector = stateDetector;
150+
151+
if (validatedRnOptions.automaticBackgroundHandling) {
152+
stateDetector.setApplicationStateListener((state) => {
153+
fdv2DataManager.setLifecycleState(
154+
state === ApplicationState.Foreground ? 'foreground' : 'background',
155+
);
156+
});
157+
}
158+
159+
if (validatedRnOptions.automaticNetworkHandling) {
160+
stateDetector.setNetworkStateListener((state) => {
161+
fdv2DataManager.setNetworkState(
162+
state === NetworkState.Available ? 'available' : 'unavailable',
163+
);
164+
});
165+
}
166+
} else {
167+
const initialConnectionMode = options.initialConnectionMode ?? 'streaming';
168+
this.setEventSendingEnabled(initialConnectionMode !== 'offline', false);
169+
170+
const dataManager = this.dataManager as MobileDataManager;
171+
const destination: ConnectionDestination = {
172+
setNetworkAvailability: (available: boolean) => {
173+
dataManager.setNetworkAvailability(available);
174+
},
175+
setEventSendingEnabled: (enabled: boolean, flush: boolean) => {
176+
this.setEventSendingEnabled(enabled, flush);
177+
},
178+
setConnectionMode: async (mode: ConnectionMode) => {
179+
dataManager.setConnectionMode(mode);
180+
},
181+
};
182+
183+
this._connectionManager = new ConnectionManager(
184+
logger,
185+
{
186+
initialConnectionMode,
187+
automaticNetworkHandling: validatedRnOptions.automaticNetworkHandling,
188+
automaticBackgroundHandling: validatedRnOptions.automaticBackgroundHandling,
189+
runInBackground: validatedRnOptions.runInBackground,
190+
},
191+
destination,
192+
new RNStateDetector(),
193+
);
194+
}
118195

119-
const initialConnectionMode = options.initialConnectionMode ?? 'streaming';
120-
this._connectionManager = new ConnectionManager(
121-
logger,
122-
{
123-
initialConnectionMode,
124-
automaticNetworkHandling: validatedRnOptions.automaticNetworkHandling,
125-
automaticBackgroundHandling: validatedRnOptions.automaticBackgroundHandling,
126-
runInBackground: validatedRnOptions.runInBackground,
127-
},
128-
destination,
129-
new RNStateDetector(),
130-
);
131196
internal.safeRegisterPlugins(
132197
logger,
133198
this.environmentMetadata,
@@ -136,24 +201,66 @@ export default class ReactNativeLDClient extends LDClientImpl {
136201
);
137202
}
138203

139-
async setConnectionMode(mode: ConnectionMode): Promise<void> {
140-
// Set the connection mode before setting offline, in case there is any mode transition work
141-
// such as flushing on entering the background.
142-
this._connectionManager.setConnectionMode(mode);
143-
// For now the data source connection and the event processing state are connected.
144-
this._connectionManager.setOffline(mode === 'offline');
204+
/**
205+
* @internal
206+
*
207+
* This feature is experimental and should NOT be considered ready for
208+
* production use. It may change or be removed without notice and is not
209+
* subject to backwards compatibility guarantees.
210+
*
211+
* Sets the connection mode for the SDK's data system.
212+
*
213+
* When the FDv2 data system is enabled (`dataSystem` option), this method
214+
* accepts FDv2 connection modes (`'streaming'`, `'polling'`, `'offline'`,
215+
* `'one-shot'`, `'background'`). Pass `undefined` to clear an explicit
216+
* override and return to automatic mode selection.
217+
*
218+
* Without FDv2, this method accepts FDv1 connection modes
219+
* (`'streaming'`, `'polling'`, `'offline'`).
220+
*
221+
* @param mode The connection mode to use, or `undefined` to clear the
222+
* override (FDv2 only).
223+
*/
224+
async setConnectionMode(mode?: ConnectionMode | FDv2ConnectionMode): Promise<void> {
225+
if (this._connectionManager) {
226+
// FDv1 path
227+
if (mode === undefined || mode === 'one-shot' || mode === 'background') {
228+
this.logger.warn(
229+
`setConnectionMode('${mode}') is only supported with the FDv2 data system (dataSystem option).`,
230+
);
231+
return;
232+
}
233+
this._connectionManager.setConnectionMode(mode as ConnectionMode);
234+
this._connectionManager.setOffline(mode === 'offline');
235+
} else {
236+
// FDv2 path
237+
if (mode !== undefined && !(mode in MODE_TABLE)) {
238+
this.logger.warn(
239+
`setConnectionMode called with invalid mode '${mode}'. ` +
240+
`Valid modes: ${Object.keys(MODE_TABLE).join(', ')}.`,
241+
);
242+
return;
243+
}
244+
(this.dataManager as FDv2DataManagerControl).setConnectionMode(
245+
mode as FDv2ConnectionMode | undefined,
246+
);
247+
}
145248
}
146249

147250
/**
148251
* Gets the SDK connection mode.
149252
*/
150-
getConnectionMode(): ConnectionMode {
151-
const dataManager = this.dataManager as MobileDataManager;
152-
return dataManager.getConnectionMode();
253+
getConnectionMode(): ConnectionMode | FDv2ConnectionMode {
254+
if (this._connectionManager) {
255+
return (this.dataManager as MobileDataManager).getConnectionMode();
256+
}
257+
return (this.dataManager as FDv2DataManagerControl).getCurrentMode();
153258
}
154259

155260
isOffline() {
156-
const dataManager = this.dataManager as MobileDataManager;
157-
return dataManager.getConnectionMode() === 'offline';
261+
if (this._connectionManager) {
262+
return (this.dataManager as MobileDataManager).getConnectionMode() === 'offline';
263+
}
264+
return (this.dataManager as FDv2DataManagerControl).getCurrentMode() === 'offline';
158265
}
159266
}

packages/sdk/react-native/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* @packageDocumentation
77
*/
88
import ReactNativeLDClient from './ReactNativeLDClient';
9-
import RNOptions, { RNStorage } from './RNOptions';
9+
import RNOptions, { RNDataSystemOptions, RNStorage } from './RNOptions';
1010

1111
export * from '@launchdarkly/js-client-sdk-common';
1212

@@ -21,4 +21,4 @@ export type {
2121
LDEvaluationDetail,
2222
} from './hooks/variation/LDEvaluationDetail';
2323

24-
export { ReactNativeLDClient, RNOptions as LDOptions, RNStorage };
24+
export { ReactNativeLDClient, RNOptions as LDOptions, RNDataSystemOptions, RNStorage };

0 commit comments

Comments
 (0)