From cbfd5b13dd0935be371664b936780163a436e143 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 27 Nov 2023 23:01:54 -0800 Subject: [PATCH 01/24] chore: initial commit. --- .eslintrc.js | 1 + packages/sdk/react-native/example/App.tsx | 48 +-- packages/sdk/react-native/example/README.md | 2 +- .../sdk/react-native/example/src/welcome.tsx | 30 ++ .../sdk/react-native/example/types/env.d.ts | 2 +- packages/sdk/react-native/link-dev.sh | 16 +- packages/sdk/react-native/package.json | 4 +- .../react-native/src/ReactNativeLDClient.ts | 9 + packages/sdk/react-native/src/hooks/index.ts | 6 + .../sdk/react-native/src/hooks/useLDClient.ts | 10 + .../src/hooks/useLDDataSourceStatus.ts | 10 + .../react-native/src/hooks/variation/index.ts | 2 + .../src/hooks/variation/useTypedVariation.ts | 76 ++++ .../src/hooks/variation/useVariation.ts | 25 ++ packages/sdk/react-native/src/index.ts | 6 +- packages/sdk/react-native/src/init.ts | 11 - .../react-native/src/provider/LDProvider.tsx | 32 ++ .../sdk/react-native/src/provider/index.ts | 4 + .../react-native/src/provider/reactContext.ts | 21 + .../src/provider/setupListeners.ts | 38 ++ packages/shared/common/src/Context.ts | 2 +- .../src/internal/stream/StreamingProcessor.ts | 3 +- .../shared/sdk-client/src/LDClientImpl.ts | 147 +++++-- .../shared/sdk-client/src/api/LDClient.ts | 362 +++++++++++++----- .../shared/sdk-client/src/api/LDEmitter.ts | 9 +- 25 files changed, 683 insertions(+), 193 deletions(-) create mode 100644 packages/sdk/react-native/example/src/welcome.tsx create mode 100644 packages/sdk/react-native/src/ReactNativeLDClient.ts create mode 100644 packages/sdk/react-native/src/hooks/index.ts create mode 100644 packages/sdk/react-native/src/hooks/useLDClient.ts create mode 100644 packages/sdk/react-native/src/hooks/useLDDataSourceStatus.ts create mode 100644 packages/sdk/react-native/src/hooks/variation/index.ts create mode 100644 packages/sdk/react-native/src/hooks/variation/useTypedVariation.ts create mode 100644 packages/sdk/react-native/src/hooks/variation/useVariation.ts delete mode 100644 packages/sdk/react-native/src/init.ts create mode 100644 packages/sdk/react-native/src/provider/LDProvider.tsx create mode 100644 packages/sdk/react-native/src/provider/index.ts create mode 100644 packages/sdk/react-native/src/provider/reactContext.ts create mode 100644 packages/sdk/react-native/src/provider/setupListeners.ts diff --git a/.eslintrc.js b/.eslintrc.js index 98be7e8ec..4a0af77b6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,6 +25,7 @@ module.exports = { ], 'import/default': 'error', 'import/export': 'error', + 'import/extensions': ['error', 'never', { json: 'always' }], 'import/no-self-import': 'error', 'import/no-cycle': 'error', 'import/no-useless-path-segments': 'error', diff --git a/packages/sdk/react-native/example/App.tsx b/packages/sdk/react-native/example/App.tsx index 93675937b..56634e16e 100644 --- a/packages/sdk/react-native/example/App.tsx +++ b/packages/sdk/react-native/example/App.tsx @@ -1,44 +1,18 @@ -import { CLIENT_SIDE_SDK_KEY } from '@env'; -import React, { useEffect, useState } from 'react'; -import { StyleSheet, Text, View } from 'react-native'; +import { MOBILE_KEY } from '@env'; -import { init, type LDClientImpl } from '@launchdarkly/react-native-client-sdk'; +import { LDProvider, ReactNativeLDClient } from '@launchdarkly/react-native-client-sdk'; -const context = { kind: 'user', key: 'test-user-1' }; - -export default function App() { - const [ldc, setLdc] = useState(); - const [flag, setFlag] = useState(false); +import Welcome from './src/welcome'; - useEffect(() => { - init(CLIENT_SIDE_SDK_KEY, context) - .then((c) => { - setLdc(c); - }) - .catch((e) => console.log(e)); - }, []); - - useEffect(() => { - const f = ldc?.boolVariation('dev-test-flag', false); - setFlag(f ?? false); - }, [ldc]); +const featureClient = new ReactNativeLDClient(MOBILE_KEY); +const context = { kind: 'user', key: 'test-user-1' }; +const App = () => { return ( - - {flag ? <>devTestFlag: {`${flag}`} : <>loading...} - + + + ); -} +}; -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - box: { - width: 60, - height: 60, - marginVertical: 20, - }, -}); +export default App; diff --git a/packages/sdk/react-native/example/README.md b/packages/sdk/react-native/example/README.md index bbd8c9b0a..14b9e17b1 100644 --- a/packages/sdk/react-native/example/README.md +++ b/packages/sdk/react-native/example/README.md @@ -6,7 +6,7 @@ To run the example app: 2. Add your client-side sdk key to that `.env` file: ```shell -CLIENT_SIDE_SDK_KEY=abcdef12456 +MOBILE_KEY=abcdef12456 ``` 3. Finally diff --git a/packages/sdk/react-native/example/src/welcome.tsx b/packages/sdk/react-native/example/src/welcome.tsx new file mode 100644 index 000000000..de2adf4fd --- /dev/null +++ b/packages/sdk/react-native/example/src/welcome.tsx @@ -0,0 +1,30 @@ +import { StyleSheet, Text, View } from 'react-native'; + +import { useBoolVariation, useLDDataSourceStatus } from '@launchdarkly/react-native-client-sdk'; + +export default function Welcome() { + const { error, status } = useLDDataSourceStatus(); + const flag = useBoolVariation('dev-test-flag', false); + + return ( + + Welcome to LaunchDarkly + status: {status} + error: {error?.message} + devTestFlag: {`${flag}`} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + box: { + width: 60, + height: 60, + marginVertical: 20, + }, +}); diff --git a/packages/sdk/react-native/example/types/env.d.ts b/packages/sdk/react-native/example/types/env.d.ts index 8b0684a33..1ed74a4b5 100644 --- a/packages/sdk/react-native/example/types/env.d.ts +++ b/packages/sdk/react-native/example/types/env.d.ts @@ -1,4 +1,4 @@ declare module '@env' { // eslint-disable-next-line import/prefer-default-export - export const CLIENT_SIDE_SDK_KEY: string; + export const MOBILE_KEY: string; } diff --git a/packages/sdk/react-native/link-dev.sh b/packages/sdk/react-native/link-dev.sh index 9bebafd6a..da6e062ed 100755 --- a/packages/sdk/react-native/link-dev.sh +++ b/packages/sdk/react-native/link-dev.sh @@ -18,13 +18,19 @@ do mkdir -p "$COMMON_DIR" mkdir -p "$CLIENT_COMMON_DIR" + rsync -av dist "$SDK_DIR" + rsync -aq src "$SDK_DIR" rsync -aq package.json "$SDK_DIR" rsync -aq LICENSE "$SDK_DIR" - rsync -aq node_modules "$SDK_DIR" - rsync -aq src "$SDK_DIR" - rsync -av dist "$SDK_DIR" + rsync -aq node_modules/base64-js "$SDK_DIR"/node_modules + rsync -aq node_modules/event-target-shim "$SDK_DIR"/node_modules + + rsync -aq ../../shared/common/dist "$COMMON_DIR" + rsync -aq ../../shared/common/src "$COMMON_DIR" + rsync -aq ../../shared/common/package.json "$COMMON_DIR" - rsync -aq ../../shared/common/ "$COMMON_DIR" rm -rf "$CLIENT_COMMON_DIR" - rsync -aq ../../shared/sdk-client/ "$CLIENT_COMMON_DIR" + rsync -aq ../../shared/sdk-client/dist "$CLIENT_COMMON_DIR" + rsync -aq ../../shared/sdk-client/src "$CLIENT_COMMON_DIR" + rsync -aq ../../shared/sdk-client/package.json "$CLIENT_COMMON_DIR" done diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index b042f1152..a5349c85a 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -40,7 +40,7 @@ "ios": "yarn ./example && yarn build && yarn ./example ios" }, "peerDependencies": { - "react-native": "*" + "react": "*" }, "dependencies": { "@launchdarkly/js-client-sdk-common": "0.0.1", @@ -50,6 +50,7 @@ "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/jest": "^29.5.0", + "@types/react": "^18.2.31", "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "eslint": "^8.45.0", @@ -61,6 +62,7 @@ "jest": "^29.5.0", "launchdarkly-js-test-helpers": "^2.2.0", "prettier": "^3.0.0", + "react": "^18.2.0", "rimraf": "^5.0.5", "ts-jest": "^29.1.0", "typedoc": "0.25.0", diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts new file mode 100644 index 000000000..4886731c6 --- /dev/null +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -0,0 +1,9 @@ +import { LDClientImpl, LDOptions } from '@launchdarkly/js-client-sdk-common'; + +import platform from './platform'; + +export default class ReactNativeLDClient extends LDClientImpl { + constructor(sdkKey: string, options: LDOptions = {}) { + super(sdkKey, platform, options); + } +} diff --git a/packages/sdk/react-native/src/hooks/index.ts b/packages/sdk/react-native/src/hooks/index.ts new file mode 100644 index 000000000..5cbe59c5e --- /dev/null +++ b/packages/sdk/react-native/src/hooks/index.ts @@ -0,0 +1,6 @@ +import useLDClient from './useLDClient'; +import useLDDataSourceStatus from './useLDDataSourceStatus'; + +export * from './variation'; + +export { useLDDataSourceStatus, useLDClient }; diff --git a/packages/sdk/react-native/src/hooks/useLDClient.ts b/packages/sdk/react-native/src/hooks/useLDClient.ts new file mode 100644 index 000000000..22cd561c9 --- /dev/null +++ b/packages/sdk/react-native/src/hooks/useLDClient.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; + +import { context, ReactContext } from '../provider/reactContext'; + +const useLDClient = () => { + const { client } = useContext(context); + return client; +}; + +export default useLDClient; diff --git a/packages/sdk/react-native/src/hooks/useLDDataSourceStatus.ts b/packages/sdk/react-native/src/hooks/useLDDataSourceStatus.ts new file mode 100644 index 000000000..2b775de82 --- /dev/null +++ b/packages/sdk/react-native/src/hooks/useLDDataSourceStatus.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; + +import { context, ReactContext } from '../provider/reactContext'; + +const useLDDataSourceStatus = () => { + const { dataSource } = useContext(context); + return dataSource; +}; + +export default useLDDataSourceStatus; diff --git a/packages/sdk/react-native/src/hooks/variation/index.ts b/packages/sdk/react-native/src/hooks/variation/index.ts new file mode 100644 index 000000000..ee37f1e8c --- /dev/null +++ b/packages/sdk/react-native/src/hooks/variation/index.ts @@ -0,0 +1,2 @@ +export * from './useVariation'; +export * from './useTypedVariation'; diff --git a/packages/sdk/react-native/src/hooks/variation/useTypedVariation.ts b/packages/sdk/react-native/src/hooks/variation/useTypedVariation.ts new file mode 100644 index 000000000..006be89a1 --- /dev/null +++ b/packages/sdk/react-native/src/hooks/variation/useTypedVariation.ts @@ -0,0 +1,76 @@ +import { useMemo } from 'react'; + +import { LDEvaluationDetailTyped } from '@launchdarkly/js-client-sdk-common'; + +import useLDClient from '../useLDClient'; +import useLDDataSourceStatus from '../useLDDataSourceStatus'; + +export const useTypedVariation = ( + flagKey: string, + defaultValue: T, +): T => { + const ldClient = useLDClient(); + const { status } = useLDDataSourceStatus(); + + if (status === 'ready') { + switch (typeof defaultValue) { + case 'boolean': + return ldClient.boolVariation(flagKey, defaultValue as boolean) as T; + case 'number': + return ldClient.numberVariation(flagKey, defaultValue as number) as T; + case 'string': + return ldClient.stringVariation(flagKey, defaultValue as string) as T; + case 'undefined': + case 'object': + return ldClient.jsonVariation(flagKey, defaultValue) as T; + default: + return defaultValue; + } + } + + return defaultValue; +}; + +export const useTypedVariationDetail = ( + flagKey: string, + defaultValue: T, +): LDEvaluationDetailTyped => { + const ldClient = useLDClient(); + const { status } = useLDDataSourceStatus(); + const unsupportedType = useMemo( + () => ({ + value: defaultValue, + variationIndex: null, + reason: { kind: 'ERROR', errorKind: 'UNSUPPORTED_VARIATION_TYPE' }, + }), + [defaultValue], + ); + + if (status === 'ready') { + switch (typeof defaultValue) { + case 'boolean': + return ldClient.boolVariationDetail( + flagKey, + defaultValue as boolean, + ) as LDEvaluationDetailTyped; + case 'number': + return ldClient.numberVariationDetail( + flagKey, + defaultValue as number, + ) as LDEvaluationDetailTyped; + case 'string': + return ldClient.stringVariationDetail( + flagKey, + defaultValue as string, + ) as LDEvaluationDetailTyped; + case 'undefined': + case 'object': + return ldClient.jsonVariationDetail(flagKey, defaultValue) as LDEvaluationDetailTyped; + default: { + return unsupportedType; + } + } + } + + return unsupportedType; +}; diff --git a/packages/sdk/react-native/src/hooks/variation/useVariation.ts b/packages/sdk/react-native/src/hooks/variation/useVariation.ts new file mode 100644 index 000000000..512d91709 --- /dev/null +++ b/packages/sdk/react-native/src/hooks/variation/useVariation.ts @@ -0,0 +1,25 @@ +import { useTypedVariation, useTypedVariationDetail } from './useTypedVariation'; + +export const useBoolVariation = (flagKey: string, defaultValue: boolean) => + useTypedVariation(flagKey, defaultValue); + +export const useBoolVariationDetail = (flagKey: string, defaultValue: boolean) => + useTypedVariationDetail(flagKey, defaultValue); + +export const useNumberVariation = (flagKey: string, defaultValue: number) => + useTypedVariation(flagKey, defaultValue); + +export const useNumberVariationDetail = (flagKey: string, defaultValue: number) => + useTypedVariationDetail(flagKey, defaultValue); + +export const useStringVariation = (flagKey: string, defaultValue: string) => + useTypedVariation(flagKey, defaultValue); + +export const useStringVariationDetail = (flagKey: string, defaultValue: string) => + useTypedVariationDetail(flagKey, defaultValue); + +export const useJsonVariation = (flagKey: string, defaultValue: unknown) => + useTypedVariation(flagKey, defaultValue); + +export const useJsonVariationDetail = (flagKey: string, defaultValue: unknown) => + useTypedVariationDetail(flagKey, defaultValue); diff --git a/packages/sdk/react-native/src/index.ts b/packages/sdk/react-native/src/index.ts index acabdb114..12f3a3769 100644 --- a/packages/sdk/react-native/src/index.ts +++ b/packages/sdk/react-native/src/index.ts @@ -7,11 +7,13 @@ * * @packageDocumentation */ -import init from './init'; import { setupPolyfill } from './polyfills'; +import ReactNativeLDClient from './ReactNativeLDClient'; setupPolyfill(); export * from '@launchdarkly/js-client-sdk-common'; -export { init }; +export * from './hooks'; +export * from './provider'; +export { ReactNativeLDClient }; diff --git a/packages/sdk/react-native/src/init.ts b/packages/sdk/react-native/src/init.ts deleted file mode 100644 index 9e32dd20f..000000000 --- a/packages/sdk/react-native/src/init.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { LDClientImpl, LDContext, LDOptions } from '@launchdarkly/js-client-sdk-common'; - -import platform from './platform'; - -const init = async (sdkKey: string, context: LDContext, options: LDOptions = {}) => { - const ldc = new LDClientImpl(sdkKey, context, platform, options); - await ldc.start(); - return ldc; -}; - -export default init; diff --git a/packages/sdk/react-native/src/provider/LDProvider.tsx b/packages/sdk/react-native/src/provider/LDProvider.tsx new file mode 100644 index 000000000..e57ed12b1 --- /dev/null +++ b/packages/sdk/react-native/src/provider/LDProvider.tsx @@ -0,0 +1,32 @@ +import { PropsWithChildren, useEffect, useState } from 'react'; + +import { type LDContext } from '@launchdarkly/js-client-sdk-common'; + +import ReactNativeLDClient from '../ReactNativeLDClient'; +import { Provider, ReactContext } from './reactContext'; +import setupListeners from './setupListeners'; + +type LDProps = { + client: ReactNativeLDClient; + context?: LDContext; +}; + +const LDProvider = ({ client, context, children }: PropsWithChildren) => { + const [state, setState] = useState({ client, context, dataSource: {} }); + + useEffect(() => { + setupListeners(client, setState); + + if (context) { + client + .identify(context) + .catch((e: any) => + client.logger.debug(`LaunchDarkly React Native Sdk identify error: ${e}`), + ); + } + }, []); + + return {children}; +}; + +export default LDProvider; diff --git a/packages/sdk/react-native/src/provider/index.ts b/packages/sdk/react-native/src/provider/index.ts new file mode 100644 index 000000000..6706518fb --- /dev/null +++ b/packages/sdk/react-native/src/provider/index.ts @@ -0,0 +1,4 @@ +import LDProvider from './LDProvider'; + +// eslint-disable-next-line import/prefer-default-export +export { LDProvider }; diff --git a/packages/sdk/react-native/src/provider/reactContext.ts b/packages/sdk/react-native/src/provider/reactContext.ts new file mode 100644 index 000000000..57f7a0e4b --- /dev/null +++ b/packages/sdk/react-native/src/provider/reactContext.ts @@ -0,0 +1,21 @@ +import { createContext } from 'react'; + +import { LDClient, LDContext } from '@launchdarkly/js-client-sdk-common'; + +export type ReactContext = { + client: LDClient; + context?: LDContext; + dataSource: { + status?: 'connecting' | 'ready' | 'error'; + error?: Error; + }; +}; + +export const context = createContext({ + client: {} as any, + dataSource: {}, +}); + +const { Provider, Consumer } = context; + +export { Provider, Consumer }; diff --git a/packages/sdk/react-native/src/provider/setupListeners.ts b/packages/sdk/react-native/src/provider/setupListeners.ts new file mode 100644 index 000000000..28d5b5eea --- /dev/null +++ b/packages/sdk/react-native/src/provider/setupListeners.ts @@ -0,0 +1,38 @@ +import type { Dispatch, SetStateAction } from 'react'; + +import { LDContext } from '@launchdarkly/js-client-sdk-common'; + +import ReactNativeLDClient from '../ReactNativeLDClient'; +import { ReactContext } from './reactContext'; + +const setupListeners = ( + client: ReactNativeLDClient, + setState: Dispatch>, +) => { + const { logger } = client; + + client.on('connecting', (c: LDContext) => { + logger.debug(`=========== connecting: ${JSON.stringify(c)}`); + setState({ client, context: c, dataSource: { status: 'connecting' } }); + }); + + client.on('ready', (c: LDContext) => { + logger.debug(`=========== ready: ${JSON.stringify(c)}`); + setState({ client, context: c, dataSource: { status: 'ready' } }); + }); + + client.on('error', (c: LDContext, e: any) => { + logger.debug(`=========== identify:error: ${JSON.stringify(c)}, ${e}`); + setState({ client, context: c, dataSource: { status: 'error', error: e } }); + }); + + // client.on('variation:error', (c: LDContext, e: any) => { + // logger.debug(`=========== variation:error: ${JSON.stringify(c)}, ${e}`); + // }); + // + // client.on('variation:success', (c: LDContext) => { + // logger.debug(`=========== variation:success: ${JSON.stringify(c)}`); + // }); +}; + +export default setupListeners; diff --git a/packages/shared/common/src/Context.ts b/packages/shared/common/src/Context.ts index d371aaaf1..4dcecaf3e 100644 --- a/packages/shared/common/src/Context.ts +++ b/packages/shared/common/src/Context.ts @@ -348,7 +348,7 @@ export default class Context { * @returns a {@link Context}, if the context was not valid, then the returned contexts `valid` * property will be false. */ - public static fromLDContext(context: LDContext): Context { + public static fromLDContext(context?: LDContext): Context { if (!context) { return Context.contextForError('unknown', 'No context specified. Returning default value'); } diff --git a/packages/shared/common/src/internal/stream/StreamingProcessor.ts b/packages/shared/common/src/internal/stream/StreamingProcessor.ts index db67c4647..cda184ed2 100644 --- a/packages/shared/common/src/internal/stream/StreamingProcessor.ts +++ b/packages/shared/common/src/internal/stream/StreamingProcessor.ts @@ -25,11 +25,12 @@ const reportJsonError = ( }; class StreamingProcessor implements LDStreamProcessor { + eventSource?: EventSource; + private readonly headers: { [key: string]: string | string[] }; private readonly streamUri: string; private readonly logger?: LDLogger; - private eventSource?: EventSource; private requests: Requests; private connectionAttemptStartTime?: number; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index c46185058..e15871da8 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { + ClientContext, clone, Context, internal, @@ -13,6 +14,8 @@ import { LDFlagValue, LDLogger, Platform, + ProcessStreamResponse, + EventName as StreamEventName, subsystem, TypeValidators, } from '@launchdarkly/js-sdk-common'; @@ -21,7 +24,8 @@ import { LDClient, type LDOptions } from './api'; import LDEmitter, { EventName } from './api/LDEmitter'; import Configuration from './configuration'; import createDiagnosticsManager from './diagnostics/createDiagnosticsManager'; -import fetchFlags, { Flags } from './evaluation/fetchFlags'; +import { Flags } from './evaluation/fetchFlags'; +import { base64UrlEncode } from './evaluation/fetchUtils'; import createEventProcessor from './events/createEventProcessor'; import EventFactory from './events/EventFactory'; @@ -30,21 +34,23 @@ const { createErrorEvaluationDetail, createSuccessEvaluationDetail, ClientMessag export default class LDClientImpl implements LDClient { config: Configuration; + context?: LDContext; diagnosticsManager?: internal.DiagnosticsManager; eventProcessor: subsystem.LDEventProcessor; + streamer?: internal.StreamingProcessor; + logger: LDLogger; private eventFactoryDefault = new EventFactory(false); private eventFactoryWithReasons = new EventFactory(true); private emitter: LDEmitter; private flags: Flags = {}; - private logger: LDLogger; + private readonly clientContext: ClientContext; /** * Creates the client object synchronously. No async, no network calls. */ constructor( public readonly sdkKey: string, - public context: LDContext, public readonly platform: Platform, options: LDOptions, ) { @@ -57,6 +63,7 @@ export default class LDClientImpl implements LDClient { } this.config = new Configuration(options); + this.clientContext = new ClientContext(sdkKey, this.config, platform); this.logger = this.config.logger; this.diagnosticsManager = createDiagnosticsManager(sdkKey, this.config, platform); this.eventProcessor = createEventProcessor( @@ -66,18 +73,6 @@ export default class LDClientImpl implements LDClient { this.diagnosticsManager, ); this.emitter = new LDEmitter(); - - // TODO: init streamer - } - - async start() { - try { - await this.identify(this.context); - this.emitter.emit('ready'); - } catch (error: any) { - this.emitter.emit('failed', error); - throw error; - } } allFlags(): LDFlagSet { @@ -106,24 +101,75 @@ export default class LDClientImpl implements LDClient { return clone(this.context); } + private createStreamListeners(): Map { + const listeners = new Map(); + + listeners.set('put', { + deserializeData: JSON.parse, + processJson: ({ data }) => { + this.logger.debug('Initializing all data'); + this.flags = {}; + Object.keys(data).forEach((key) => { + this.flags[key] = data[key]; + }); + }, + }); + + listeners.set('patch', { + deserializeData: JSON.parse, + processJson: ({ data }) => { + this.logger.debug(`Updating ${data.key}`); + this.flags[data.key] = data; + }, + }); + + listeners.set('patch', { + deserializeData: JSON.parse, + processJson: ({ data }) => { + this.logger.debug(`Deleting ${data.key}`); + delete this.flags[data.key]; + }, + }); + + return listeners; + } + + makeStreamUri(context: LDContext) { + return `${this.config.serviceEndpoints.streaming}/meval/${base64UrlEncode( + JSON.stringify(context), + this.platform.encoding!, + )}`; + } + // TODO: implement secure mode - async identify(context: LDContext, hash?: string): Promise { + async identify(context: LDContext, _hash?: string): Promise { const checkedContext = Context.fromLDContext(context); if (!checkedContext.valid) { const error = new Error('Context was unspecified or had no key'); this.logger.error(error); - this.emitter.emit('error', error); + this.emitter.emit('error', context, error); throw error; } - try { - this.flags = await fetchFlags(this.sdkKey, context, this.config, this.platform); - this.context = context; - } catch (error: any) { - this.logger.error(error); - this.emitter.emit('error', error); - throw error; - } + this.context = context; + + this.streamer = new internal.StreamingProcessor( + this.sdkKey, + this.clientContext, + this.makeStreamUri(context), + this.createStreamListeners(), + this.diagnosticsManager, + (e) => this.logger.error(e), + ); + + this.emitter.emit('connecting', context); + this.streamer.start(); + this.streamer.eventSource!.onopen = () => { + this.emitter.emit('ready', context); + }; + this.streamer.eventSource!.onerror = (err: any) => { + this.emitter.emit('error', context, err); + }; } off(eventName: EventName, listener?: Function): void { @@ -162,7 +208,7 @@ export default class LDClientImpl implements LDClient { if (!found) { const error = new LDClientError(`Unknown feature flag "${flagKey}"; returning default value`); - this.emitter.emit('error', error); + this.emitter.emit('variation:error', this.context, error); this.eventProcessor.sendEvent( this.eventFactoryDefault.unknownFlagEvent(flagKey, defaultValue ?? null, evalContext), ); @@ -184,6 +230,10 @@ export default class LDClientImpl implements LDClient { reason, ), ); + const error = new LDClientError( + `Wrong type "${type}" for feature flag "${flagKey}"; returning default value`, + ); + this.emitter.emit('variation:error', this.context, error); return createErrorEvaluationDetail(ErrorKinds.WrongType, defaultValue); } } @@ -193,6 +243,7 @@ export default class LDClientImpl implements LDClient { this.logger.debug('Result value is null in variation'); successDetail.value = defaultValue; } + this.emitter.emit('variation:success', this.context); this.eventProcessor.sendEvent( eventFactory.evalEventClient(flagKey, value, defaultValue, found, evalContext, reason), ); @@ -216,7 +267,6 @@ export default class LDClientImpl implements LDClient { return this.variationInternal(key, defaultValue, eventFactory, typeChecker); } - // TODO: add other typed variation functions boolVariation(key: string, defaultValue: boolean): boolean { return this.typedEval(key, defaultValue, this.eventFactoryDefault, (value) => [ TypeValidators.Boolean.is(value), @@ -224,6 +274,49 @@ export default class LDClientImpl implements LDClient { ]).value; } + jsonVariation(key: string, defaultValue: unknown): unknown { + return this.variation(key, defaultValue); + } + + numberVariation(key: string, defaultValue: number): number { + return this.typedEval(key, defaultValue, this.eventFactoryDefault, (value) => [ + TypeValidators.Number.is(value), + TypeValidators.Number.getType(), + ]).value; + } + + stringVariation(key: string, defaultValue: string): string { + return this.typedEval(key, defaultValue, this.eventFactoryDefault, (value) => [ + TypeValidators.String.is(value), + TypeValidators.String.getType(), + ]).value; + } + + boolVariationDetail(key: string, defaultValue: boolean): LDEvaluationDetailTyped { + return this.typedEval(key, defaultValue, this.eventFactoryWithReasons, (value) => [ + TypeValidators.Boolean.is(value), + TypeValidators.Boolean.getType(), + ]); + } + + numberVariationDetail(key: string, defaultValue: number): LDEvaluationDetailTyped { + return this.typedEval(key, defaultValue, this.eventFactoryWithReasons, (value) => [ + TypeValidators.Number.is(value), + TypeValidators.Number.getType(), + ]); + } + + stringVariationDetail(key: string, defaultValue: string): LDEvaluationDetailTyped { + return this.typedEval(key, defaultValue, this.eventFactoryWithReasons, (value) => [ + TypeValidators.String.is(value), + TypeValidators.String.getType(), + ]); + } + + jsonVariationDetail(key: string, defaultValue: unknown): LDEvaluationDetailTyped { + return this.variationDetail(key, defaultValue); + } + waitForInitialization(): Promise { // TODO: return Promise.resolve(undefined); diff --git a/packages/shared/sdk-client/src/api/LDClient.ts b/packages/shared/sdk-client/src/api/LDClient.ts index 5a9c3e4b7..3f6659e34 100644 --- a/packages/shared/sdk-client/src/api/LDClient.ts +++ b/packages/shared/sdk-client/src/api/LDClient.ts @@ -1,4 +1,11 @@ -import { LDContext, LDEvaluationDetail, LDFlagSet, LDFlagValue } from '@launchdarkly/js-sdk-common'; +import { + LDContext, + LDEvaluationDetail, + LDEvaluationDetailTyped, + LDFlagSet, + LDFlagValue, + LDLogger, +} from '@launchdarkly/js-sdk-common'; /** * The basic interface for the LaunchDarkly client. Platform-specific SDKs may add some methods of their own. @@ -9,70 +16,75 @@ import { LDContext, LDEvaluationDetail, LDFlagSet, LDFlagValue } from '@launchda */ export interface LDClient { /** - * Returns a Promise that tracks the client's initialization state. - * - * The returned Promise will be resolved once the client has either successfully initialized - * or failed to initialize (e.g. due to an invalid environment key or a server error). It will - * never be rejected. - * - * ``` - * // using a Promise then() handler - * client.waitUntilReady().then(() => { - * doSomethingWithClient(); - * }); - * - * // using async/await - * await client.waitUntilReady(); - * doSomethingWithClient(); - * ``` + * Returns a map of all available flags to the current context's values. * - * If you want to distinguish between these success and failure conditions, use - * {@link waitForInitialization} instead. + * @returns + * An object in which each key is a feature flag key and each value is the flag value. + * Note that there is no way to specify a default value for each flag as there is with + * {@link variation}, so any flag that cannot be evaluated will have a null value. + */ + allFlags(): LDFlagSet; + + /** + * Determines the boolean variation of a feature flag. * - * If you prefer to use event listeners ({@link on}) rather than Promises, you can listen on the - * client for a `"ready"` event, which will be fired in either case. + * If the flag variation does not have a boolean value, defaultValue is returned. * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. * @returns - * A Promise that will be resolved once the client is no longer trying to initialize. + * The boolean value. */ - waitUntilReady(): Promise; + boolVariation(key: string, defaultValue: boolean): boolean; /** - * Returns a Promise that tracks the client's initialization state. + * Determines the boolean variation of a feature flag for a context, along with information about + * how it was calculated. * - * The Promise will be resolved if the client successfully initializes, or rejected if client - * initialization has irrevocably failed (for instance, if it detects that the SDK key is invalid). + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. * - * ``` - * // using Promise then() and catch() handlers - * client.waitForInitialization().then(() => { - * doSomethingWithSuccessfullyInitializedClient(); - * }).catch(err => { - * doSomethingForFailedStartup(err); - * }); + * If the flag variation does not have a boolean value, defaultValue is returned. The reason will + * indicate an error of the type `WRONG_KIND` in this case. * - * // using async/await - * try { - * await client.waitForInitialization(); - * doSomethingWithSuccessfullyInitializedClient(); - * } catch (err) { - * doSomethingForFailedStartup(err); - * } - * ``` + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#react-native). * - * It is important that you handle the rejection case; otherwise it will become an unhandled Promise - * rejection, which is a serious error on some platforms. The Promise is not created unless you - * request it, so if you never call `waitForInitialization()` then you do not have to worry about - * unhandled rejections. + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * The result (as an {@link LDEvaluationDetailTyped}). + */ + boolVariationDetail(key: string, defaultValue: boolean): LDEvaluationDetailTyped; + + /** + * Shuts down the client and releases its resources, after delivering any pending analytics + * events. After the client is closed, all calls to {@link variation} will return default values, + * and it will not make any requests to LaunchDarkly. + */ + close(): void; + + /** + * Flushes all pending analytics events. * - * Note that you can also use event listeners ({@link on}) for the same purpose: the event `"initialized"` - * indicates success, and `"failed"` indicates failure. + * Normally, batches of events are delivered in the background at intervals determined by the + * `flushInterval` property of {@link LDOptions}. Calling `flush()` triggers an immediate delivery. * * @returns - * A Promise that will be resolved if the client initializes successfully, or rejected if it - * fails. + * A Promise which resolves once + * flushing is finished. You can inspect the result of the flush for errors. */ - waitForInitialization(): Promise; + flush(): Promise<{ error?: Error; result: boolean }>; + + /** + * Returns the client's current context. + * + * This is the context that was most recently passed to {@link identify}, or, if {@link identify} has never + * been called, the initial context specified when the client was created. + */ + getContext(): LDContext; /** * Identifies a context to LaunchDarkly. @@ -95,72 +107,93 @@ export interface LDClient { identify(context: LDContext, hash?: string): Promise; /** - * Returns the client's current context. + * Determines the json variation of a feature flag. * - * This is the context that was most recently passed to {@link identify}, or, if {@link identify} has never - * been called, the initial context specified when the client was created. + * This version may be favored in TypeScript versus `variation` because it returns + * an `unknown` type instead of `any`. `unknown` will require a cast before usage. + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * The json value. */ - getContext(): LDContext; + jsonVariation(key: string, defaultValue: unknown): unknown; /** - * Flushes all pending analytics events. + * Determines the variation of a feature flag for a context, along with information about how it + * was calculated. * - * Normally, batches of events are delivered in the background at intervals determined by the - * `flushInterval` property of {@link LDOptions}. Calling `flush()` triggers an immediate delivery. + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. * + * This version may be favored in TypeScript versus `variation` because it returns + * an `unknown` type instead of `any`. `unknown` will require a cast before usage. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#react-native). + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. * @returns - * A Promise which resolves once - * flushing is finished. You can inspect the result of the flush for errors. + * If you provided a callback, then nothing. Otherwise, a Promise which will be resolved with + * the result (as an{@link LDEvaluationDetailTyped}). */ - flush(): Promise<{ error?: Error; result: boolean }>; + jsonVariationDetail(key: string, defaultValue: unknown): LDEvaluationDetailTyped; /** - * Determines the variation of a feature flag for the current context. + * Returns the logger configured as part of LDOptions during construction. * - * In the client-side JavaScript SDKs, this is always a fast synchronous operation because all of - * the feature flag values for the current context have already been loaded into memory. + * For more, read {@link LDOptions.logger}. + */ + logger: LDLogger; + + /** + * Determines the numeric variation of a feature flag. * - * @param key - * The unique key of the feature flag. - * @param defaultValue - * The default value of the flag, to be used if the value is not available from LaunchDarkly. + * If the flag variation does not have a numeric value, defaultValue is returned. + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. * @returns - * The flag's value. + * The numeric value. */ - variation(key: string, defaultValue?: LDFlagValue): LDFlagValue; + numberVariation(key: string, defaultValue: number): number; /** - * Determines the variation of a feature flag for a context, along with information about how it was - * calculated. - * - * Note that this will only work if you have set `evaluationExplanations` to true in {@link LDOptions}. - * Otherwise, the `reason` property of the result will be null. + * Determines the numeric variation of a feature flag for a context, along with information about + * how it was calculated. * * The `reason` property of the result will also be included in analytics events, if you are * capturing detailed event data for this flag. * - * For more information, see the [SDK reference guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#javascript). + * If the flag variation does not have a numeric value, defaultValue is returned. The reason will + * indicate an error of the type `WRONG_KIND` in this case. * - * @param key - * The unique key of the feature flag. - * @param defaultValue - * The default value of the flag, to be used if the value is not available from LaunchDarkly. + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#react-native). * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. * @returns - * An {@link LDEvaluationDetail} object containing the value and explanation. + * The result (as an {@link LDEvaluationDetailTyped}). */ - variationDetail(key: string, defaultValue?: LDFlagValue): LDEvaluationDetail; + numberVariationDetail(key: string, defaultValue: number): LDEvaluationDetailTyped; /** - * Specifies whether or not to open a streaming connection to LaunchDarkly for live flag updates. - * - * If this is true, the client will always attempt to maintain a streaming connection; if false, - * it never will. If you leave the value undefined (the default), the client will open a streaming - * connection if you subscribe to `"change"` or `"change:flag-key"` events (see {@link LDClient.on}). + * Deregisters an event listener. See {@link on} for the available event types. * - * This can also be set as the `streaming` property of {@link LDOptions}. + * @param key + * The name of the event for which to stop listening. + * @param callback + * The function to deregister. + * @param context + * The `this` context for the callback, if one was specified for {@link on}. */ - setStreaming(value?: boolean): void; + off(key: string, callback: (...args: any[]) => void, context?: any): void; /** * Registers an event listener. @@ -203,16 +236,49 @@ export interface LDClient { on(key: string, callback: (...args: any[]) => void, context?: any): void; /** - * Deregisters an event listener. See {@link on} for the available event types. + * Specifies whether or not to open a streaming connection to LaunchDarkly for live flag updates. * - * @param key - * The name of the event for which to stop listening. - * @param callback - * The function to deregister. - * @param context - * The `this` context for the callback, if one was specified for {@link on}. + * If this is true, the client will always attempt to maintain a streaming connection; if false, + * it never will. If you leave the value undefined (the default), the client will open a streaming + * connection if you subscribe to `"change"` or `"change:flag-key"` events (see {@link LDClient.on}). + * + * This can also be set as the `streaming` property of {@link LDOptions}. */ - off(key: string, callback: (...args: any[]) => void, context?: any): void; + setStreaming(value?: boolean): void; + + /** + * Determines the string variation of a feature flag. + * + * If the flag variation does not have a string value, defaultValue is returned. + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * The string value. + */ + stringVariation(key: string, defaultValue: string): string; + + /** + * Determines the string variation of a feature flag for a context, along with information about + * how it was calculated. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * If the flag variation does not have a string value, defaultValue is returned. The reason will + * indicate an error of the type `WRONG_KIND` in this case. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#react-native). + * + * @param key The unique key of the feature flag. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * The result (as an {@link LDEvaluationDetailTyped}). + */ + stringVariationDetail(key: string, defaultValue: string): LDEvaluationDetailTyped; /** * Track page events to use in goals or A/B tests. @@ -234,19 +300,105 @@ export interface LDClient { track(key: string, data?: any, metricValue?: number): void; /** - * Returns a map of all available flags to the current context's values. + * Determines the variation of a feature flag for the current context. * + * In the client-side JavaScript SDKs, this is always a fast synchronous operation because all of + * the feature flag values for the current context have already been loaded into memory. + * + * @param key + * The unique key of the feature flag. + * @param defaultValue + * The default value of the flag, to be used if the value is not available from LaunchDarkly. * @returns - * An object in which each key is a feature flag key and each value is the flag value. - * Note that there is no way to specify a default value for each flag as there is with - * {@link variation}, so any flag that cannot be evaluated will have a null value. + * The flag's value. */ - allFlags(): LDFlagSet; + variation(key: string, defaultValue?: LDFlagValue): LDFlagValue; /** - * Shuts down the client and releases its resources, after delivering any pending analytics - * events. After the client is closed, all calls to {@link variation} will return default values, - * and it will not make any requests to LaunchDarkly. + * Determines the variation of a feature flag for a context, along with information about how it was + * calculated. + * + * Note that this will only work if you have set `evaluationExplanations` to true in {@link LDOptions}. + * Otherwise, the `reason` property of the result will be null. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * For more information, see the [SDK reference guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#javascript). + * + * @param key + * The unique key of the feature flag. + * @param defaultValue + * The default value of the flag, to be used if the value is not available from LaunchDarkly. + * + * @returns + * An {@link LDEvaluationDetail} object containing the value and explanation. */ - close(): void; + variationDetail(key: string, defaultValue?: LDFlagValue): LDEvaluationDetail; + + /** + * Returns a Promise that tracks the client's initialization state. + * + * The Promise will be resolved if the client successfully initializes, or rejected if client + * initialization has irrevocably failed (for instance, if it detects that the SDK key is invalid). + * + * ``` + * // using Promise then() and catch() handlers + * client.waitForInitialization().then(() => { + * doSomethingWithSuccessfullyInitializedClient(); + * }).catch(err => { + * doSomethingForFailedStartup(err); + * }); + * + * // using async/await + * try { + * await client.waitForInitialization(); + * doSomethingWithSuccessfullyInitializedClient(); + * } catch (err) { + * doSomethingForFailedStartup(err); + * } + * ``` + * + * It is important that you handle the rejection case; otherwise it will become an unhandled Promise + * rejection, which is a serious error on some platforms. The Promise is not created unless you + * request it, so if you never call `waitForInitialization()` then you do not have to worry about + * unhandled rejections. + * + * Note that you can also use event listeners ({@link on}) for the same purpose: the event `"initialized"` + * indicates success, and `"failed"` indicates failure. + * + * @returns + * A Promise that will be resolved if the client initializes successfully, or rejected if it + * fails. + */ + waitForInitialization(): Promise; + + /** + * Returns a Promise that tracks the client's initialization state. + * + * The returned Promise will be resolved once the client has either successfully initialized + * or failed to initialize (e.g. due to an invalid environment key or a server error). It will + * never be rejected. + * + * ``` + * // using a Promise then() handler + * client.waitUntilReady().then(() => { + * doSomethingWithClient(); + * }); + * + * // using async/await + * await client.waitUntilReady(); + * doSomethingWithClient(); + * ``` + * + * If you want to distinguish between these success and failure conditions, use + * {@link waitForInitialization} instead. + * + * If you prefer to use event listeners ({@link on}) rather than Promises, you can listen on the + * client for a `"ready"` event, which will be fired in either case. + * + * @returns + * A Promise that will be resolved once the client is no longer trying to initialize. + */ + waitUntilReady(): Promise; } diff --git a/packages/shared/sdk-client/src/api/LDEmitter.ts b/packages/shared/sdk-client/src/api/LDEmitter.ts index 53ad6be38..15cecf46e 100644 --- a/packages/shared/sdk-client/src/api/LDEmitter.ts +++ b/packages/shared/sdk-client/src/api/LDEmitter.ts @@ -1,4 +1,11 @@ -export type EventName = 'change' | 'ready' | 'failed' | 'error'; +export type EventName = + | 'change' + | 'ready' + | 'failed' + | 'error' + | 'connecting' + | 'variation:success' + | 'variation:error'; type CustomEventListeners = { original: Function; From da018558db859b921f152b2631b1426a58c56114 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 27 Nov 2023 23:16:11 -0800 Subject: [PATCH 02/24] chore: added npm commands to run natively. --- packages/sdk/react-native/example/.gitignore | 3 +++ packages/sdk/react-native/example/app.json | 10 +++++++--- packages/sdk/react-native/example/package.json | 14 +++++++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/sdk/react-native/example/.gitignore b/packages/sdk/react-native/example/.gitignore index 88aca5470..b252443a3 100644 --- a/packages/sdk/react-native/example/.gitignore +++ b/packages/sdk/react-native/example/.gitignore @@ -34,3 +34,6 @@ yarn-error.* # typescript *.tsbuildinfo + +ios +android diff --git a/packages/sdk/react-native/example/app.json b/packages/sdk/react-native/example/app.json index c728b74ef..b797a3a7e 100644 --- a/packages/sdk/react-native/example/app.json +++ b/packages/sdk/react-native/example/app.json @@ -11,15 +11,19 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" }, - "assetBundlePatterns": ["**/*"], + "assetBundlePatterns": [ + "**/*" + ], "ios": { - "supportsTablet": true + "supportsTablet": true, + "bundleIdentifier": "com.anonymous.reactnativeexample" }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" - } + }, + "package": "com.anonymous.reactnativeexample" }, "web": { "favicon": "./assets/favicon.png" diff --git a/packages/sdk/react-native/example/package.json b/packages/sdk/react-native/example/package.json index d6ca0e3e7..113904380 100644 --- a/packages/sdk/react-native/example/package.json +++ b/packages/sdk/react-native/example/package.json @@ -4,13 +4,21 @@ "main": "node_modules/expo/AppEntry.js", "scripts": { "start": "expo start", - "android": "expo start --android --clear", - "ios": "expo start --ios --clear", + "expo-clean": "expo prebuild --clean", + "android": "expo run:android", + "android-release": "expo run:android --variant release", + "android-go": "expo start --android --clear", + "android-log": "react-native log-android", + "ios": "expo run:ios", + "ios-release": "expo run:ios --configuration Release", + "ios-go": "expo start --ios --clear", + "ios-log": "react-native log-ios", "web": "expo start --web --clear", - "clean": "yarn cache clean && rm -rf node_modules" + "clean": "expo prebuild --clean && yarn cache clean && rm -rf node_modules && rm -rf .expo" }, "dependencies": { "expo": "~49.0.16", + "expo-splash-screen": "~0.20.5", "expo-status-bar": "~1.7.1", "react": "18.2.0", "react-native": "0.72.6" From fc41787f4cdc46900138a18a4040951bb5ed3d19 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Tue, 28 Nov 2023 00:07:13 -0800 Subject: [PATCH 03/24] chore: fixed wrong listener argument shape. fixed missing error argument in event source. TODO: fix malformed es uri. fix broken typedvariation eval. --- .../sdk/react-native/example/src/welcome.tsx | 13 +++++++++++-- .../src/react-native-sse/EventSource.ts | 8 +++++--- packages/shared/sdk-client/src/LDClientImpl.ts | 18 +++++++++--------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/sdk/react-native/example/src/welcome.tsx b/packages/sdk/react-native/example/src/welcome.tsx index de2adf4fd..5345954a3 100644 --- a/packages/sdk/react-native/example/src/welcome.tsx +++ b/packages/sdk/react-native/example/src/welcome.tsx @@ -1,10 +1,19 @@ import { StyleSheet, Text, View } from 'react-native'; -import { useBoolVariation, useLDDataSourceStatus } from '@launchdarkly/react-native-client-sdk'; +import { + useBoolVariation, + useLDClient, + useLDDataSourceStatus, +} from '@launchdarkly/react-native-client-sdk'; export default function Welcome() { const { error, status } = useLDDataSourceStatus(); - const flag = useBoolVariation('dev-test-flag', false); + const client = useLDClient(); + const flag = client.boolVariation('dev-test-flag', false); + + // TODO: debug why typed variation hooks don't work. + // const flag = useBoolVariation('dev-test-flag', false); + console.log(`============== status: ${status}`); return ( diff --git a/packages/sdk/react-native/src/react-native-sse/EventSource.ts b/packages/sdk/react-native/src/react-native-sse/EventSource.ts index 420c5fdad..1d6ff8682 100644 --- a/packages/sdk/react-native/src/react-native-sse/EventSource.ts +++ b/packages/sdk/react-native/src/react-native-sse/EventSource.ts @@ -56,7 +56,9 @@ export default class EventSource { ...options, }; - this.url = url; + // TODO: debug why uri is malformed from LDClientImpl + this.url = + 'https://clientstream.launchdarkly.com/meval/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlci1rZXktMSJ9'; this.method = opts.method!; this.timeout = opts.timeout!; this.timeoutBeforeConnection = opts.timeoutBeforeConnection!; @@ -287,7 +289,7 @@ export default class EventSource { this.onclose(); break; case 'error': - this.onerror(); + this.onerror(data); break; case 'retry': this.onretrying(); @@ -309,6 +311,6 @@ export default class EventSource { onopen() {} onclose() {} - onerror() {} + onerror(_err: any) {} onretrying() {} } diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index e15871da8..4aaf355f7 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -106,28 +106,28 @@ export default class LDClientImpl implements LDClient { listeners.set('put', { deserializeData: JSON.parse, - processJson: ({ data }) => { + processJson: (dataJson) => { this.logger.debug('Initializing all data'); this.flags = {}; - Object.keys(data).forEach((key) => { - this.flags[key] = data[key]; + Object.keys(dataJson).forEach((key) => { + this.flags[key] = dataJson[key]; }); }, }); listeners.set('patch', { deserializeData: JSON.parse, - processJson: ({ data }) => { - this.logger.debug(`Updating ${data.key}`); - this.flags[data.key] = data; + processJson: (dataJson) => { + this.logger.debug(`Updating ${dataJson.key}`); + this.flags[dataJson.key] = dataJson; }, }); listeners.set('patch', { deserializeData: JSON.parse, - processJson: ({ data }) => { - this.logger.debug(`Deleting ${data.key}`); - delete this.flags[data.key]; + processJson: (dataJson) => { + this.logger.debug(`Deleting ${dataJson.key}`); + delete this.flags[dataJson.key]; }, }); From 7eb9de56907ce7b577984c08e352721013a8e267 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot Date: Tue, 28 Nov 2023 11:00:37 -0800 Subject: [PATCH 04/24] Delete index.test.tsx --- packages/sdk/react-native/src/index.test.tsx | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/sdk/react-native/src/index.test.tsx diff --git a/packages/sdk/react-native/src/index.test.tsx b/packages/sdk/react-native/src/index.test.tsx deleted file mode 100644 index 0d78ac8b8..000000000 --- a/packages/sdk/react-native/src/index.test.tsx +++ /dev/null @@ -1 +0,0 @@ -test.todo('write a test'); From 78134085734f8fcb1657dff7d986ad48a6ff0b1f Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot Date: Tue, 28 Nov 2023 11:01:59 -0800 Subject: [PATCH 05/24] Update index.ts --- packages/sdk/react-native/src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/sdk/react-native/src/index.ts b/packages/sdk/react-native/src/index.ts index 12f3a3769..78b49da15 100644 --- a/packages/sdk/react-native/src/index.ts +++ b/packages/sdk/react-native/src/index.ts @@ -1,8 +1,6 @@ /** * This is the API reference for the React Native LaunchDarkly SDK. * - * TODO: - * * For more information, see the SDK reference guide. * * @packageDocumentation From c0e4577948326c838a5014a3b3b26b27e9efced0 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot Date: Tue, 28 Nov 2023 11:54:45 -0800 Subject: [PATCH 06/24] fix: erroneous stream uri path --- .../sdk/react-native/src/react-native-sse/EventSource.ts | 4 +--- packages/shared/sdk-client/src/LDClientImpl.ts | 9 +++------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/sdk/react-native/src/react-native-sse/EventSource.ts b/packages/sdk/react-native/src/react-native-sse/EventSource.ts index 1d6ff8682..58a4c4409 100644 --- a/packages/sdk/react-native/src/react-native-sse/EventSource.ts +++ b/packages/sdk/react-native/src/react-native-sse/EventSource.ts @@ -56,9 +56,7 @@ export default class EventSource { ...options, }; - // TODO: debug why uri is malformed from LDClientImpl - this.url = - 'https://clientstream.launchdarkly.com/meval/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlci1rZXktMSJ9'; + this.url = url; this.method = opts.method!; this.timeout = opts.timeout!; this.timeoutBeforeConnection = opts.timeoutBeforeConnection!; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 4aaf355f7..2e33f8511 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -134,11 +134,8 @@ export default class LDClientImpl implements LDClient { return listeners; } - makeStreamUri(context: LDContext) { - return `${this.config.serviceEndpoints.streaming}/meval/${base64UrlEncode( - JSON.stringify(context), - this.platform.encoding!, - )}`; + private createStreamUriPath(context: LDContext) { + return `/meval/${base64UrlEncode(JSON.stringify(context), this.platform.encoding!)}`; } // TODO: implement secure mode @@ -156,7 +153,7 @@ export default class LDClientImpl implements LDClient { this.streamer = new internal.StreamingProcessor( this.sdkKey, this.clientContext, - this.makeStreamUri(context), + this.createStreamUriPath(context), this.createStreamListeners(), this.diagnosticsManager, (e) => this.logger.error(e), From 06f08a953d99e3f8cf2e7c1661166913dd2c9bfc Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Tue, 28 Nov 2023 14:45:46 -0800 Subject: [PATCH 07/24] fix: emit ready when put is finished. --- packages/sdk/react-native/example/src/welcome.tsx | 12 ++---------- packages/shared/sdk-client/src/LDClientImpl.ts | 4 +--- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/sdk/react-native/example/src/welcome.tsx b/packages/sdk/react-native/example/src/welcome.tsx index 5345954a3..c32025030 100644 --- a/packages/sdk/react-native/example/src/welcome.tsx +++ b/packages/sdk/react-native/example/src/welcome.tsx @@ -1,18 +1,10 @@ import { StyleSheet, Text, View } from 'react-native'; -import { - useBoolVariation, - useLDClient, - useLDDataSourceStatus, -} from '@launchdarkly/react-native-client-sdk'; +import { useBoolVariation, useLDDataSourceStatus } from '@launchdarkly/react-native-client-sdk'; export default function Welcome() { const { error, status } = useLDDataSourceStatus(); - const client = useLDClient(); - const flag = client.boolVariation('dev-test-flag', false); - - // TODO: debug why typed variation hooks don't work. - // const flag = useBoolVariation('dev-test-flag', false); + const flag = useBoolVariation('dev-test-flag', false); console.log(`============== status: ${status}`); return ( diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 2e33f8511..34e565bc0 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -112,6 +112,7 @@ export default class LDClientImpl implements LDClient { Object.keys(dataJson).forEach((key) => { this.flags[key] = dataJson[key]; }); + this.emitter.emit('ready', this.context); }, }); @@ -161,9 +162,6 @@ export default class LDClientImpl implements LDClient { this.emitter.emit('connecting', context); this.streamer.start(); - this.streamer.eventSource!.onopen = () => { - this.emitter.emit('ready', context); - }; this.streamer.eventSource!.onerror = (err: any) => { this.emitter.emit('error', context, err); }; From afd468058d99f493dcbf0608029a6e66fa637e8a Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Tue, 28 Nov 2023 15:28:57 -0800 Subject: [PATCH 08/24] fix: override createStreamUriPath so client sdks can specify custom paths. moved base64 function to common. --- .../react-native/src/ReactNativeLDClient.ts | 11 +++++++++- packages/shared/common/src/utils/http.ts | 14 ++++++++++++- packages/shared/common/src/utils/index.ts | 3 ++- .../shared/sdk-client/src/LDClientImpl.ts | 20 +++++++++++++++--- .../sdk-client/src/evaluation/fetchUtils.ts | 21 +++++++------------ 5 files changed, 50 insertions(+), 19 deletions(-) diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts index 4886731c6..330f02fb4 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -1,4 +1,9 @@ -import { LDClientImpl, LDOptions } from '@launchdarkly/js-client-sdk-common'; +import { + base64UrlEncode, + LDClientImpl, + type LDContext, + type LDOptions, +} from '@launchdarkly/js-client-sdk-common'; import platform from './platform'; @@ -6,4 +11,8 @@ export default class ReactNativeLDClient extends LDClientImpl { constructor(sdkKey: string, options: LDOptions = {}) { super(sdkKey, platform, options); } + + override createStreamUriPath(context: LDContext) { + return `/meval/${base64UrlEncode(JSON.stringify(context), platform.encoding!)}`; + } } diff --git a/packages/shared/common/src/utils/http.ts b/packages/shared/common/src/utils/http.ts index 25ffd29da..b4e21d029 100644 --- a/packages/shared/common/src/utils/http.ts +++ b/packages/shared/common/src/utils/http.ts @@ -1,4 +1,4 @@ -import { HttpErrorResponse, Info } from '../api'; +import { Encoding, HttpErrorResponse, Info } from '../api'; import { isHttpRecoverable } from '../errors'; import { ApplicationTags } from '../options'; @@ -58,3 +58,15 @@ export function httpErrorMessage( export function shouldRetry({ status }: HttpErrorResponse) { return status ? isHttpRecoverable(status) : true; } + +/** + * In react-native use base64-js to polyfill btoa. This is safe + * because the react-native repo uses it too. Set the global.btoa to the encode + * function of base64-js. + * https://github.com/beatgammit/base64-js + * https://github.com/axios/axios/issues/2235#issuecomment-512204616 + * + * Ripped from https://thewoods.blog/base64url/ + */ +export const base64UrlEncode = (s: string, encoding: Encoding): string => + encoding.btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); diff --git a/packages/shared/common/src/utils/index.ts b/packages/shared/common/src/utils/index.ts index 84381a70f..693e3afb7 100644 --- a/packages/shared/common/src/utils/index.ts +++ b/packages/shared/common/src/utils/index.ts @@ -1,11 +1,12 @@ import clone from './clone'; import { secondsToMillis } from './date'; -import { defaultHeaders, httpErrorMessage, LDHeaders, shouldRetry } from './http'; +import { base64UrlEncode, defaultHeaders, httpErrorMessage, LDHeaders, shouldRetry } from './http'; import noop from './noop'; import sleep from './sleep'; import { VoidFunction } from './VoidFunction'; export { + base64UrlEncode, clone, defaultHeaders, httpErrorMessage, diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 34e565bc0..c4de5ddc0 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { + base64UrlEncode, ClientContext, clone, Context, @@ -25,7 +26,6 @@ import LDEmitter, { EventName } from './api/LDEmitter'; import Configuration from './configuration'; import createDiagnosticsManager from './diagnostics/createDiagnosticsManager'; import { Flags } from './evaluation/fetchFlags'; -import { base64UrlEncode } from './evaluation/fetchUtils'; import createEventProcessor from './events/createEventProcessor'; import EventFactory from './events/EventFactory'; @@ -135,8 +135,22 @@ export default class LDClientImpl implements LDClient { return listeners; } - private createStreamUriPath(context: LDContext) { - return `/meval/${base64UrlEncode(JSON.stringify(context), this.platform.encoding!)}`; + /** + * Generates the url subpath for streamer. + * + * For mobile key: /meval/${base64-encoded-context} + * For clientSideId: /eval/${envId}/${base64-encoded-context} + * + * @param context The LD context object to be base64 encoded and appended to + * the path + * + * @protected This function must be overridden in subclasses for streamer + * to work + */ + protected createStreamUriPath(context: LDContext) { + throw new Error( + 'createStreamUriPath not implemented. client sdks must implement createStreamUriPath for streamer to work', + ); } // TODO: implement secure mode diff --git a/packages/shared/sdk-client/src/evaluation/fetchUtils.ts b/packages/shared/sdk-client/src/evaluation/fetchUtils.ts index ded227f76..ea415ee64 100644 --- a/packages/shared/sdk-client/src/evaluation/fetchUtils.ts +++ b/packages/shared/sdk-client/src/evaluation/fetchUtils.ts @@ -1,19 +1,14 @@ -import { defaultHeaders, Encoding, Info, LDContext, Options } from '@launchdarkly/js-sdk-common'; +import { + base64UrlEncode, + defaultHeaders, + Encoding, + Info, + LDContext, + Options, +} from '@launchdarkly/js-sdk-common'; import Configuration from '../configuration'; -/** - * In react-native use base64-js to polyfill btoa. This is safe - * because the react-native repo uses it too. Set the global.btoa to the encode - * function of base64-js. - * https://github.com/beatgammit/base64-js - * https://github.com/axios/axios/issues/2235#issuecomment-512204616 - * - * Ripped from https://thewoods.blog/base64url/ - */ -export const base64UrlEncode = (s: string, encoding: Encoding): string => - encoding.btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); - export const createFetchPath = ( sdkKey: string, context: LDContext, From 9ff493052689d54049cd01b068ddb868c31bdad2 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Tue, 28 Nov 2023 15:29:31 -0800 Subject: [PATCH 09/24] fix: createStreamUriPath returns string now. --- packages/shared/sdk-client/src/LDClientImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index c4de5ddc0..52927f683 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -147,7 +147,7 @@ export default class LDClientImpl implements LDClient { * @protected This function must be overridden in subclasses for streamer * to work */ - protected createStreamUriPath(context: LDContext) { + protected createStreamUriPath(context: LDContext): string { throw new Error( 'createStreamUriPath not implemented. client sdks must implement createStreamUriPath for streamer to work', ); From ced9caa2c5e7a77a4600fcac2589b0765fad575e Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Tue, 28 Nov 2023 15:57:14 -0800 Subject: [PATCH 10/24] fix: fix broken tests related to fromLDContext arg being undefined. --- packages/shared/common/src/Context.ts | 2 +- packages/shared/sdk-client/src/LDClientImpl.test.ts | 8 ++++---- packages/shared/sdk-client/src/LDClientImpl.ts | 10 +++++++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/shared/common/src/Context.ts b/packages/shared/common/src/Context.ts index 4dcecaf3e..d371aaaf1 100644 --- a/packages/shared/common/src/Context.ts +++ b/packages/shared/common/src/Context.ts @@ -348,7 +348,7 @@ export default class Context { * @returns a {@link Context}, if the context was not valid, then the returned contexts `valid` * property will be false. */ - public static fromLDContext(context?: LDContext): Context { + public static fromLDContext(context: LDContext): Context { if (!context) { return Context.contextForError('unknown', 'No context specified. Returning default value'); } diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index b2e14d315..ab98b3ffd 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -20,7 +20,7 @@ describe('sdk-client object', () => { beforeEach(() => { mockFetchFlags.mockResolvedValue(mockResponseJson); - ldc = new LDClientImpl(testSdkKey, context, basicPlatform, { logger }); + ldc = new LDClientImpl(testSdkKey, basicPlatform, { logger }); [mockEmitter] = (LDEmitter as jest.Mock).mock.instances; }); @@ -29,7 +29,7 @@ describe('sdk-client object', () => { }); test('instantiate with blank options', () => { - ldc = new LDClientImpl(testSdkKey, context, basicPlatform, {}); + ldc = new LDClientImpl(testSdkKey, basicPlatform, {}); expect(ldc.config).toMatchObject({ allAttributesPrivate: false, baseUri: 'https://sdk.launchdarkly.com', @@ -62,7 +62,7 @@ describe('sdk-client object', () => { }); test('all flags', async () => { - await ldc.start(); + await ldc.identify(context); const all = ldc.allFlags(); expect(all).toEqual({ @@ -78,7 +78,7 @@ describe('sdk-client object', () => { }); test('variation', async () => { - await ldc.start(); + await ldc.identify(context); const devTestFlag = ldc.variation('dev-test-flag'); expect(devTestFlag).toBe(true); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 52927f683..42acf2f7f 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -194,8 +194,11 @@ export default class LDClientImpl implements LDClient { } track(key: string, data?: any, metricValue?: number): void { + if (!this.context) { + this.logger?.warn(ClientMessages.missingContextKeyNoEvent); + return; + } const checkedContext = Context.fromLDContext(this.context); - if (!checkedContext.valid) { this.logger?.warn(ClientMessages.missingContextKeyNoEvent); return; @@ -212,6 +215,11 @@ export default class LDClientImpl implements LDClient { eventFactory: EventFactory, typeChecker?: (value: any) => [boolean, string], ): LDFlagValue { + if (!this.context) { + this.logger?.warn(ClientMessages.missingContextKeyNoEvent); + return createErrorEvaluationDetail(ErrorKinds.UserNotSpecified, defaultValue); + } + const evalContext = Context.fromLDContext(this.context); const found = this.flags[flagKey]; From 51aef407486ec1ee839aa8f44f813623ccaa9500 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot Date: Wed, 29 Nov 2023 13:03:15 -0800 Subject: [PATCH 11/24] chore: allow mock streamprocessor to return custom json. starting to fix ldclient tests. --- .../shared/mocks/src/streamingProcessor.ts | 6 ++-- .../sdk-client/src/LDClientImpl.test.ts | 29 +++++++++++++++++-- .../shared/sdk-client/src/LDClientImpl.ts | 6 ++-- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/shared/mocks/src/streamingProcessor.ts b/packages/shared/mocks/src/streamingProcessor.ts index e58cbe583..9cee41e4d 100644 --- a/packages/shared/mocks/src/streamingProcessor.ts +++ b/packages/shared/mocks/src/streamingProcessor.ts @@ -8,7 +8,7 @@ import type { export const MockStreamingProcessor = jest.fn(); -export const setupMockStreamingProcessor = (shouldError: boolean = false) => { +export const setupMockStreamingProcessor = (shouldError: boolean = false, putJson?: any) => { MockStreamingProcessor.mockImplementation( ( sdkKey: string, @@ -32,11 +32,13 @@ export const setupMockStreamingProcessor = (shouldError: boolean = false) => { } else { // execute put which will resolve the init promise process.nextTick( - () => listeners.get('put')?.processJson({ data: { flags: {}, segments: {} } }), + () => + listeners.get('put')?.processJson(putJson ?? { data: { flags: {}, segments: {} } }), ); } }), close: jest.fn(), + eventSource: {}, }), ); }; diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index ab98b3ffd..4aa768769 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -1,5 +1,10 @@ import { LDContext } from '@launchdarkly/js-sdk-common'; -import { basicPlatform, logger } from '@launchdarkly/private-js-mocks'; +import { + basicPlatform, + logger, + MockStreamingProcessor, + setupMockStreamingProcessor, +} from '@launchdarkly/private-js-mocks'; import LDEmitter from './api/LDEmitter'; import fetchFlags from './evaluation/fetchFlags'; @@ -8,7 +13,18 @@ import LDClientImpl from './LDClientImpl'; jest.mock('./api/LDEmitter'); jest.mock('./evaluation/fetchFlags'); - +jest.mock('@launchdarkly/js-sdk-common', () => { + const actual = jest.requireActual('@launchdarkly/js-sdk-common'); + return { + ...actual, + ...{ + internal: { + ...actual.internal, + StreamingProcessor: MockStreamingProcessor, + }, + }, + }; +}); describe('sdk-client object', () => { const testSdkKey = 'test-sdk-key'; const context: LDContext = { kind: 'org', key: 'Testy Pizza' }; @@ -18,9 +34,15 @@ describe('sdk-client object', () => { let mockEmitter: LDEmitter; beforeEach(() => { + jest.useFakeTimers(); + setupMockStreamingProcessor(false, mockResponseJson); + mockFetchFlags.mockResolvedValue(mockResponseJson); ldc = new LDClientImpl(testSdkKey, basicPlatform, { logger }); + jest + .spyOn(LDClientImpl.prototype as any, 'createStreamUriPath') + .mockReturnValue('/stream/path'); [mockEmitter] = (LDEmitter as jest.Mock).mock.instances; }); @@ -61,8 +83,9 @@ describe('sdk-client object', () => { }); }); - test('all flags', async () => { + test.only('all flags', async () => { await ldc.identify(context); + jest.runAllTicks(); const all = ldc.allFlags(); expect(all).toEqual({ diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 42acf2f7f..53761df87 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -136,16 +136,16 @@ export default class LDClientImpl implements LDClient { } /** - * Generates the url subpath for streamer. + * Generates the url path for streamer. * * For mobile key: /meval/${base64-encoded-context} * For clientSideId: /eval/${envId}/${base64-encoded-context} * * @param context The LD context object to be base64 encoded and appended to - * the path + * the path. * * @protected This function must be overridden in subclasses for streamer - * to work + * to work. */ protected createStreamUriPath(context: LDContext): string { throw new Error( From f8f6c237e6e842d8e24d80e77992ae49adf203b8 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot Date: Wed, 29 Nov 2023 15:35:08 -0800 Subject: [PATCH 12/24] chore: return a promise from identify and fix unit tests. --- .../sdk-client/src/LDClientImpl.test.ts | 30 ++++++------------- .../shared/sdk-client/src/LDClientImpl.ts | 28 +++++++++++++---- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index 4aa768769..b7deb8778 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -6,12 +6,9 @@ import { setupMockStreamingProcessor, } from '@launchdarkly/private-js-mocks'; -import LDEmitter from './api/LDEmitter'; -import fetchFlags from './evaluation/fetchFlags'; import * as mockResponseJson from './evaluation/mockResponse.json'; import LDClientImpl from './LDClientImpl'; -jest.mock('./api/LDEmitter'); jest.mock('./evaluation/fetchFlags'); jest.mock('@launchdarkly/js-sdk-common', () => { const actual = jest.requireActual('@launchdarkly/js-sdk-common'); @@ -28,22 +25,15 @@ jest.mock('@launchdarkly/js-sdk-common', () => { describe('sdk-client object', () => { const testSdkKey = 'test-sdk-key'; const context: LDContext = { kind: 'org', key: 'Testy Pizza' }; - const mockFetchFlags = fetchFlags as jest.Mock; - let ldc: LDClientImpl; - let mockEmitter: LDEmitter; beforeEach(() => { - jest.useFakeTimers(); setupMockStreamingProcessor(false, mockResponseJson); - mockFetchFlags.mockResolvedValue(mockResponseJson); - ldc = new LDClientImpl(testSdkKey, basicPlatform, { logger }); jest .spyOn(LDClientImpl.prototype as any, 'createStreamUriPath') .mockReturnValue('/stream/path'); - [mockEmitter] = (LDEmitter as jest.Mock).mock.instances; }); afterEach(() => { @@ -83,9 +73,8 @@ describe('sdk-client object', () => { }); }); - test.only('all flags', async () => { + test('all flags', async () => { await ldc.identify(context); - jest.runAllTicks(); const all = ldc.allFlags(); expect(all).toEqual({ @@ -109,7 +98,6 @@ describe('sdk-client object', () => { test('identify success', async () => { mockResponseJson['dev-test-flag'].value = false; - mockFetchFlags.mockResolvedValue(mockResponseJson); const carContext: LDContext = { kind: 'car', key: 'mazda-cx7' }; await ldc.identify(carContext); @@ -128,18 +116,18 @@ describe('sdk-client object', () => { await expect(ldc.identify(carContext)).rejects.toThrowError(/no key/); expect(logger.error).toBeCalledTimes(1); - expect(mockEmitter.emit).toHaveBeenNthCalledWith(1, 'error', expect.any(Error)); - expect(ldc.getContext()).toEqual(context); + expect(ldc.getContext()).toBeUndefined(); }); - test('identify error fetch error', async () => { - // @ts-ignore - mockFetchFlags.mockRejectedValue(new Error('unknown test fetch error')); + test('identify error stream error', async () => { + setupMockStreamingProcessor(true); const carContext: LDContext = { kind: 'car', key: 'mazda-3' }; - await expect(ldc.identify(carContext)).rejects.toThrowError(/fetch error/); + await expect(ldc.identify(carContext)).rejects.toMatchObject({ + code: 401, + message: 'test-error', + }); expect(logger.error).toBeCalledTimes(1); - expect(mockEmitter.emit).toHaveBeenNthCalledWith(1, 'error', expect.any(Error)); - expect(ldc.getContext()).toEqual(context); + expect(ldc.getContext()).toEqual(carContext); }); }); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 53761df87..d4ca1454e 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -98,7 +98,7 @@ export default class LDClientImpl implements LDClient { } getContext(): LDContext { - return clone(this.context); + return this.context ? clone(this.context) : undefined; } private createStreamListeners(): Map { @@ -165,20 +165,36 @@ export default class LDClientImpl implements LDClient { this.context = context; + const p = new Promise((resolve, reject) => { + this.emitter.on('ready', (c: LDContext) => { + if (c === context) { + this.logger.debug(`ready: ${JSON.stringify(c)}`); + resolve(); + } + }); + + this.emitter.on('error', (c: LDContext, err: any) => { + this.logger.debug(`error: ${err}, context: ${JSON.stringify(c)}`); + if (c === context) { + reject(err); + } + }); + }); + this.streamer = new internal.StreamingProcessor( this.sdkKey, this.clientContext, this.createStreamUriPath(context), this.createStreamListeners(), this.diagnosticsManager, - (e) => this.logger.error(e), + (e) => { + this.logger.error(e); + this.emitter.emit('error', context, e); + }, ); - this.emitter.emit('connecting', context); this.streamer.start(); - this.streamer.eventSource!.onerror = (err: any) => { - this.emitter.emit('error', context, err); - }; + return p; } off(eventName: EventName, listener?: Function): void { From dea6e4539a295306d63379768e7e8d9cb0102f63 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot Date: Wed, 29 Nov 2023 15:46:15 -0800 Subject: [PATCH 13/24] chore: added newline. --- packages/shared/sdk-client/src/LDClientImpl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index d4ca1454e..ede2f9ddb 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -194,6 +194,7 @@ export default class LDClientImpl implements LDClient { ); this.emitter.emit('connecting', context); this.streamer.start(); + return p; } From efc36ee1de51f2bcb7acc3ec4b7511557c5e4b1d Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot Date: Wed, 29 Nov 2023 16:02:26 -0800 Subject: [PATCH 14/24] chore: removed unused listeners. revert eventSource to private. removed confusing failed EventName. --- .../sdk/react-native/src/provider/setupListeners.ts | 13 ------------- .../src/internal/stream/StreamingProcessor.ts | 3 +-- packages/shared/sdk-client/src/api/LDEmitter.ts | 5 ++--- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/packages/sdk/react-native/src/provider/setupListeners.ts b/packages/sdk/react-native/src/provider/setupListeners.ts index 28d5b5eea..bde53b1c7 100644 --- a/packages/sdk/react-native/src/provider/setupListeners.ts +++ b/packages/sdk/react-native/src/provider/setupListeners.ts @@ -9,30 +9,17 @@ const setupListeners = ( client: ReactNativeLDClient, setState: Dispatch>, ) => { - const { logger } = client; - client.on('connecting', (c: LDContext) => { - logger.debug(`=========== connecting: ${JSON.stringify(c)}`); setState({ client, context: c, dataSource: { status: 'connecting' } }); }); client.on('ready', (c: LDContext) => { - logger.debug(`=========== ready: ${JSON.stringify(c)}`); setState({ client, context: c, dataSource: { status: 'ready' } }); }); client.on('error', (c: LDContext, e: any) => { - logger.debug(`=========== identify:error: ${JSON.stringify(c)}, ${e}`); setState({ client, context: c, dataSource: { status: 'error', error: e } }); }); - - // client.on('variation:error', (c: LDContext, e: any) => { - // logger.debug(`=========== variation:error: ${JSON.stringify(c)}, ${e}`); - // }); - // - // client.on('variation:success', (c: LDContext) => { - // logger.debug(`=========== variation:success: ${JSON.stringify(c)}`); - // }); }; export default setupListeners; diff --git a/packages/shared/common/src/internal/stream/StreamingProcessor.ts b/packages/shared/common/src/internal/stream/StreamingProcessor.ts index cda184ed2..db67c4647 100644 --- a/packages/shared/common/src/internal/stream/StreamingProcessor.ts +++ b/packages/shared/common/src/internal/stream/StreamingProcessor.ts @@ -25,12 +25,11 @@ const reportJsonError = ( }; class StreamingProcessor implements LDStreamProcessor { - eventSource?: EventSource; - private readonly headers: { [key: string]: string | string[] }; private readonly streamUri: string; private readonly logger?: LDLogger; + private eventSource?: EventSource; private requests: Requests; private connectionAttemptStartTime?: number; diff --git a/packages/shared/sdk-client/src/api/LDEmitter.ts b/packages/shared/sdk-client/src/api/LDEmitter.ts index 15cecf46e..cfa28ae10 100644 --- a/packages/shared/sdk-client/src/api/LDEmitter.ts +++ b/packages/shared/sdk-client/src/api/LDEmitter.ts @@ -1,9 +1,8 @@ export type EventName = - | 'change' + | 'connecting' | 'ready' - | 'failed' | 'error' - | 'connecting' + | 'change' | 'variation:success' | 'variation:error'; From 900f65485741aecebfd19f6ca6dae4266b54d342 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 29 Nov 2023 19:48:40 -0800 Subject: [PATCH 15/24] chore: removed variation:error and variation:success. only set ldcontext after a successful put. emit events for change and delete. fixed a bug where delete handler is listening to patch. --- .../shared/mocks/src/streamingProcessor.ts | 10 +++++----- .../sdk-client/src/LDClientImpl.test.ts | 2 +- .../shared/sdk-client/src/LDClientImpl.ts | 20 +++++++++---------- .../shared/sdk-client/src/api/LDEmitter.ts | 8 +------- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/shared/mocks/src/streamingProcessor.ts b/packages/shared/mocks/src/streamingProcessor.ts index 9cee41e4d..dbf9bf8e0 100644 --- a/packages/shared/mocks/src/streamingProcessor.ts +++ b/packages/shared/mocks/src/streamingProcessor.ts @@ -8,7 +8,10 @@ import type { export const MockStreamingProcessor = jest.fn(); -export const setupMockStreamingProcessor = (shouldError: boolean = false, putJson?: any) => { +export const setupMockStreamingProcessor = ( + shouldError: boolean = false, + putResponseJson: any = { data: { flags: {}, segments: {} } }, +) => { MockStreamingProcessor.mockImplementation( ( sdkKey: string, @@ -31,10 +34,7 @@ export const setupMockStreamingProcessor = (shouldError: boolean = false, putJso }); } else { // execute put which will resolve the init promise - process.nextTick( - () => - listeners.get('put')?.processJson(putJson ?? { data: { flags: {}, segments: {} } }), - ); + process.nextTick(() => listeners.get('put')?.processJson(putResponseJson)); } }), close: jest.fn(), diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index b7deb8778..653e0bac8 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -128,6 +128,6 @@ describe('sdk-client object', () => { message: 'test-error', }); expect(logger.error).toBeCalledTimes(1); - expect(ldc.getContext()).toEqual(carContext); + expect(ldc.getContext()).toBeUndefined(); }); }); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index ede2f9ddb..ea387deb5 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -101,18 +101,19 @@ export default class LDClientImpl implements LDClient { return this.context ? clone(this.context) : undefined; } - private createStreamListeners(): Map { + private createStreamListeners(context: LDContext): Map { const listeners = new Map(); listeners.set('put', { deserializeData: JSON.parse, processJson: (dataJson) => { this.logger.debug('Initializing all data'); + this.context = context; this.flags = {}; Object.keys(dataJson).forEach((key) => { this.flags[key] = dataJson[key]; }); - this.emitter.emit('ready', this.context); + this.emitter.emit('ready', context); }, }); @@ -121,14 +122,16 @@ export default class LDClientImpl implements LDClient { processJson: (dataJson) => { this.logger.debug(`Updating ${dataJson.key}`); this.flags[dataJson.key] = dataJson; + this.emitter.emit('change', context, dataJson.key); }, }); - listeners.set('patch', { + listeners.set('delete', { deserializeData: JSON.parse, processJson: (dataJson) => { this.logger.debug(`Deleting ${dataJson.key}`); delete this.flags[dataJson.key]; + this.emitter.emit('change', context, dataJson.key); }, }); @@ -160,11 +163,9 @@ export default class LDClientImpl implements LDClient { const error = new Error('Context was unspecified or had no key'); this.logger.error(error); this.emitter.emit('error', context, error); - throw error; + return Promise.reject(error); } - this.context = context; - const p = new Promise((resolve, reject) => { this.emitter.on('ready', (c: LDContext) => { if (c === context) { @@ -185,7 +186,7 @@ export default class LDClientImpl implements LDClient { this.sdkKey, this.clientContext, this.createStreamUriPath(context), - this.createStreamListeners(), + this.createStreamListeners(context), this.diagnosticsManager, (e) => { this.logger.error(e); @@ -242,7 +243,7 @@ export default class LDClientImpl implements LDClient { if (!found) { const error = new LDClientError(`Unknown feature flag "${flagKey}"; returning default value`); - this.emitter.emit('variation:error', this.context, error); + this.emitter.emit('error', this.context, error); this.eventProcessor.sendEvent( this.eventFactoryDefault.unknownFlagEvent(flagKey, defaultValue ?? null, evalContext), ); @@ -267,7 +268,7 @@ export default class LDClientImpl implements LDClient { const error = new LDClientError( `Wrong type "${type}" for feature flag "${flagKey}"; returning default value`, ); - this.emitter.emit('variation:error', this.context, error); + this.emitter.emit('error', this.context, error); return createErrorEvaluationDetail(ErrorKinds.WrongType, defaultValue); } } @@ -277,7 +278,6 @@ export default class LDClientImpl implements LDClient { this.logger.debug('Result value is null in variation'); successDetail.value = defaultValue; } - this.emitter.emit('variation:success', this.context); this.eventProcessor.sendEvent( eventFactory.evalEventClient(flagKey, value, defaultValue, found, evalContext, reason), ); diff --git a/packages/shared/sdk-client/src/api/LDEmitter.ts b/packages/shared/sdk-client/src/api/LDEmitter.ts index cfa28ae10..8c5350748 100644 --- a/packages/shared/sdk-client/src/api/LDEmitter.ts +++ b/packages/shared/sdk-client/src/api/LDEmitter.ts @@ -1,10 +1,4 @@ -export type EventName = - | 'connecting' - | 'ready' - | 'error' - | 'change' - | 'variation:success' - | 'variation:error'; +export type EventName = 'connecting' | 'ready' | 'error' | 'change'; type CustomEventListeners = { original: Function; From 9d458f28625c12169df614c8d2289f9c0952cc45 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 29 Nov 2023 20:06:20 -0800 Subject: [PATCH 16/24] chore: improve readme. --- packages/sdk/react-native/README.md | 12 +----------- packages/sdk/react-native/example/README.md | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/sdk/react-native/README.md b/packages/sdk/react-native/README.md index 08f5848de..da3d52acb 100644 --- a/packages/sdk/react-native/README.md +++ b/packages/sdk/react-native/README.md @@ -25,8 +25,6 @@ yarn add @launchdarkly/react-native-client-sdk TODO ```typescript -import { init } from '@launchdarkly/react-native-client-sdk'; - // TODO ``` @@ -34,15 +32,7 @@ See the full [example app](https://github.com/launchdarkly/js-core/tree/main/pac ## Developing this SDK -:information_source: You will need to setup your sdk key in the example dir. See the example [README](https://github.com/launchdarkly/js-core/blob/main/packages/sdk/react-native/example/README.md#L1). - -```shell -# at js-core repo root -yarn && yarn build - -# at sdk/react-native repo -yarn android | ios -``` +:information_source: See the example [README](https://github.com/launchdarkly/js-core/blob/main/packages/sdk/react-native/example/README.md#L1). ## About LaunchDarkly diff --git a/packages/sdk/react-native/example/README.md b/packages/sdk/react-native/example/README.md index 14b9e17b1..2307d41b9 100644 --- a/packages/sdk/react-native/example/README.md +++ b/packages/sdk/react-native/example/README.md @@ -2,19 +2,29 @@ To run the example app: -1. Create a `.env` file at the same level as this README -2. Add your client-side sdk key to that `.env` file: +1. At the js-core repo root: + +```shell +yarn && yarn build +``` + +2. Create an `.env` file at the same level as this README and add your mobile key to that `.env` file: ```shell MOBILE_KEY=abcdef12456 ``` -3. Finally +3. Replace `dev-test-flag` with your flag key in `src/welcome.tsx`. + +4. Run the app: ```shell +# Note for android, there's an issue with Flipper interfering with streaming connections +# so please run the release build. There's no such issue with ios. + # android -yarn && yarn android +yarn && yarn android-release # ios -yarn && yarn ios +yarn && yarn ios-go ``` From 66499d07f0a679ba26e3d76bcc601fd3c5794111 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 29 Nov 2023 20:49:49 -0800 Subject: [PATCH 17/24] chore: refactored promise creation to a separate function. --- .../shared/sdk-client/src/LDClientImpl.ts | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index ea387deb5..46c392720 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -44,8 +44,10 @@ export default class LDClientImpl implements LDClient { private eventFactoryWithReasons = new EventFactory(true); private emitter: LDEmitter; private flags: Flags = {}; - private readonly clientContext: ClientContext; + private identifyReadyListener?: (c: LDContext) => void; + private identifyErrorListener?: (c: LDContext, err: any) => void; + private readonly clientContext: ClientContext; /** * Creates the client object synchronously. No async, no network calls. */ @@ -156,6 +158,29 @@ export default class LDClientImpl implements LDClient { ); } + private createIdentifyPromise() { + return new Promise((resolve, reject) => { + if (this.identifyReadyListener) { + this.emitter.off('ready', this.identifyReadyListener); + } + if (this.identifyErrorListener) { + this.emitter.off('ready', this.identifyErrorListener); + } + + this.identifyReadyListener = (c: LDContext) => { + this.logger.debug(`ready: ${JSON.stringify(c)}`); + resolve(); + }; + this.identifyErrorListener = (c: LDContext, err: any) => { + this.logger.debug(`error: ${err}, context: ${JSON.stringify(c)}`); + reject(err); + }; + + this.emitter.on('ready', this.identifyReadyListener); + this.emitter.on('error', this.identifyErrorListener); + }); + } + // TODO: implement secure mode async identify(context: LDContext, _hash?: string): Promise { const checkedContext = Context.fromLDContext(context); @@ -166,22 +191,6 @@ export default class LDClientImpl implements LDClient { return Promise.reject(error); } - const p = new Promise((resolve, reject) => { - this.emitter.on('ready', (c: LDContext) => { - if (c === context) { - this.logger.debug(`ready: ${JSON.stringify(c)}`); - resolve(); - } - }); - - this.emitter.on('error', (c: LDContext, err: any) => { - this.logger.debug(`error: ${err}, context: ${JSON.stringify(c)}`); - if (c === context) { - reject(err); - } - }); - }); - this.streamer = new internal.StreamingProcessor( this.sdkKey, this.clientContext, @@ -196,7 +205,7 @@ export default class LDClientImpl implements LDClient { this.emitter.emit('connecting', context); this.streamer.start(); - return p; + return this.createIdentifyPromise(); } off(eventName: EventName, listener?: Function): void { From cbd13ccb01ab79850a40474fb3faa6cc120c97fc Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Wed, 29 Nov 2023 20:58:28 -0800 Subject: [PATCH 18/24] chore: added basic tests. --- .../src/ReactNativeLDClient.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/sdk/react-native/src/ReactNativeLDClient.test.ts diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts b/packages/sdk/react-native/src/ReactNativeLDClient.test.ts new file mode 100644 index 000000000..8cbb77b61 --- /dev/null +++ b/packages/sdk/react-native/src/ReactNativeLDClient.test.ts @@ -0,0 +1,23 @@ +import { type LDContext } from '@launchdarkly/js-client-sdk-common'; + +import ReactNativeLDClient from './ReactNativeLDClient'; + +describe('ReactNativeLDClient', () => { + let ldc: ReactNativeLDClient; + + beforeEach(() => { + ldc = new ReactNativeLDClient('mob-test'); + }); + + test('constructor', () => { + expect(ldc.sdkKey).toEqual('mob-test'); + }); + + test('createStreamUriPath', () => { + const context: LDContext = { kind: 'user', key: 'test-user-key-1' }; + + expect(ldc.createStreamUriPath(context)).toEqual( + '/meval/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlci1rZXktMSJ9', + ); + }); +}); From cfba59b372b6d964d7d3d73b375387641c6a0256 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 30 Nov 2023 10:32:47 -0800 Subject: [PATCH 19/24] Update welcome.tsx --- .../sdk/react-native/example/src/welcome.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/sdk/react-native/example/src/welcome.tsx b/packages/sdk/react-native/example/src/welcome.tsx index c32025030..fa39757fc 100644 --- a/packages/sdk/react-native/example/src/welcome.tsx +++ b/packages/sdk/react-native/example/src/welcome.tsx @@ -1,18 +1,26 @@ -import { StyleSheet, Text, View } from 'react-native'; +import { Button, StyleSheet, Text, View } from 'react-native'; -import { useBoolVariation, useLDDataSourceStatus } from '@launchdarkly/react-native-client-sdk'; +import { + useBoolVariation, + useLDClient, + useLDDataSourceStatus, +} from '@launchdarkly/react-native-client-sdk'; export default function Welcome() { const { error, status } = useLDDataSourceStatus(); const flag = useBoolVariation('dev-test-flag', false); - console.log(`============== status: ${status}`); + const ldc = useLDClient(); + const login = () => { + ldc.identify({ kind: 'user', key: 'test-user-2' }); + }; return ( Welcome to LaunchDarkly - status: {status} - error: {error?.message} + status: {status ?? 'not connected'} + {error ? error: {error.message} : null} devTestFlag: {`${flag}`} +