From 4454fd73fa860ded694acebf63b05a92bb8e9bec Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 23 Oct 2023 14:49:21 -0700 Subject: [PATCH 1/5] chore: added provider and hook. --- packages/sdk/react-native/example/App.tsx | 45 ++++--------------- .../sdk/react-native/example/src/welcome.tsx | 28 ++++++++++++ packages/sdk/react-native/link-dev.sh | 4 +- packages/sdk/react-native/package.json | 5 +++ packages/sdk/react-native/src/hooks/index.ts | 4 ++ .../react-native/src/hooks/useVariation.ts | 12 +++++ packages/sdk/react-native/src/index.ts | 5 ++- .../react-native/src/provider/LDProvider.tsx | 30 +++++++++++++ .../sdk/react-native/src/provider/index.ts | 3 ++ .../src/provider/reactSdkContext.ts | 14 ++++++ 10 files changed, 110 insertions(+), 40 deletions(-) create mode 100644 packages/sdk/react-native/example/src/welcome.tsx create mode 100644 packages/sdk/react-native/src/hooks/index.ts create mode 100644 packages/sdk/react-native/src/hooks/useVariation.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/reactSdkContext.ts diff --git a/packages/sdk/react-native/example/App.tsx b/packages/sdk/react-native/example/App.tsx index 93675937b..c996bd037 100644 --- a/packages/sdk/react-native/example/App.tsx +++ b/packages/sdk/react-native/example/App.tsx @@ -1,44 +1,17 @@ import { CLIENT_SIDE_SDK_KEY } from '@env'; -import React, { useEffect, useState } from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import { init, type LDClientImpl } from '@launchdarkly/react-native-client-sdk'; +import { LDProvider } 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 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/src/welcome.tsx b/packages/sdk/react-native/example/src/welcome.tsx new file mode 100644 index 000000000..3f1c7b824 --- /dev/null +++ b/packages/sdk/react-native/example/src/welcome.tsx @@ -0,0 +1,28 @@ +import React, { useEffect, useState } from 'react'; +import { StyleSheet, Text, View } from 'react-native'; + +import { useVariation } from '@launchdarkly/react-native-client-sdk'; + +export default function Welcome() { + const flag = useVariation('dev-test-flag'); + + return ( + + Welcome to LaunchDarkly + {flag ? <>devTestFlag: {`${flag}`} : <>loading...} + + ); +} + +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/link-dev.sh b/packages/sdk/react-native/link-dev.sh index 9bebafd6a..f9524ab97 100755 --- a/packages/sdk/react-native/link-dev.sh +++ b/packages/sdk/react-native/link-dev.sh @@ -1,7 +1,7 @@ #!/bin/bash -echo "===== Installing all dependencies..." -yarn +echo "===== Installing prod dependencies..." +yarn workspaces focus --production declare -a examples=(example) diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index b042f1152..26cf1f268 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -40,6 +40,7 @@ "ios": "yarn ./example && yarn build && yarn ./example ios" }, "peerDependencies": { + "react": "*", "react-native": "*" }, "dependencies": { @@ -50,6 +51,8 @@ "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/jest": "^29.5.0", + "@types/react": "^18.2.31", + "@types/react-native": "^0.72.5", "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "eslint": "^8.45.0", @@ -61,6 +64,8 @@ "jest": "^29.5.0", "launchdarkly-js-test-helpers": "^2.2.0", "prettier": "^3.0.0", + "react": "^18.2.0", + "react-native": "^0.72.6", "rimraf": "^5.0.5", "ts-jest": "^29.1.0", "typedoc": "0.25.0", 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..ecdbbbcc2 --- /dev/null +++ b/packages/sdk/react-native/src/hooks/index.ts @@ -0,0 +1,4 @@ +import useVariation from './useVariation'; + +// eslint-disable-next-line import/prefer-default-export +export { useVariation }; diff --git a/packages/sdk/react-native/src/hooks/useVariation.ts b/packages/sdk/react-native/src/hooks/useVariation.ts new file mode 100644 index 000000000..e1805fd19 --- /dev/null +++ b/packages/sdk/react-native/src/hooks/useVariation.ts @@ -0,0 +1,12 @@ +import { useContext } from 'react'; + +import { LDFlagValue } from '@launchdarkly/js-client-sdk-common'; + +import { context, ReactSdkContext } from '../provider/reactSdkContext'; + +const useVariation = (flagKey: string): LDFlagValue => { + const { ldClient } = useContext(context); + return ldClient?.variation(flagKey); +}; + +export default useVariation; diff --git a/packages/sdk/react-native/src/index.ts b/packages/sdk/react-native/src/index.ts index acabdb114..5f8ee1403 100644 --- a/packages/sdk/react-native/src/index.ts +++ b/packages/sdk/react-native/src/index.ts @@ -7,11 +7,12 @@ * * @packageDocumentation */ -import init from './init'; +import { useVariation } from './hooks'; import { setupPolyfill } from './polyfills'; +import { LDProvider } from './provider'; setupPolyfill(); export * from '@launchdarkly/js-client-sdk-common'; -export { init }; +export { LDProvider, useVariation }; 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..be91cf90a --- /dev/null +++ b/packages/sdk/react-native/src/provider/LDProvider.tsx @@ -0,0 +1,30 @@ +import { PropsWithChildren, useEffect, useState } from 'react'; + +import { LDClient, LDContext, LDOptions } from '@launchdarkly/js-client-sdk-common'; + +import init from '../init'; +import { Provider, ReactSdkContext } from './reactSdkContext'; + +type LDProps = { + clientSideSdkKey: string; + context: LDContext; + options?: LDOptions; +}; +const LDProvider = ({ + clientSideSdkKey, + context, + options, + children, +}: PropsWithChildren) => { + const [value, setValue] = useState({}); + + useEffect(() => { + init(clientSideSdkKey, context, options).then((ldClient: LDClient) => { + setValue({ allFlags: ldClient.allFlags(), ldClient }); + }); + }, []); + + 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..3d231f38f --- /dev/null +++ b/packages/sdk/react-native/src/provider/index.ts @@ -0,0 +1,3 @@ +import LDProvider from './LDProvider'; + +export { LDProvider }; diff --git a/packages/sdk/react-native/src/provider/reactSdkContext.ts b/packages/sdk/react-native/src/provider/reactSdkContext.ts new file mode 100644 index 000000000..8351aca45 --- /dev/null +++ b/packages/sdk/react-native/src/provider/reactSdkContext.ts @@ -0,0 +1,14 @@ +import { createContext } from 'react'; + +import { LDClient, LDFlagSet } from '@launchdarkly/js-client-sdk-common'; + +type ReactSdkContext = { + allFlags?: LDFlagSet; + ldClient?: LDClient; +}; + +const context = createContext({}); + +const { Provider, Consumer } = context; + +export { context, Provider, Consumer, ReactSdkContext }; From d8a1801639f35cb375312ee3f0065e8271ad1129 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 23 Oct 2023 15:38:57 -0700 Subject: [PATCH 2/5] chore: added typed variation[Detail] functions --- .../sdk/react-native/example/src/welcome.tsx | 5 +- packages/sdk/react-native/src/hooks/index.ts | 4 +- .../sdk/react-native/src/hooks/useLDClient.ts | 10 + .../react-native/src/hooks/useVariation.ts | 5 +- packages/sdk/react-native/src/index.ts | 4 +- .../shared/sdk-client/src/LDClientImpl.ts | 44 ++- .../shared/sdk-client/src/api/LDClient.ts | 354 ++++++++++++------ 7 files changed, 311 insertions(+), 115 deletions(-) create mode 100644 packages/sdk/react-native/src/hooks/useLDClient.ts diff --git a/packages/sdk/react-native/example/src/welcome.tsx b/packages/sdk/react-native/example/src/welcome.tsx index 3f1c7b824..8f9c13283 100644 --- a/packages/sdk/react-native/example/src/welcome.tsx +++ b/packages/sdk/react-native/example/src/welcome.tsx @@ -1,10 +1,9 @@ -import React, { useEffect, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import { useVariation } from '@launchdarkly/react-native-client-sdk'; +import { useLDClient } from '@launchdarkly/react-native-client-sdk'; export default function Welcome() { - const flag = useVariation('dev-test-flag'); + const flag = useLDClient()?.variation('dev-test-flag'); return ( diff --git a/packages/sdk/react-native/src/hooks/index.ts b/packages/sdk/react-native/src/hooks/index.ts index ecdbbbcc2..4ae49eb75 100644 --- a/packages/sdk/react-native/src/hooks/index.ts +++ b/packages/sdk/react-native/src/hooks/index.ts @@ -1,4 +1,4 @@ import useVariation from './useVariation'; +import useLDClient from './useLDClient'; -// eslint-disable-next-line import/prefer-default-export -export { useVariation }; +export { useLDClient, useVariation }; 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..2f9a9e596 --- /dev/null +++ b/packages/sdk/react-native/src/hooks/useLDClient.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; + +import { context, ReactSdkContext } from '../provider/reactSdkContext'; + +const useLDClient = () => { + const { ldClient } = useContext(context); + return ldClient; +}; + +export default useLDClient; diff --git a/packages/sdk/react-native/src/hooks/useVariation.ts b/packages/sdk/react-native/src/hooks/useVariation.ts index e1805fd19..2dabe92ea 100644 --- a/packages/sdk/react-native/src/hooks/useVariation.ts +++ b/packages/sdk/react-native/src/hooks/useVariation.ts @@ -4,9 +4,10 @@ import { LDFlagValue } from '@launchdarkly/js-client-sdk-common'; import { context, ReactSdkContext } from '../provider/reactSdkContext'; -const useVariation = (flagKey: string): LDFlagValue => { +const useVariation = (flagKey: string, defaultValue?: any): LDFlagValue => { const { ldClient } = useContext(context); - return ldClient?.variation(flagKey); + + return ldClient?.variation(flagKey) ?? defaultValue; }; export default useVariation; diff --git a/packages/sdk/react-native/src/index.ts b/packages/sdk/react-native/src/index.ts index 5f8ee1403..31e4b5bf1 100644 --- a/packages/sdk/react-native/src/index.ts +++ b/packages/sdk/react-native/src/index.ts @@ -7,7 +7,7 @@ * * @packageDocumentation */ -import { useVariation } from './hooks'; +import { useLDClient, useVariation } from './hooks'; import { setupPolyfill } from './polyfills'; import { LDProvider } from './provider'; @@ -15,4 +15,4 @@ setupPolyfill(); export * from '@launchdarkly/js-client-sdk-common'; -export { LDProvider, useVariation }; +export { LDProvider, useLDClient, useVariation }; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index c46185058..bd92242bf 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -216,7 +216,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 +223,24 @@ 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; + } + waitForInitialization(): Promise { // TODO: return Promise.resolve(undefined); @@ -233,4 +250,29 @@ export default class LDClientImpl implements LDClient { // TODO: return Promise.resolve(undefined); } + + 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); + } } diff --git a/packages/shared/sdk-client/src/api/LDClient.ts b/packages/shared/sdk-client/src/api/LDClient.ts index 5a9c3e4b7..bdaa32b11 100644 --- a/packages/shared/sdk-client/src/api/LDClient.ts +++ b/packages/shared/sdk-client/src/api/LDClient.ts @@ -1,4 +1,10 @@ -import { LDContext, LDEvaluationDetail, LDFlagSet, LDFlagValue } from '@launchdarkly/js-sdk-common'; +import { + LDContext, + LDEvaluationDetail, + LDEvaluationDetailTyped, + LDFlagSet, + LDFlagValue, +} from '@launchdarkly/js-sdk-common'; /** * The basic interface for the LaunchDarkly client. Platform-specific SDKs may add some methods of their own. @@ -9,70 +15,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 +106,86 @@ 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. + * Determines the numeric variation of a feature flag. * - * 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. + * 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. + * @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 +228,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 +292,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; } From 5b0c2d4c564e0450a650321f214e67d5616efcfa Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 23 Oct 2023 16:03:40 -0700 Subject: [PATCH 3/5] chore: added variation[Detail] hooks. labelled outstanding todos. --- .../sdk/react-native/example/src/welcome.tsx | 4 +-- packages/sdk/react-native/src/hooks/index.ts | 20 ++++++++++-- .../src/hooks/useBoolVariation.ts | 13 ++++++++ .../src/hooks/useJsonVariation.ts | 13 ++++++++ .../src/hooks/useNumberVariation.ts | 13 ++++++++ .../src/hooks/useStringVariation.ts | 13 ++++++++ .../react-native/src/hooks/useVariation.ts | 14 +++++---- packages/sdk/react-native/src/index.ts | 31 +++++++++++++++++-- .../shared/sdk-client/src/LDClientImpl.ts | 6 ++-- 9 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 packages/sdk/react-native/src/hooks/useBoolVariation.ts create mode 100644 packages/sdk/react-native/src/hooks/useJsonVariation.ts create mode 100644 packages/sdk/react-native/src/hooks/useNumberVariation.ts create mode 100644 packages/sdk/react-native/src/hooks/useStringVariation.ts diff --git a/packages/sdk/react-native/example/src/welcome.tsx b/packages/sdk/react-native/example/src/welcome.tsx index 8f9c13283..757944512 100644 --- a/packages/sdk/react-native/example/src/welcome.tsx +++ b/packages/sdk/react-native/example/src/welcome.tsx @@ -1,9 +1,9 @@ import { StyleSheet, Text, View } from 'react-native'; -import { useLDClient } from '@launchdarkly/react-native-client-sdk'; +import { useBoolVariation } from '@launchdarkly/react-native-client-sdk'; export default function Welcome() { - const flag = useLDClient()?.variation('dev-test-flag'); + const flag = useBoolVariation('dev-test-flag', false); return ( diff --git a/packages/sdk/react-native/src/hooks/index.ts b/packages/sdk/react-native/src/hooks/index.ts index 4ae49eb75..436359785 100644 --- a/packages/sdk/react-native/src/hooks/index.ts +++ b/packages/sdk/react-native/src/hooks/index.ts @@ -1,4 +1,20 @@ -import useVariation from './useVariation'; +import { useBoolVariation, useBoolVariationDetail } from './useBoolVariation'; +import { useJsonVariation, useJsonVariationDetail } from './useJsonVariation'; import useLDClient from './useLDClient'; +import { useNumberVariation, useNumberVariationDetail } from './useNumberVariation'; +import { useStringVariation, useStringVariationDetail } from './useStringVariation'; +import { useVariation, useVariationDetail } from './useVariation'; -export { useLDClient, useVariation }; +export { + useLDClient, + useVariation, + useVariationDetail, + useNumberVariation, + useNumberVariationDetail, + useBoolVariation, + useBoolVariationDetail, + useStringVariation, + useStringVariationDetail, + useJsonVariationDetail, + useJsonVariation, +}; diff --git a/packages/sdk/react-native/src/hooks/useBoolVariation.ts b/packages/sdk/react-native/src/hooks/useBoolVariation.ts new file mode 100644 index 000000000..2705fadd6 --- /dev/null +++ b/packages/sdk/react-native/src/hooks/useBoolVariation.ts @@ -0,0 +1,13 @@ +import useLDClient from './useLDClient'; + +export const useBoolVariation = (flagKey: string, defaultValue: boolean) => { + const ldClient = useLDClient(); + return ldClient?.boolVariation(flagKey, defaultValue) ?? defaultValue; +}; + +export const useBoolVariationDetail = (flagKey: string, defaultValue: boolean) => { + const ldClient = useLDClient(); + return ldClient?.boolVariationDetail(flagKey, defaultValue) ?? defaultValue; +}; + +export default useBoolVariation; diff --git a/packages/sdk/react-native/src/hooks/useJsonVariation.ts b/packages/sdk/react-native/src/hooks/useJsonVariation.ts new file mode 100644 index 000000000..d52b5c7d3 --- /dev/null +++ b/packages/sdk/react-native/src/hooks/useJsonVariation.ts @@ -0,0 +1,13 @@ +import useLDClient from './useLDClient'; + +export const useJsonVariation = (flagKey: string, defaultValue: unknown) => { + const ldClient = useLDClient(); + return ldClient?.jsonVariation(flagKey, defaultValue) ?? defaultValue; +}; + +export const useJsonVariationDetail = (flagKey: string, defaultValue: unknown) => { + const ldClient = useLDClient(); + return ldClient?.jsonVariationDetail(flagKey, defaultValue) ?? defaultValue; +}; + +export default useJsonVariation; diff --git a/packages/sdk/react-native/src/hooks/useNumberVariation.ts b/packages/sdk/react-native/src/hooks/useNumberVariation.ts new file mode 100644 index 000000000..1311e688a --- /dev/null +++ b/packages/sdk/react-native/src/hooks/useNumberVariation.ts @@ -0,0 +1,13 @@ +import useLDClient from './useLDClient'; + +export const useNumberVariation = (flagKey: string, defaultValue: number) => { + const ldClient = useLDClient(); + return ldClient?.numberVariation(flagKey, defaultValue) ?? defaultValue; +}; + +export const useNumberVariationDetail = (flagKey: string, defaultValue: number) => { + const ldClient = useLDClient(); + return ldClient?.numberVariationDetail(flagKey, defaultValue) ?? defaultValue; +}; + +export default useNumberVariation; diff --git a/packages/sdk/react-native/src/hooks/useStringVariation.ts b/packages/sdk/react-native/src/hooks/useStringVariation.ts new file mode 100644 index 000000000..825c4788c --- /dev/null +++ b/packages/sdk/react-native/src/hooks/useStringVariation.ts @@ -0,0 +1,13 @@ +import useLDClient from './useLDClient'; + +export const useStringVariation = (flagKey: string, defaultValue: string) => { + const ldClient = useLDClient(); + return ldClient?.stringVariation(flagKey, defaultValue) ?? defaultValue; +}; + +export const useStringVariationDetail = (flagKey: string, defaultValue: string) => { + const ldClient = useLDClient(); + return ldClient?.stringVariationDetail(flagKey, defaultValue) ?? defaultValue; +}; + +export default useStringVariation; diff --git a/packages/sdk/react-native/src/hooks/useVariation.ts b/packages/sdk/react-native/src/hooks/useVariation.ts index 2dabe92ea..daea9cc21 100644 --- a/packages/sdk/react-native/src/hooks/useVariation.ts +++ b/packages/sdk/react-native/src/hooks/useVariation.ts @@ -1,13 +1,15 @@ -import { useContext } from 'react'; - import { LDFlagValue } from '@launchdarkly/js-client-sdk-common'; -import { context, ReactSdkContext } from '../provider/reactSdkContext'; +import useLDClient from './useLDClient'; -const useVariation = (flagKey: string, defaultValue?: any): LDFlagValue => { - const { ldClient } = useContext(context); +export const useVariation = (flagKey: string, defaultValue?: any): LDFlagValue => { + const ldClient = useLDClient(); + return ldClient?.variation(flagKey, defaultValue) ?? defaultValue; +}; - return ldClient?.variation(flagKey) ?? defaultValue; +export const useVariationDetail = (flagKey: string, defaultValue?: any): LDFlagValue => { + const ldClient = useLDClient(); + return ldClient?.variationDetail(flagKey, defaultValue) ?? defaultValue; }; export default useVariation; diff --git a/packages/sdk/react-native/src/index.ts b/packages/sdk/react-native/src/index.ts index 31e4b5bf1..74746efc6 100644 --- a/packages/sdk/react-native/src/index.ts +++ b/packages/sdk/react-native/src/index.ts @@ -1,13 +1,25 @@ /** * This is the API reference for the React Native LaunchDarkly SDK. * - * TODO: + * TODO: add rn sdk api docs * * For more information, see the SDK reference guide. * * @packageDocumentation */ -import { useLDClient, useVariation } from './hooks'; +import { + useBoolVariation, + useBoolVariationDetail, + useJsonVariation, + useJsonVariationDetail, + useLDClient, + useNumberVariation, + useNumberVariationDetail, + useStringVariation, + useStringVariationDetail, + useVariation, + useVariationDetail, +} from './hooks'; import { setupPolyfill } from './polyfills'; import { LDProvider } from './provider'; @@ -15,4 +27,17 @@ setupPolyfill(); export * from '@launchdarkly/js-client-sdk-common'; -export { LDProvider, useLDClient, useVariation }; +export { + LDProvider, + useLDClient, + useVariation, + useVariationDetail, + useNumberVariation, + useNumberVariationDetail, + useBoolVariation, + useBoolVariationDetail, + useStringVariation, + useStringVariationDetail, + useJsonVariationDetail, + useJsonVariation, +}; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index bd92242bf..6180546d3 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -134,8 +134,8 @@ export default class LDClientImpl implements LDClient { this.emitter.on(eventName, listener); } + // TODO: setStreaming setStreaming(value?: boolean): void { - // TODO: } track(key: string, data?: any, metricValue?: number): void { @@ -241,13 +241,13 @@ export default class LDClientImpl implements LDClient { ]).value; } + // TODO: waitForInitialization waitForInitialization(): Promise { - // TODO: return Promise.resolve(undefined); } + // TODO: waitUntilReady waitUntilReady(): Promise { - // TODO: return Promise.resolve(undefined); } From d96c415de9bd4f31e5e61d81c80ee236ac972abf Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 23 Oct 2023 16:23:38 -0700 Subject: [PATCH 4/5] fix: eslint error. disable eslint file ext import rule except for json. --- .eslintrc.js | 1 + packages/sdk/react-native/src/provider/index.ts | 1 + packages/shared/sdk-client/src/LDClientImpl.ts | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) 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/src/provider/index.ts b/packages/sdk/react-native/src/provider/index.ts index 3d231f38f..6706518fb 100644 --- a/packages/sdk/react-native/src/provider/index.ts +++ b/packages/sdk/react-native/src/provider/index.ts @@ -1,3 +1,4 @@ import LDProvider from './LDProvider'; +// eslint-disable-next-line import/prefer-default-export export { LDProvider }; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 6180546d3..ef25af56a 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -135,8 +135,7 @@ export default class LDClientImpl implements LDClient { } // TODO: setStreaming - setStreaming(value?: boolean): void { - } + setStreaming(value?: boolean): void {} track(key: string, data?: any, metricValue?: number): void { const checkedContext = Context.fromLDContext(this.context); From 8602f39cc5b551adadd00303a980394829931663 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Mon, 23 Oct 2023 16:31:14 -0700 Subject: [PATCH 5/5] fix: only copy prod deps to example app. --- packages/sdk/react-native/link-dev.sh | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/sdk/react-native/link-dev.sh b/packages/sdk/react-native/link-dev.sh index f9524ab97..da6e062ed 100755 --- a/packages/sdk/react-native/link-dev.sh +++ b/packages/sdk/react-native/link-dev.sh @@ -1,7 +1,7 @@ #!/bin/bash -echo "===== Installing prod dependencies..." -yarn workspaces focus --production +echo "===== Installing all dependencies..." +yarn declare -a examples=(example) @@ -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