diff --git a/.babelrc b/.babelrc deleted file mode 100644 index d4b74b5..0000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["module:metro-react-native-babel-preset"] -} diff --git a/.circleci/config.yml b/.circleci/config.yml index 53fff3e..f401b45 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -50,17 +50,30 @@ jobs: common: docker: - - image: circleci/node:11.10.1 + - image: cimg/node:current steps: - checkout - run: npm install + - run: mkdir -p reports/jest + - run: + command: npm run test:junit + environment: + JEST_JUNIT_OUTPUT_DIR: "./reports/jest" + - run: npm run check-typescript + - store_test_results: + path: reports + workflows: version: 2 android-ios: jobs: - - android - - ios - common + - android: + requires: + - common + - ios: + requires: + - common diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ae7bdb..0398d37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ All notable changes to the LaunchDarkly React Native SDK will be documented in t ### Fixed: - Correct usages of undeclared variables when registering or un-registering connection mode or all flags listeners. ([#82](https://github.com/launchdarkly/react-native-client-sdk/issues/82)) - ## [4.0.4] - 2021-06-02 ### Fixed: - iOS: Fixed an issue where an exception was thrown when calling `LDClient.configure` with an optional `timeout` ([#80](https://github.com/launchdarkly/react-native-client-sdk/issues/80)). diff --git a/README.md b/README.md index 5e3eab0..e4ad87b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ LaunchDarkly Client-Side SDK for React Native =========================== -[![CircleCI](https://circleci.com/gh/launchdarkly/react-native-client-sdk.svg?style=svg)](https://circleci.com/gh/launchdarkly/react-native-client-sdk) +[![NPM](https://img.shields.io/npm/v/launchdarkly-react-native-client-sdk.svg)](https://www.npmjs.com/package/launchdarkly-react-native-client-sdk) +[![CircleCI](https://circleci.com/gh/launchdarkly/react-native-client-sdk.svg?style=shield)](https://circleci.com/gh/launchdarkly/react-native-client-sdk) +[![Documentation](https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8)](https://launchdarkly.github.io/react-native-client-sdk) LaunchDarkly overview ------------------------- @@ -23,12 +25,12 @@ This SDK is currently compatible with React Native 0.64.x and Xcode 12 and is te Getting started --------------- -Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/client-side/react-native#getting-started) for instructions on getting started with using the SDK. +Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/client-side/react/react-native#getting-started) for instructions on getting started with using the SDK. Learn more ----------- -Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/client-side/react-native). +Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/client-side/react/react-native). Testing ------- diff --git a/__mocks__/native.js b/__mocks__/native.js new file mode 100644 index 0000000..a4a9fce --- /dev/null +++ b/__mocks__/native.js @@ -0,0 +1,84 @@ +const mockNativeModule = { + FLAG_PREFIX: "test-flag-prefix", + ALL_FLAGS_PREFIX: "test-all-flags-prefix", + CONNECTION_MODE_PREFIX: "test-connection-mode-prefix", + + configure: jest.fn(), + configureWithTimeout: jest.fn(), + + boolVariation: jest.fn(), + boolVariationDefaultValue: jest.fn(), + numberVariation: jest.fn(), + numberVariationDefaultValue: jest.fn(), + stringVariation: jest.fn(), + stringVariationDefaultValue: jest.fn(), + jsonVariationNone: jest.fn(), + jsonVariationNumber: jest.fn(), + jsonVariationBool: jest.fn(), + jsonVariationString: jest.fn(), + jsonVariationArray: jest.fn(), + jsonVariationObject: jest.fn(), + + boolVariationDetail: jest.fn(), + boolVariationDetailDefaultValue: jest.fn(), + numberVariationDetail: jest.fn(), + numberVariationDetailDefaultValue: jest.fn(), + stringVariationDetail: jest.fn(), + stringVariationDetailDefaultValue: jest.fn(), + jsonVariationDetailNone: jest.fn(), + jsonVariationDetailNumber: jest.fn(), + jsonVariationDetailBool: jest.fn(), + jsonVariationDetailString: jest.fn(), + jsonVariationDetailArray: jest.fn(), + jsonVariationDetailObject: jest.fn(), + + allFlags: jest.fn(), + + trackNumber: jest.fn(), + trackBool: jest.fn(), + trackString: jest.fn(), + trackArray: jest.fn(), + trackObject: jest.fn(), + track: jest.fn(), + + trackNumberMetricValue: jest.fn(), + trackBoolMetricValue: jest.fn(), + trackStringMetricValue: jest.fn(), + trackArrayMetricValue: jest.fn(), + trackObjectMetricValue: jest.fn(), + trackMetricValue: jest.fn(), + + setOffline: jest.fn(), + isOffline: jest.fn(), + setOnline: jest.fn(), + isInitialized: jest.fn(), + flush: jest.fn(), + close: jest.fn(), + identify: jest.fn(), + alias: jest.fn(), + getConnectionMode: jest.fn(), + getLastSuccessfulConnection: jest.fn(), + getLastFailedConnection: jest.fn(), + getLastFailure: jest.fn(), + + registerFeatureFlagListener: jest.fn(), + unregisterFeatureFlagListener: jest.fn(), + registerCurrentConnectionModeListener: jest.fn(), + unregisterCurrentConnectionModeListener: jest.fn(), + registerAllFlagsListener: jest.fn(), + unregisterAllFlagsListener: jest.fn() +}; + +jest.mock('react-native', + () => { + return { + NativeModules: { + LaunchdarklyReactNativeClient: mockNativeModule + }, + NativeEventEmitter: jest.fn().mockImplementation(() => { + return { addListener: jest.fn() }; + }) + } + }, + { virtual: true } +); diff --git a/android/build.gradle b/android/build.gradle index 8eeeced..0ccd3a9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,8 +1,7 @@ - buildscript { ext { buildToolsVersion = "28.0.2" - minSdkVersion = 16 + minSdkVersion = 21 compileSdkVersion = 28 targetSdkVersion = 28 supportLibVersion = "28.0.0" @@ -13,7 +12,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.android.tools.build:gradle:4.1.3' } } @@ -24,7 +23,7 @@ android { buildToolsVersion "28.0.3" defaultConfig { - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 26 versionCode 1 versionName "1.0" @@ -32,6 +31,10 @@ android { lintOptions { abortOnError false } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } allprojects { @@ -48,9 +51,9 @@ allprojects { } dependencies { - implementation 'com.facebook.react:react-native:+' - implementation 'com.launchdarkly:launchdarkly-android-client-sdk:2.14.1' - implementation 'com.jakewharton.timber:timber:4.7.1' - implementation "com.google.code.gson:gson:2.8.5" + implementation("com.facebook.react:react-native:+") + implementation("com.launchdarkly:launchdarkly-android-client-sdk:3.1.0") + implementation("com.jakewharton.timber:timber:4.7.1") + implementation("com.google.code.gson:gson:2.8.6") } diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..5bac8ac --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 82cdf6a..77115ed 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/src/main/java/com/launchdarkly/reactnative/LaunchdarklyReactNativeClientModule.java b/android/src/main/java/com/launchdarkly/reactnative/LaunchdarklyReactNativeClientModule.java index 47910e6..31af933 100644 --- a/android/src/main/java/com/launchdarkly/reactnative/LaunchdarklyReactNativeClientModule.java +++ b/android/src/main/java/com/launchdarkly/reactnative/LaunchdarklyReactNativeClientModule.java @@ -3,7 +3,10 @@ import android.app.Application; import android.net.Uri; +import androidx.arch.core.util.Function; + import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; @@ -18,34 +21,31 @@ import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonParser; -import com.google.gson.JsonParseException; -import com.launchdarkly.android.FeatureFlagChangeListener; -import com.launchdarkly.android.LDClient; -import com.launchdarkly.android.LDConfig; -import com.launchdarkly.android.LDCountryCode; -import com.launchdarkly.android.LDUser; -import com.launchdarkly.android.ConnectionInformation; -import com.launchdarkly.android.LDStatusListener; -import com.launchdarkly.android.LDAllFlagsListener; -import com.launchdarkly.android.EvaluationDetail; -import com.launchdarkly.android.EvaluationReason; -import com.launchdarkly.android.LDFailure; -import com.launchdarkly.android.LaunchDarklyException; +import com.launchdarkly.sdk.ArrayBuilder; +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.ObjectBuilder; +import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.android.ConnectionInformation; +import com.launchdarkly.sdk.android.FeatureFlagChangeListener; +import com.launchdarkly.sdk.android.LDAllFlagsListener; +import com.launchdarkly.sdk.android.LDClient; +import com.launchdarkly.sdk.android.LDConfig; +import com.launchdarkly.sdk.android.LDFailure; +import com.launchdarkly.sdk.android.LDStatusListener; +import com.launchdarkly.sdk.android.LaunchDarklyException; + +import org.jetbrains.annotations.NotNull; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.List; import java.util.concurrent.ExecutionException; import timber.log.Timber; @@ -53,32 +53,37 @@ public class LaunchdarklyReactNativeClientModule extends ReactContextBaseJavaModule { enum ConfigMapping { - CONFIG_MOBILE_KEY("mobileKey", ConfigEntryType.String, "setMobileKey"), - CONFIG_BASE_URI("pollUri", ConfigEntryType.Uri, "setPollUri"), - CONFIG_EVENTS_URI("eventsUri", ConfigEntryType.UriMobile, "setEventsUri"), - CONFIG_STREAM_URI("streamUri", ConfigEntryType.Uri, "setStreamUri"), - CONFIG_EVENTS_CAPACITY("eventsCapacity", ConfigEntryType.Integer, "setEventsCapacity"), - CONFIG_EVENTS_FLUSH_INTERVAL("eventsFlushIntervalMillis", ConfigEntryType.Integer, "setEventsFlushIntervalMillis"), - CONFIG_CONNECTION_TIMEOUT("connectionTimeoutMillis", ConfigEntryType.Integer, "setConnectionTimeoutMillis"), - CONFIG_POLLING_INTERVAL("pollingIntervalMillis", ConfigEntryType.Integer, "setPollingIntervalMillis"), - CONFIG_BACKGROUND_POLLING_INTERVAL("backgroundPollingIntervalMillis", ConfigEntryType.Integer, "setBackgroundPollingIntervalMillis"), - CONFIG_USE_REPORT("useReport", ConfigEntryType.Boolean, "setUseReport"), - CONFIG_STREAM("stream", ConfigEntryType.Boolean, "setStream"), - CONFIG_DISABLE_BACKGROUND_UPDATING("disableBackgroundUpdating", ConfigEntryType.Boolean, "setDisableBackgroundUpdating"), - CONFIG_OFFLINE("offline", ConfigEntryType.Boolean, "setOffline"), - CONFIG_PRIVATE_ATTRIBUTES("privateAttributeNames", ConfigEntryType.StringSet, "setPrivateAttributeNames"), - CONFIG_EVALUATION_REASONS("evaluationReasons", ConfigEntryType.Boolean, "setEvaluationReasons"), - CONFIG_WRAPPER_NAME("wrapperName", ConfigEntryType.String, "setWrapperName"), - CONFIG_WRAPPER_VERSION("wrapperVersion", ConfigEntryType.String, "setWrapperVersion"), - CONFIG_MAX_CACHED_USERS("maxCachedUsers", ConfigEntryType.Integer, "setMaxCachedUsers"), - CONFIG_DIAGNOSTIC_OPT_OUT("diagnosticOptOut", ConfigEntryType.Boolean, "setDiagnosticOptOut"), - CONFIG_DIAGNOSTIC_RECORDING_INTERVAL("diagnosticRecordingIntervalMillis", ConfigEntryType.Integer, "setDiagnosticRecordingIntervalMillis"), - CONFIG_SECONDARY_MOBILE_KEYS("secondaryMobileKeys", ConfigEntryType.Map, "setSecondaryMobileKeys"); + CONFIG_MOBILE_KEY("mobileKey", ConfigEntryType.String), + CONFIG_BASE_URI("pollUri", ConfigEntryType.Uri), + CONFIG_EVENTS_URI("eventsUri", ConfigEntryType.Uri), + CONFIG_STREAM_URI("streamUri", ConfigEntryType.Uri), + CONFIG_EVENTS_CAPACITY("eventsCapacity", ConfigEntryType.Integer), + CONFIG_EVENTS_FLUSH_INTERVAL("eventsFlushIntervalMillis", ConfigEntryType.Integer), + CONFIG_CONNECTION_TIMEOUT("connectionTimeoutMillis", ConfigEntryType.Integer), + CONFIG_POLLING_INTERVAL("pollingIntervalMillis", ConfigEntryType.Integer), + CONFIG_BACKGROUND_POLLING_INTERVAL("backgroundPollingIntervalMillis", ConfigEntryType.Integer), + CONFIG_USE_REPORT("useReport", ConfigEntryType.Boolean), + CONFIG_STREAM("stream", ConfigEntryType.Boolean), + CONFIG_DISABLE_BACKGROUND_UPDATING("disableBackgroundUpdating", ConfigEntryType.Boolean), + CONFIG_OFFLINE("offline", ConfigEntryType.Boolean), + CONFIG_PRIVATE_ATTRIBUTES("privateAttributeNames", ConfigEntryType.UserAttributes, "privateAttributes"), + CONFIG_EVALUATION_REASONS("evaluationReasons", ConfigEntryType.Boolean), + CONFIG_WRAPPER_NAME("wrapperName", ConfigEntryType.String), + CONFIG_WRAPPER_VERSION("wrapperVersion", ConfigEntryType.String), + CONFIG_MAX_CACHED_USERS("maxCachedUsers", ConfigEntryType.Integer), + CONFIG_DIAGNOSTIC_OPT_OUT("diagnosticOptOut", ConfigEntryType.Boolean), + CONFIG_DIAGNOSTIC_RECORDING_INTERVAL("diagnosticRecordingIntervalMillis", ConfigEntryType.Integer), + CONFIG_SECONDARY_MOBILE_KEYS("secondaryMobileKeys", ConfigEntryType.Map), + CONFIG_AUTO_ALIASING_OPT_OUT("autoAliasingOptOut", ConfigEntryType.Boolean); final String key; final ConfigEntryType type; private final Method setter; + ConfigMapping(String key, ConfigEntryType type) { + this(key, type, key); + } + ConfigMapping(String key, ConfigEntryType type, String setterName) { this.key = key; this.type = type; @@ -89,9 +94,7 @@ void loadFromMap(ReadableMap map, LDConfig.Builder builder) { if (map.hasKey(key) && map.getType(key).equals(type.getReadableType())) { try { setter.invoke(builder, type.getFromMap(map, key)); - } catch (IllegalAccessException e) { - Timber.w(e); - } catch (InvocationTargetException e) { + } catch (IllegalAccessException | InvocationTargetException e) { Timber.w(e); } } @@ -107,7 +110,7 @@ enum UserConfigMapping { USER_NAME("name", ConfigEntryType.String, "name", "privateName"), USER_SECONDARY("secondary", ConfigEntryType.String, "secondary", "privateSecondary"), USER_AVATAR("avatar", ConfigEntryType.String, "avatar", "privateAvatar"), - USER_COUNTRY("country", ConfigEntryType.Country, "country", "privateCountry"); + USER_COUNTRY("country", ConfigEntryType.String, "country", "privateCountry"); final String key; final ConfigEntryType type; @@ -129,20 +132,19 @@ void loadFromMap(ReadableMap map, LDUser.Builder builder, Set privateAtt } else { setter.invoke(builder, type.getFromMap(map, key)); } - } catch (IllegalAccessException e) { - Timber.w(e); - } catch (InvocationTargetException e) { + } catch (IllegalAccessException | InvocationTargetException e) { Timber.w(e); } } } } - private Map listeners = new HashMap<>(); - private Map connectionModeListeners = new HashMap<>(); - private Map allFlagsListeners = new HashMap<>(); + private final Map listeners = new HashMap<>(); + private final Map connectionModeListeners = new HashMap<>(); + private final Map allFlagsListeners = new HashMap<>(); - private static Gson gson = new Gson(); + private static final Gson gson = new Gson(); + private static boolean debugLoggingStarted = false; public LaunchdarklyReactNativeClientModule(ReactApplicationContext reactContext) { super(reactContext); @@ -156,7 +158,7 @@ public LaunchdarklyReactNativeClientModule(ReactApplicationContext reactContext) */ @SuppressWarnings("SameReturnValue") @Override - public String getName() { + public @NotNull String getName() { return "LaunchdarklyReactNativeClient"; } @@ -195,6 +197,14 @@ public void configureWithTimeout(ReadableMap config, ReadableMap user, Integer t } private void internalConfigure(ReadableMap config, ReadableMap user, final Integer timeout, final Promise promise) { + if (!debugLoggingStarted + && config.hasKey("debugMode") + && config.getType("debugMode").equals(ReadableType.Boolean) + && config.getBoolean("debugMode")) { + Timber.plant(new Timber.DebugTree()); + LaunchdarklyReactNativeClientModule.debugLoggingStarted = true; + } + try { LDClient.get(); promise.reject(ERROR_INIT, "Client was already initialized"); @@ -283,76 +293,12 @@ private LDUser.Builder userBuild(ReadableMap options) { } if (options.hasKey("custom") && options.getType("custom") == ReadableType.Map) { - ReadableMap custom = options.getMap("custom"); - ReadableMapKeySetIterator iterator = custom.keySetIterator(); - while (iterator.hasNextKey()) { - String customKey = iterator.nextKey(); - switch (custom.getType(customKey)) { - case Boolean: - if (privateAttrs.contains(customKey)) { - userBuilder.privateCustom(customKey, custom.getBoolean(customKey)); - } else { - userBuilder.custom(customKey, custom.getBoolean(customKey)); - } - break; - case Number: - if (privateAttrs.contains(customKey)) { - userBuilder.privateCustom(customKey, custom.getDouble(customKey)); - } else { - userBuilder.custom(customKey, custom.getDouble(customKey)); - } - break; - case String: - if (privateAttrs.contains(customKey)) { - userBuilder.privateCustom(customKey, custom.getString(customKey)); - } else { - userBuilder.custom(customKey, custom.getString(customKey)); - } - break; - case Array: - ReadableArray array = custom.getArray(customKey); - ArrayList strArray = null; - ArrayList numArray = null; - for (int i = 0; i < array.size(); i++) { - if (strArray != null) { - if (array.getType(i) == ReadableType.String) { - strArray.add(array.getString(i)); - } - } else if (numArray != null) { - if (array.getType(i) == ReadableType.Number) { - numArray.add(array.getDouble(i)); - } - } else if (array.getType(i) == ReadableType.String) { - strArray = new ArrayList<>(); - strArray.add(array.getString(i)); - } else if (array.getType(i) == ReadableType.Number) { - numArray = new ArrayList<>(); - numArray.add(array.getDouble(i)); - } - } - if (strArray != null) { - if (privateAttrs.contains(customKey)) { - userBuilder.privateCustomString(customKey, strArray); - } else { - userBuilder.customString(customKey, strArray); - } - } else if (numArray != null) { - if (privateAttrs.contains(customKey)) { - userBuilder.privateCustomNumber(customKey, numArray); - } else { - userBuilder.customNumber(customKey, numArray); - } - } else { - if (privateAttrs.contains(customKey)) { - userBuilder.privateCustomString(customKey, new ArrayList()); - } else { - userBuilder.customString(customKey, new ArrayList()); - } - } - break; - case Null: - case Map: - break; + LDValue custom = toLDValue(options.getMap("custom")); + for (String customKey : custom.keys()) { + if (privateAttrs.contains(customKey)) { + userBuilder.privateCustom(customKey, custom.get(customKey)); + } else { + userBuilder.custom(customKey, custom.get(customKey)); } } } @@ -361,244 +307,150 @@ private LDUser.Builder userBuild(ReadableMap options) { } @ReactMethod - public void boolVariation(String flagKey, String environment, Promise promise) { - boolVariationDefaultValue(flagKey, null, environment, promise); - } - - @ReactMethod - public void boolVariationDefaultValue(String flagKey, Boolean defaultValue, String environment, Promise promise) { - try { - promise.resolve(LDClient.getForMobileKey(environment).boolVariation(flagKey, defaultValue)); - } catch (Exception e) { - promise.resolve(defaultValue); - } - } - - @ReactMethod - public void intVariation(String flagKey, String environment, Promise promise) { - intVariationDefaultValue(flagKey, null, environment, promise); - } - - @ReactMethod - public void intVariationDefaultValue(String flagKey, Integer defaultValue, String environment, Promise promise) { - try { - promise.resolve(LDClient.getForMobileKey(environment).intVariation(flagKey, defaultValue)); - } catch (Exception e) { - promise.resolve(defaultValue); - } - } - - @ReactMethod - public void floatVariation(String flagKey, String environment, Promise promise) { - floatVariationDefaultValue(flagKey, null, environment, promise); + public void boolVariation(String flagKey, boolean defaultValue, String environment, Promise promise) { + variation(LDClient::boolVariation, LDValue::of, flagKey, defaultValue, environment, promise); } @ReactMethod - public void floatVariationDefaultValue(String flagKey, Float defaultValue, String environment, Promise promise) { - try { - promise.resolve(LDClient.getForMobileKey(environment).doubleVariation(flagKey, defaultValue.doubleValue())); - } catch (Exception e) { - promise.resolve(defaultValue); - } + public void numberVariation(String flagKey, double defaultValue, String environment, Promise promise) { + variation(LDClient::doubleVariation, LDValue::of, flagKey, defaultValue, environment, promise); } @ReactMethod - public void stringVariation(String flagKey, String environment, Promise promise) { - stringVariationDefaultValue(flagKey, null, environment, promise); - } - - @ReactMethod - public void stringVariationDefaultValue(String flagKey, String defaultValue, String environment, Promise promise) { - try { - promise.resolve(LDClient.getForMobileKey(environment).stringVariation(flagKey, defaultValue)); - } catch (Exception e) { - promise.resolve(defaultValue); - } + public void stringVariation(String flagKey, String defaultValue, String environment, Promise promise) { + variation(LDClient::stringVariation, LDValue::of, flagKey, defaultValue, environment, promise); } @ReactMethod public void jsonVariationNone(String flagKey, String environment, Promise promise) { - jsonVariationBase(flagKey, null, environment, promise); + variation(LDClient::jsonValueVariation, id -> id, flagKey, LDValue.ofNull(), environment, promise); } @ReactMethod - public void jsonVariationNumber(String flagKey, Double defaultValue, String environment, Promise promise) { - jsonVariationBase(flagKey, new JsonPrimitive(defaultValue), environment, promise); + public void jsonVariationNumber(String flagKey, double defaultValue, String environment, Promise promise) { + variation(LDClient::jsonValueVariation, id -> id, flagKey, LDValue.of(defaultValue), environment, promise); } @ReactMethod - public void jsonVariationBool(String flagKey, Boolean defaultValue, String environment, Promise promise) { - jsonVariationBase(flagKey, new JsonPrimitive(defaultValue), environment, promise); + public void jsonVariationBool(String flagKey, boolean defaultValue, String environment, Promise promise) { + variation(LDClient::jsonValueVariation, id -> id, flagKey, LDValue.of(defaultValue), environment, promise); } @ReactMethod public void jsonVariationString(String flagKey, String defaultValue, String environment, Promise promise) { - jsonVariationBase(flagKey, new JsonPrimitive(defaultValue), environment, promise); + variation(LDClient::jsonValueVariation, id -> id, flagKey, LDValue.of(defaultValue), environment, promise); } @ReactMethod public void jsonVariationArray(String flagKey, ReadableArray defaultValue, String environment, Promise promise) { - jsonVariationBase(flagKey, toJsonArray(defaultValue), environment, promise); + variation(LDClient::jsonValueVariation, id -> id, flagKey, toLDValue(defaultValue), environment, promise); } @ReactMethod public void jsonVariationObject(String flagKey, ReadableMap defaultValue, String environment, Promise promise) { - jsonVariationBase(flagKey, toJsonObject(defaultValue), environment, promise); - } - - @ReactMethod - public void boolVariationDetail(String flagKey, String environment, Promise promise) { - boolVariationDetailDefaultValue(flagKey, null, environment, promise); - } - - @ReactMethod - public void boolVariationDetailDefaultValue(String flagKey, Boolean defaultValue, String environment, Promise promise) { - EvaluationDetail detailResult; - try { - detailResult = LDClient.getForMobileKey(environment).boolVariationDetail(flagKey, defaultValue); - } catch (Exception e) { - Timber.w(e); - detailResult = new EvaluationDetail(EvaluationReason.error(EvaluationReason.ErrorKind.EXCEPTION), null, defaultValue); - } - JsonObject jsonObject = gson.toJsonTree(detailResult).getAsJsonObject(); - WritableMap detailMap = fromJsonObject(jsonObject); - promise.resolve(detailMap); + variation(LDClient::jsonValueVariation, id -> id, flagKey, toLDValue(defaultValue), environment, promise); } - @ReactMethod - public void intVariationDetail(String flagKey, String environment, Promise promise) { - intVariationDetailDefaultValue(flagKey, null, environment, promise); + interface EvalCall { + T call(LDClient client, String flagKey, T defaultValue); } - @ReactMethod - public void intVariationDetailDefaultValue(String flagKey, Integer defaultValue, String environment, Promise promise) { - EvaluationDetail detailResult; + private void variation(EvalCall eval, Function transform, + String flagKey, T defaultValue, String environment, Promise promise) { try { - detailResult = LDClient.getForMobileKey(environment).intVariationDetail(flagKey, defaultValue); + promise.resolve(ldValueToBridge(transform.apply(eval.call(LDClient.getForMobileKey(environment), flagKey, defaultValue)))); } catch (Exception e) { - Timber.w(e); - detailResult = new EvaluationDetail(EvaluationReason.error(EvaluationReason.ErrorKind.EXCEPTION), null, defaultValue); + promise.resolve(ldValueToBridge(transform.apply(defaultValue))); } - JsonObject jsonObject = gson.toJsonTree(detailResult).getAsJsonObject(); - WritableMap detailMap = fromJsonObject(jsonObject); - promise.resolve(detailMap); } @ReactMethod - public void floatVariationDetail(String flagKey, String environment, Promise promise) { - floatVariationDetailDefaultValue(flagKey, null, environment, promise); + public void boolVariationDetail(String flagKey, boolean defaultValue, String environment, Promise promise) { + detailVariation(LDClient::boolVariationDetail, LDValue::of, flagKey, defaultValue, environment, promise); } @ReactMethod - public void floatVariationDetailDefaultValue(String flagKey, Float defaultValue, String environment, Promise promise) { - EvaluationDetail detailResult; - Double doubleValue = defaultValue.doubleValue(); - try { - detailResult = LDClient.getForMobileKey(environment).doubleVariationDetail(flagKey, doubleValue); - } catch (Exception e) { - Timber.w(e); - detailResult = new EvaluationDetail(EvaluationReason.error(EvaluationReason.ErrorKind.EXCEPTION), null, doubleValue); - } - JsonObject jsonObject = gson.toJsonTree(detailResult).getAsJsonObject(); - WritableMap detailMap = fromJsonObject(jsonObject); - promise.resolve(detailMap); + public void numberVariationDetail(String flagKey, double defaultValue, String environment, Promise promise) { + detailVariation(LDClient::doubleVariationDetail, LDValue::of, flagKey, defaultValue, environment, promise); } @ReactMethod - public void stringVariationDetail(String flagKey, String environment, Promise promise) { - stringVariationDetailDefaultValue(flagKey, null, environment, promise); - } - - @ReactMethod - public void stringVariationDetailDefaultValue(String flagKey, String defaultValue, String environment, Promise promise) { - EvaluationDetail detailResult; - try { - detailResult = LDClient.getForMobileKey(environment).stringVariationDetail(flagKey, defaultValue); - } catch (Exception e) { - Timber.w(e); - detailResult = new EvaluationDetail(EvaluationReason.error(EvaluationReason.ErrorKind.EXCEPTION), null, defaultValue); - } - JsonObject jsonObject = gson.toJsonTree(detailResult).getAsJsonObject(); - WritableMap detailMap = fromJsonObject(jsonObject); - promise.resolve(detailMap); + public void stringVariationDetail(String flagKey, String defaultValue, String environment, Promise promise) { + detailVariation(LDClient::stringVariationDetail, LDValue::of, flagKey, defaultValue, environment, promise); } @ReactMethod public void jsonVariationDetailNone(String flagKey, String environment, Promise promise) { - jsonVariationDetailBase(flagKey, null, environment, promise); + detailVariation(LDClient::jsonValueVariationDetail, id -> id, flagKey, LDValue.ofNull(), environment, promise); } @ReactMethod - public void jsonVariationDetailNumber(String flagKey, Double defaultValue, String environment, Promise promise) { - jsonVariationDetailBase(flagKey, new JsonPrimitive(defaultValue), environment, promise); + public void jsonVariationDetailNumber(String flagKey, double defaultValue, String environment, Promise promise) { + detailVariation(LDClient::jsonValueVariationDetail, id -> id, flagKey, LDValue.of(defaultValue), environment, promise); } @ReactMethod - public void jsonVariationDetailBool(String flagKey, Boolean defaultValue, String environment, Promise promise) { - jsonVariationDetailBase(flagKey, new JsonPrimitive(defaultValue), environment, promise); + public void jsonVariationDetailBool(String flagKey, boolean defaultValue, String environment, Promise promise) { + detailVariation(LDClient::jsonValueVariationDetail, id -> id, flagKey, LDValue.of(defaultValue), environment, promise); } @ReactMethod public void jsonVariationDetailString(String flagKey, String defaultValue, String environment, Promise promise) { - jsonVariationDetailBase(flagKey, new JsonPrimitive(defaultValue), environment, promise); + detailVariation(LDClient::jsonValueVariationDetail, id -> id, flagKey, LDValue.of(defaultValue), environment, promise); } @ReactMethod public void jsonVariationDetailArray(String flagKey, ReadableArray defaultValue, String environment, Promise promise) { - jsonVariationDetailBase(flagKey, toJsonArray(defaultValue), environment, promise); + detailVariation(LDClient::jsonValueVariationDetail, id -> id, flagKey, toLDValue(defaultValue), environment, promise); } @ReactMethod public void jsonVariationDetailObject(String flagKey, ReadableMap defaultValue, String environment, Promise promise) { - jsonVariationDetailBase(flagKey, toJsonObject(defaultValue), environment, promise); + detailVariation(LDClient::jsonValueVariationDetail, id -> id, flagKey, toLDValue(defaultValue), environment, promise); } - private void jsonVariationBase(String flagKey, JsonElement defaultValue, String environment, Promise promise) { - JsonElement jsonElement; - try { - jsonElement = LDClient.getForMobileKey(environment).jsonVariation(flagKey, defaultValue); - resolveJsonElement(promise, jsonElement); - } catch (Exception e) { - resolveJsonElement(promise, defaultValue); - } + interface EvalDetailCall { + EvaluationDetail call(LDClient client, String flagKey, T defaultValue); } - private void jsonVariationDetailBase(String flagKey, JsonElement defaultValue, String environment, Promise promise) { - EvaluationDetail jsonElementDetail; + private void detailVariation(EvalDetailCall eval, Function transform, + String flagKey, T defaultValue, String environment, Promise promise) { try { - jsonElementDetail = LDClient.getForMobileKey(environment).jsonVariationDetail(flagKey, defaultValue); + LDClient client = LDClient.getForMobileKey(environment); + EvaluationDetail detail = eval.call(client, flagKey, defaultValue); + ObjectBuilder resultBuilder = objectBuilderFromDetail(detail); + resultBuilder.put("value", transform.apply(detail.getValue())); + promise.resolve(ldValueToBridge(resultBuilder.build())); } catch (Exception e) { - Timber.w(e); - jsonElementDetail = new EvaluationDetail(EvaluationReason.error(EvaluationReason.ErrorKind.EXCEPTION), null, defaultValue); + ObjectBuilder resultBuilder = LDValue.buildObject(); + resultBuilder.put("kind", EvaluationReason.Kind.ERROR.name()); + resultBuilder.put("errorKind", EvaluationReason.ErrorKind.EXCEPTION.name()); + resultBuilder.put("value", transform.apply(defaultValue)); + promise.resolve(ldValueToBridge(resultBuilder.build())); } - resolveJsonElementDetail(promise, jsonElementDetail); } - private void resolveJsonElement(Promise promise, JsonElement jsonElement) { - if (jsonElement == null || jsonElement.isJsonNull()) { - promise.resolve(null); - } else if (jsonElement.isJsonArray()) { - promise.resolve(fromJsonArray(jsonElement.getAsJsonArray())); - } else if (jsonElement.isJsonObject()) { - promise.resolve(fromJsonObject(jsonElement.getAsJsonObject())); - } else { - JsonPrimitive prim = jsonElement.getAsJsonPrimitive(); - if (prim.isBoolean()) { - promise.resolve(prim.getAsBoolean()); - } else if (prim.isString()) { - promise.resolve(prim.getAsString()); - } else { - promise.resolve(prim.getAsNumber().doubleValue()); - } + private ObjectBuilder objectBuilderFromDetail(EvaluationDetail detail) { + ObjectBuilder resultMap = LDValue.buildObject(); + if (!detail.isDefaultValue()) { + resultMap.put("variationIndex", detail.getVariationIndex()); } - } - - private void resolveJsonElementDetail(Promise promise, EvaluationDetail jsonElementDetail) { - JsonObject jsonObject = new JsonObject(); - jsonObject.add("value", jsonElementDetail.getValue()); - jsonObject.addProperty("variationIndex", jsonElementDetail.getVariationIndex()); - jsonObject.add("reason", gson.toJsonTree(jsonElementDetail.getReason())); - resolveJsonElement(promise, jsonObject); + EvaluationReason reason = detail.getReason(); + ObjectBuilder reasonMap = LDValue.buildObject(); + reasonMap.put("kind", reason.getKind().name()); + switch (reason.getKind()) { + case RULE_MATCH: + reasonMap.put("ruleIndex", reason.getRuleIndex()); + if (reason.getRuleId() != null) { + reasonMap.put("ruleId", reason.getRuleId()); + } + break; + case PREREQUISITE_FAILED: reasonMap.put("prerequisiteKey", reason.getPrerequisiteKey()); break; + case ERROR: reasonMap.put("errorKind", reason.getErrorKind().name()); break; + default: break; + } + resultMap.put("reason", reasonMap.build()); + return resultMap; } @ReactMethod @@ -611,51 +463,11 @@ public void allFlags(String environment, Promise promise) { } try { - Map flags = LDClient.getForMobileKey(environment).allFlags(); - - WritableMap response = new WritableNativeMap(); - for (Map.Entry entry : flags.entrySet()) { - if (entry.getValue() == null) { - response.putNull(entry.getKey()); - } else if (entry.getValue() instanceof String) { - try { - JsonElement parsedJson = new JsonParser().parse((String) entry.getValue()); - if (parsedJson.isJsonObject()) { - response.putMap(entry.getKey(), fromJsonObject((JsonObject) parsedJson.getAsJsonObject())); - } else if (parsedJson.isJsonArray()) { - response.putArray(entry.getKey(), fromJsonArray((JsonArray) parsedJson.getAsJsonArray())); - } else { - response.putString(entry.getKey(),(String) entry.getValue()); - } - } catch (JsonParseException e) { - response.putString(entry.getKey(),(String) entry.getValue()); - } - } else if (entry.getValue() instanceof Boolean) { - response.putBoolean(entry.getKey(), (Boolean) entry.getValue()); - } else if (entry.getValue() instanceof Double) { - response.putDouble(entry.getKey(), (Double) entry.getValue()); - } else if (entry.getValue() instanceof Float) { - response.putDouble(entry.getKey(), (Float) entry.getValue()); - } else if (entry.getValue() instanceof Integer) { - response.putInt(entry.getKey(), (Integer) entry.getValue()); - } else if (entry.getValue() instanceof JsonNull) { - response.putNull(entry.getKey()); - } else if (entry.getValue() instanceof JsonArray) { - response.putArray(entry.getKey(), fromJsonArray((JsonArray) entry.getValue())); - } else if (entry.getValue() instanceof JsonObject) { - response.putMap(entry.getKey(), fromJsonObject((JsonObject) entry.getValue())); - } else if (entry.getValue() instanceof JsonPrimitive) { - JsonPrimitive primitive = (JsonPrimitive) entry.getValue(); - if (primitive.isString()) { - response.putString(entry.getKey(), primitive.getAsString()); - } else if (primitive.isBoolean()) { - response.putBoolean(entry.getKey(), primitive.getAsBoolean()); - } else if (primitive.isNumber()) { - response.putDouble(entry.getKey(), primitive.getAsDouble()); - } - } + ObjectBuilder resultBuilder = LDValue.buildObject(); + for (Map.Entry entry : LDClient.getForMobileKey(environment).allFlags().entrySet()) { + resultBuilder.put(entry.getKey(), entry.getValue()); } - promise.resolve(response); + promise.resolve(ldValueToBridge(resultBuilder.build())); } catch (LaunchDarklyException e) { // Since we confirmed the SDK has been configured, this exception should only be thrown if the env doesn't exist promise.reject(ERROR_UNKNOWN, "SDK not configured with requested environment"); @@ -666,108 +478,73 @@ public void allFlags(String environment, Promise promise) { } @ReactMethod - public void trackNumber(String eventName, Double data, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName, new JsonPrimitive(data)); - } catch (Exception e) { - Timber.w(e); - } + public void trackNumber(String eventName, double data, String environment) { + trackSafe(environment, eventName, LDValue.of(data), null); } @ReactMethod - public void trackBool(String eventName, Boolean data, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName, new JsonPrimitive(data)); - } catch (Exception e) { - Timber.w(e); - } + public void trackBool(String eventName, boolean data, String environment) { + trackSafe(environment, eventName, LDValue.of(data), null); } @ReactMethod public void trackString(String eventName, String data, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName, new JsonPrimitive(data)); - } catch (Exception e) { - Timber.w(e); - } + trackSafe(environment, eventName, LDValue.of(data), null); } @ReactMethod public void trackArray(String eventName, ReadableArray data, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName, toJsonArray(data)); - } catch (Exception e) { - Timber.w(e); - } + trackSafe(environment, eventName, toLDValue(data), null); } @ReactMethod public void trackObject(String eventName, ReadableMap data, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName, toJsonObject(data)); - } catch (Exception e) { - Timber.w(e); - } + trackSafe(environment, eventName, toLDValue(data), null); } @ReactMethod public void track(String eventName, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName); - } catch (Exception e) { - Timber.w(e); - } + trackSafe(environment, eventName, LDValue.ofNull(), null); } @ReactMethod - public void trackNumberMetricValue(String eventName, Double data, Double metricValue, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName, new JsonPrimitive(data), metricValue); - } catch (Exception e) { - Timber.w(e); - } + public void trackNumberMetricValue(String eventName, double data, double metricValue, String environment) { + trackSafe(environment, eventName, LDValue.of(data), metricValue); } @ReactMethod - public void trackBoolMetricValue(String eventName, Boolean data, Double metricValue, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName, new JsonPrimitive(data), metricValue); - } catch (Exception e) { - Timber.w(e); - } + public void trackBoolMetricValue(String eventName, boolean data, double metricValue, String environment) { + trackSafe(environment, eventName, LDValue.of(data), metricValue); } @ReactMethod - public void trackStringMetricValue(String eventName, String data, Double metricValue, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName, new JsonPrimitive(data), metricValue); - } catch (Exception e) { - Timber.w(e); - } + public void trackStringMetricValue(String eventName, String data, double metricValue, String environment) { + trackSafe(environment, eventName, LDValue.of(data), metricValue); } @ReactMethod - public void trackArrayMetricValue(String eventName, ReadableArray data, Double metricValue, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName, toJsonArray(data), metricValue); - } catch (Exception e) { - Timber.w(e); - } + public void trackArrayMetricValue(String eventName, ReadableArray data, double metricValue, String environment) { + trackSafe(environment, eventName, toLDValue(data), metricValue); } @ReactMethod - public void trackObjectMetricValue(String eventName, ReadableMap data, Double metricValue, String environment) { - try { - LDClient.getForMobileKey(environment).track(eventName, toJsonObject(data), metricValue); - } catch (Exception e) { - Timber.w(e); - } + public void trackObjectMetricValue(String eventName, ReadableMap data, double metricValue, String environment) { + trackSafe(environment, eventName, toLDValue(data), metricValue); } @ReactMethod - public void trackMetricValue(String eventName, Double metricValue, String environment) { + public void trackMetricValue(String eventName, double metricValue, String environment) { + trackSafe(environment, eventName, LDValue.ofNull(), metricValue); + } + + private void trackSafe(String environment, String eventName, LDValue value, Double metricValue) { try { - LDClient.getForMobileKey(environment).track(eventName, new JsonPrimitive(""), metricValue); + LDClient instance = LDClient.getForMobileKey(environment); + if (metricValue != null) { + instance.trackMetric(eventName, value, metricValue); + } else { + instance.trackData(eventName, value); + } } catch (Exception e) { Timber.w(e); } @@ -860,6 +637,20 @@ public void run() { background.start(); } + @ReactMethod + public void alias(String environment, ReadableMap user, ReadableMap previousUser) { + LDUser.Builder userBuilder = userBuild(user); + LDUser.Builder previousUserBuilder = userBuild(previousUser); + if (userBuilder == null || previousUserBuilder == null) { + return; + } + try { + LDClient.getForMobileKey(environment).alias(userBuilder.build(), previousUserBuilder.build()); + } catch (LaunchDarklyException e) { + Timber.w("LaunchDarkly alias called with invalid environment"); + } + } + @ReactMethod public void getConnectionMode(String environment,Promise promise) { try { @@ -1014,124 +805,75 @@ public void unregisterAllFlagsListener(String listenerId, String environment) { } } - private static JsonObject toJsonObject(ReadableMap readableMap) { - if (readableMap == null) - return null; - - JsonObject jsonObject = new JsonObject(); - - ReadableMapKeySetIterator keySet = readableMap.keySetIterator(); - - while (keySet.hasNextKey()) { - String key = keySet.nextKey(); - ReadableType type = readableMap.getType(key); - - switch (type) { - case Null: - jsonObject.add(key, null); - break; - case Boolean: - jsonObject.addProperty(key, readableMap.getBoolean(key)); - break; - case Number: - jsonObject.addProperty(key, readableMap.getDouble(key)); - break; - case String: - jsonObject.addProperty(key, readableMap.getString(key)); - break; - case Map: - jsonObject.add(key, toJsonObject(readableMap.getMap(key))); - break; - case Array: - jsonObject.add(key, toJsonArray(readableMap.getArray(key))); - break; - } + private static LDValue toLDValue(Dynamic data) { + if (data == null) { + return LDValue.ofNull(); + } + switch (data.getType()) { + case Boolean: return LDValue.of(data.asBoolean()); + case Number: return LDValue.of(data.asDouble()); + case String: return LDValue.of(data.asString()); + case Array: return toLDValue(data.asArray()); + case Map: return toLDValue(data.asMap()); + default: return LDValue.ofNull(); } - - return jsonObject; } - private static JsonArray toJsonArray(ReadableArray readableArray) { - if (readableArray == null) - return null; - - JsonArray jsonArray = new JsonArray(); - + private static LDValue toLDValue(ReadableArray readableArray) { + ArrayBuilder array = LDValue.buildArray(); for (int i = 0; i < readableArray.size(); i++) { - ReadableType type = readableArray.getType(i); - - switch (type) { - case Null: - jsonArray.add((Boolean) null); - break; - case Boolean: - jsonArray.add(readableArray.getBoolean(i)); - break; - case Number: - jsonArray.add(readableArray.getDouble(i)); - break; - case String: - jsonArray.add(readableArray.getString(i)); - break; - case Map: - jsonArray.add(toJsonObject(readableArray.getMap(i))); - break; - case Array: - jsonArray.add(toJsonArray(readableArray.getArray(i))); - break; - } + array.add(toLDValue(readableArray.getDynamic(i))); } + return array.build(); + } - return jsonArray; + private static LDValue toLDValue(ReadableMap readableMap) { + ObjectBuilder object = LDValue.buildObject(); + ReadableMapKeySetIterator iter = readableMap.keySetIterator(); + while (iter.hasNextKey()) { + String key = iter.nextKey(); + object.put(key, toLDValue(readableMap.getDynamic(key))); + } + return object.build(); } - private static WritableArray fromJsonArray(JsonArray jsonArray) { - if (jsonArray == null) - return null; + private static Object ldValueToBridge(LDValue value) { + switch (value.getType()) { + case BOOLEAN: return value.booleanValue(); + case NUMBER: return value.doubleValue(); + case STRING: return value.stringValue(); + case ARRAY: return ldValueToArray(value); + case OBJECT: return ldValueToMap(value); + default: return null; + } + } + private static WritableArray ldValueToArray(LDValue value) { WritableArray result = new WritableNativeArray(); - for (JsonElement element : jsonArray) { - if (element == null || element.isJsonNull()) { - result.pushNull(); - } else if (element.isJsonObject()) { - result.pushMap(fromJsonObject(element.getAsJsonObject())); - } else if (element.isJsonArray()) { - result.pushArray(fromJsonArray(element.getAsJsonArray())); - } else if (element.isJsonPrimitive()) { - JsonPrimitive primitive = element.getAsJsonPrimitive(); - if (primitive.isBoolean()) { - result.pushBoolean(primitive.getAsBoolean()); - } else if (primitive.isString()) { - result.pushString(primitive.getAsString()); - } else if (primitive.isNumber()) { - result.pushDouble(primitive.getAsDouble()); - } + for (LDValue val : value.values()) { + switch (val.getType()) { + case NULL: result.pushNull(); break; + case BOOLEAN: result.pushBoolean(val.booleanValue()); break; + case NUMBER: result.pushDouble(val.doubleValue()); break; + case STRING: result.pushString(val.stringValue()); break; + case ARRAY: result.pushArray(ldValueToArray(val)); break; + case OBJECT: result.pushMap(ldValueToMap(val)); break; } } return result; } - private static WritableMap fromJsonObject(JsonObject jsonObject) { - if (jsonObject == null) - return null; - + private static WritableMap ldValueToMap(LDValue value) { WritableMap result = new WritableNativeMap(); - for (Map.Entry entry : jsonObject.entrySet()) { - if (entry.getValue() == null || entry.getValue().isJsonNull()) { - result.putNull(entry.getKey()); - } else if (entry.getValue().isJsonObject()) { - result.putMap(entry.getKey(), fromJsonObject(entry.getValue().getAsJsonObject())); - } else if (entry.getValue().isJsonArray()) { - result.putArray(entry.getKey(), fromJsonArray(entry.getValue().getAsJsonArray())); - } else if (entry.getValue().isJsonPrimitive()) { - JsonPrimitive primitive = entry.getValue().getAsJsonPrimitive(); - if (primitive.isBoolean()) { - result.putBoolean(entry.getKey(), primitive.getAsBoolean()); - } else if (primitive.isString()) { - result.putString(entry.getKey(), primitive.getAsString()); - } else if (primitive.isNumber()) { - result.putDouble(entry.getKey(), primitive.getAsDouble()); - } + for (String key : value.keys()) { + LDValue val = value.get(key); + switch (val.getType()) { + case NULL: result.putNull(key); break; + case BOOLEAN: result.putBoolean(key, val.booleanValue()); break; + case NUMBER: result.putDouble(key, val.doubleValue()); break; + case STRING: result.putString(key, val.stringValue()); break; + case ARRAY: result.putArray(key, ldValueToArray(val)); break; + case OBJECT: result.putMap(key, ldValueToMap(val)); break; } } return result; @@ -1152,16 +894,6 @@ public Uri getFromMap(ReadableMap map, String key) { return android.net.Uri.parse(map.getString(key)); } }, - UriMobile(ReadableType.String) { - public Uri getFromMap(ReadableMap map, String key) { - return android.net.Uri.parse(map.getString(key) + "/mobile"); - } - }, - Country(ReadableType.String) { - public LDCountryCode getFromMap(ReadableMap map, String key) { - return LDCountryCode.valueOf(map.getString(key)); - } - }, Integer(ReadableType.Number) { public Integer getFromMap(ReadableMap map, String key) { return map.getInt(key); @@ -1177,16 +909,16 @@ public Map getFromMap(ReadableMap map, String key) { return map.getMap(key).toHashMap(); } }, - StringSet(ReadableType.Array) { - public Set getFromMap(ReadableMap map, String key) { + UserAttributes(ReadableType.Array) { + public UserAttribute[] getFromMap(ReadableMap map, String key) { ReadableArray array = map.getArray(key); - Set returnSet = new HashSet<>(); + Set userAttributes = new HashSet<>(); for (int i = 0; i < array.size(); i++) { if (array.getType(i).equals(ReadableType.String)) { - returnSet.add(array.getString(i)); + userAttributes.add(UserAttribute.forName(array.getString(i))); } } - return returnSet; + return userAttributes.toArray(new UserAttribute[0]); } }; diff --git a/index.d.ts b/index.d.ts index 860e678..b578f04 100644 --- a/index.d.ts +++ b/index.d.ts @@ -454,7 +454,7 @@ declare module 'launchdarkly-react-native-client-sdk' { boolVariation(flagKey: string, defaultValue: boolean, environment?: string): Promise; /** - * Determines the variation of an integer feature flag for the current user. + * Determines the variation of a numeric feature flag for the current user. * * @param flagKey * The unique key of the feature flag. @@ -465,21 +465,7 @@ declare module 'launchdarkly-react-native-client-sdk' { * @returns * A promise containing the flag's value. */ - intVariation(flagKey: string, defaultValue: number, environment?: string): Promise; - - /** - * Determines the variation of a floating-point feature flag for the current user. - * - * @param flagKey - * 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 environment - * Optional environment name to obtain the result from the corresponding secondary environment - * @returns - * A promise containing the flag's value. - */ - floatVariation(flagKey: string, defaultValue: number, environment?: string): Promise; + numberVariation(flagKey: string, defaultValue: number, environment?: string): Promise; /** * Determines the variation of a string feature flag for the current user. @@ -538,31 +524,7 @@ declare module 'launchdarkly-react-native-client-sdk' { ): Promise>; /** - * Determines the variation of an integer feature flag for a user, along with information about how it was - * calculated. - * - * Note that this will only work if you have set `evaluationReasons` to true in [[LDConfig]]. - * Otherwise, the `reason` property of the result will be null. - * - * For more information, see the [SDK reference guide](https://docs.launchdarkly.com/sdk/concepts/evaluation-reasons). - * - * @param flagKey - * 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 environment - * Optional environment name to obtain the result from the corresponding secondary environment - * @returns - * A promise containing an [[LDEvaluationDetail]] object containing the value and explanation. - */ - intVariationDetail( - flagKey: string, - defaultValue: number, - environment?: string - ): Promise>; - - /** - * Determines the variation of a floating-point feature flag for a user, along with information about how it was + * Determines the variation of a numeric feature flag for a user, along with information about how it was * calculated. * * Note that this will only work if you have set `evaluationReasons` to true in [[LDConfig]]. @@ -579,7 +541,7 @@ declare module 'launchdarkly-react-native-client-sdk' { * @returns * A promise containing an [[LDEvaluationDetail]] object containing the value and explanation. */ - floatVariationDetail( + numberVariationDetail( flagKey: string, defaultValue: number, environment?: string @@ -640,7 +602,6 @@ declare module 'launchdarkly-react-native-client-sdk' { * Optional environment name to obtain the result from the corresponding secondary environment * @returns * A promise containing an object in which each key is a feature flag key and each value is the flag value. - * The promise will be rejected if the SDK has not yet completed initialization. * Note that there is no way to specify a default value for each flag as there is with the * `*Variation` methods, so any flag that cannot be evaluated will have a null value. */ @@ -733,6 +694,23 @@ declare module 'launchdarkly-react-native-client-sdk' { * A promise indicating when this operation is complete (meaning that flags are ready for evaluation). */ identify(user: LDUser): Promise; + + /** + * Alias associates two users for analytics purposes by generating an alias event. + * + * This can be helpful in the situation where a person is represented by multiple + * LaunchDarkly users. This may happen, for example, when a person initially logs into + * an application-- the person might be represented by an anonymous user prior to logging + * in and a different user after logging in, as denoted by a different user key. + * + * @param user + * The new user context + * @param previousUser + * The original user context + * @param environment + * Optional string to execute the function in a different environment than the default. + */ + alias(user: LDUser, previousUser: LDUser, environment?: string): void; /** * Registers a callback to be called when the flag with key `flagKey` changes from its current value. diff --git a/index.js b/index.js index 54d7d6f..97a9237 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -import { NativeModules, NativeEventEmitter, Platform } from 'react-native'; +import { NativeModules, NativeEventEmitter } from 'react-native'; import { version } from './package.json'; let LaunchdarklyReactNativeClient = NativeModules.LaunchdarklyReactNativeClient; @@ -34,48 +34,46 @@ export default class LDClient { }, config); if (timeout == undefined) { - return LaunchdarklyReactNativeClient.configure(configWithOverriddenDefaults, this._addUserOverrides(user)); + return LaunchdarklyReactNativeClient.configure(configWithOverriddenDefaults, user); } else { - return LaunchdarklyReactNativeClient.configureWithTimeout(configWithOverriddenDefaults, this._addUserOverrides(user), timeout); + return LaunchdarklyReactNativeClient.configureWithTimeout(configWithOverriddenDefaults, user, timeout); } } ); } - boolVariation(flagKey, defaultValue, environment) { - const env = environment !== undefined ? environment : "default"; - if (defaultValue == undefined) { - return LaunchdarklyReactNativeClient.boolVariation(flagKey, env); - } else { - return LaunchdarklyReactNativeClient.boolVariationDefaultValue(flagKey, defaultValue, env); + _validateDefault(defaultType, defaultValue, validator) { + if (typeof defaultValue !== defaultType || + (typeof validator === 'function' && !validator(defaultValue))) { + return Promise.reject(new Error('Missing or invalid defaultValue for variation call')); } + return Promise.resolve(); } - intVariation(flagKey, defaultValue, environment) { - const env = environment !== undefined ? environment : "default"; - if (defaultValue == undefined) { - return LaunchdarklyReactNativeClient.intVariation(flagKey, env); - } else { - return LaunchdarklyReactNativeClient.intVariationDefaultValue(flagKey, defaultValue, env); + _normalizeEnv(environment) { + if (typeof environment !== 'string') { + return 'default'; } + return environment; } - floatVariation(flagKey, defaultValue, environment) { - const env = environment !== undefined ? environment : "default"; - if (defaultValue == undefined) { - return LaunchdarklyReactNativeClient.floatVariation(flagKey, env); - } else { - return LaunchdarklyReactNativeClient.floatVariationDefaultValue(flagKey, defaultValue, env); - } + boolVariation(flagKey, defaultValue, environment) { + return this._validateDefault('boolean', defaultValue) + .then(() => LaunchdarklyReactNativeClient.boolVariation(flagKey, defaultValue, this._normalizeEnv(environment))); + } + + numberVariation(flagKey, defaultValue, environment) { + return this._validateDefault('number', defaultValue, val => !isNaN(val)) + .then(() => LaunchdarklyReactNativeClient.numberVariation(flagKey, defaultValue, this._normalizeEnv(environment))); } stringVariation(flagKey, defaultValue, environment) { - const env = environment !== undefined ? environment : "default"; - if (defaultValue == undefined) { - return LaunchdarklyReactNativeClient.stringVariation(flagKey, env); - } else { - return LaunchdarklyReactNativeClient.stringVariationDefaultValue(flagKey, defaultValue, env); + if (defaultValue != null && typeof defaultValue !== 'string') { + return Promise.reject(new Error('Missing or invalid defaultValue for variation call')); + } else if (defaultValue === undefined) { + defaultValue = null; } + return LaunchdarklyReactNativeClient.stringVariation(flagKey, defaultValue, this._normalizeEnv(environment)); } jsonVariation(flagKey, defaultValue, environment) { @@ -97,39 +95,22 @@ export default class LDClient { } boolVariationDetail(flagKey, defaultValue, environment) { - const env = environment !== undefined ? environment : "default"; - if (defaultValue == undefined) { - return LaunchdarklyReactNativeClient.boolVariationDetail(flagKey, env); - } else { - return LaunchdarklyReactNativeClient.boolVariationDetailDefaultValue(flagKey, defaultValue, env); - } + return this._validateDefault('boolean', defaultValue) + .then(() => LaunchdarklyReactNativeClient.boolVariationDetail(flagKey, defaultValue, this._normalizeEnv(environment))); } - intVariationDetail(flagKey, defaultValue, environment) { - const env = environment !== undefined ? environment : "default"; - if (defaultValue == undefined) { - return LaunchdarklyReactNativeClient.intVariationDetail(flagKey, env); - } else { - return LaunchdarklyReactNativeClient.intVariationDetailDefaultValue(flagKey, defaultValue, env); - } - } - - floatVariationDetail(flagKey, defaultValue, environment) { - const env = environment !== undefined ? environment : "default"; - if (defaultValue == undefined) { - return LaunchdarklyReactNativeClient.floatVariationDetail(flagKey, env); - } else { - return LaunchdarklyReactNativeClient.floatVariationDetailDefaultValue(flagKey, defaultValue, env); - } + numberVariationDetail(flagKey, defaultValue, environment) { + return this._validateDefault('number', defaultValue, val => !isNaN(val)) + .then(() => LaunchdarklyReactNativeClient.numberVariationDetail(flagKey, defaultValue, this._normalizeEnv(environment))); } stringVariationDetail(flagKey, defaultValue, environment) { - const env = environment !== undefined ? environment : "default"; - if (defaultValue == undefined) { - return LaunchdarklyReactNativeClient.stringVariationDetail(flagKey, env); - } else { - return LaunchdarklyReactNativeClient.stringVariationDetailDefaultValue(flagKey, defaultValue, env); + if (defaultValue != null && typeof defaultValue !== 'string') { + return Promise.reject(new Error('Missing or invalid defaultValue for variation call')); + } else if (defaultValue === undefined) { + defaultValue = null; } + return LaunchdarklyReactNativeClient.stringVariationDetail(flagKey, defaultValue, this._normalizeEnv(environment)); } jsonVariationDetail(flagKey, defaultValue, environment) { @@ -157,7 +138,7 @@ export default class LDClient { track(eventName, data, metricValue, environment) { const env = environment !== undefined ? environment : "default"; - if (metricValue) { + if (typeof metricValue === 'number') { if (data === null || typeof data === 'undefined') { LaunchdarklyReactNativeClient.trackMetricValue(eventName, metricValue, env); } else if (typeof data === 'number') { @@ -216,13 +197,12 @@ export default class LDClient { } identify(user) { - return LaunchdarklyReactNativeClient.identify(this._addUserOverrides(user)); + return LaunchdarklyReactNativeClient.identify(user); } - _addUserOverrides(user) { - return Object.assign({ - anonymous: false // the iOS SDK defaults this to true - }, user); + alias(user, previousUser, environment) { + const env = environment !== undefined ? environment : "default"; + LaunchdarklyReactNativeClient.alias(env, user, previousUser); } _flagUpdateListener(changedFlag) { diff --git a/index.test.js b/index.test.js new file mode 100644 index 0000000..20318e8 --- /dev/null +++ b/index.test.js @@ -0,0 +1,438 @@ +import { NativeModules, NativeEventEmitter } from 'react-native'; +import LDClient from './index.js'; + +const defValErr = new Error('Missing or invalid defaultValue for variation call'); + +var client; +var addListenerMock; +let nativeMock = NativeModules.LaunchdarklyReactNativeClient; + +function getClientFlagListener() { + return addListenerMock.calls[0][1]; +} + +function getClientFlagsListener() { + return addListenerMock.calls[1][1]; +} + +function getClientConnectionListener() { + return addListenerMock.calls[2][1]; +} + +beforeEach(() => { + Object.values(nativeMock).forEach(v => { + if (typeof v === 'function') { + v.mockClear(); + } + }); + NativeEventEmitter.mockClear(); + + client = new LDClient(); + expect(NativeEventEmitter).toHaveBeenCalledTimes(1); + expect(NativeEventEmitter.mock.calls[0].length).toBe(1); + expect(Object.is(NativeEventEmitter.mock.calls[0][0], NativeModules.LaunchdarklyReactNativeClient)).toBe(true); + + addListenerMock = NativeEventEmitter.mock.results[0].value.addListener.mock; +}); + +test('constructor', () => { + expect(addListenerMock.calls.length).toBe(3); + expect(addListenerMock.calls[0].length).toBe(2); + expect(addListenerMock.calls[1].length).toBe(2); + expect(addListenerMock.calls[2].length).toBe(2); + expect(addListenerMock.calls[0][0]).toBe(nativeMock.FLAG_PREFIX); + expect(addListenerMock.calls[1][0]).toBe(nativeMock.ALL_FLAGS_PREFIX); + expect(addListenerMock.calls[2][0]).toBe(nativeMock.CONNECTION_MODE_PREFIX); +}); + +describe('boolVariation', () => { + test('validates defaultValue', async () => { + expect.assertions(11); + + await expect(client.boolVariation('flagKey')).rejects.toEqual(defValErr); + await expect(client.boolVariation('flagKey', null)).rejects.toEqual(defValErr); + await expect(client.boolVariation('flagKey', 5)).rejects.toEqual(defValErr); + await expect(client.boolVariationDetail('flagKey')).rejects.toEqual(defValErr); + await expect(client.boolVariationDetail('flagKey', null)).rejects.toEqual(defValErr); + await expect(client.boolVariationDetail('flagKey', 5)).rejects.toEqual(defValErr); + + expect(nativeMock.boolVariation).toHaveBeenCalledTimes(0); + expect(nativeMock.boolVariationDetail).toHaveBeenCalledTimes(0); + }); + + test('calls native', async () => { + nativeMock.boolVariation.mockImplementation((k, def, env) => Promise.resolve(!def)); + + await expect(client.boolVariation('key1', true)).resolves.toEqual(false); + await expect(client.boolVariation('key2', false, 'alt')).resolves.toEqual(true); + await expect(client.boolVariation('key3', false, 5)).resolves.toEqual(true); + + expect(nativeMock.boolVariation).toHaveBeenCalledTimes(3); + expect(nativeMock.boolVariation).toHaveBeenNthCalledWith(1, 'key1', true, 'default'); + expect(nativeMock.boolVariation).toHaveBeenNthCalledWith(2, 'key2', false, 'alt'); + expect(nativeMock.boolVariation).toHaveBeenNthCalledWith(3, 'key3', false, 'default'); + }); + + test('detailed calls native', async () => { + nativeMock.boolVariationDetail.mockImplementation((k, def, env) => Promise.resolve(!def)); + + await expect(client.boolVariationDetail('key1', true)).resolves.toEqual(false); + await expect(client.boolVariationDetail('key2', false, 'alt')).resolves.toEqual(true); + await expect(client.boolVariationDetail('key3', false, 5)).resolves.toEqual(true); + + expect(nativeMock.boolVariationDetail).toHaveBeenCalledTimes(3); + expect(nativeMock.boolVariationDetail).toHaveBeenNthCalledWith(1, 'key1', true, 'default'); + expect(nativeMock.boolVariationDetail).toHaveBeenNthCalledWith(2, 'key2', false, 'alt'); + expect(nativeMock.boolVariationDetail).toHaveBeenNthCalledWith(3, 'key3', false, 'default'); + }); +}); + +describe('numberVariation', () => { + test('validates defaultValue', async () => { + expect.assertions(13); + + await expect(client.numberVariation('flagKey')).rejects.toEqual(defValErr); + await expect(client.numberVariation('flagKey', null)).rejects.toEqual(defValErr); + await expect(client.numberVariation('flagKey', false)).rejects.toEqual(defValErr); + await expect(client.numberVariation('flagKey', NaN)).rejects.toEqual(defValErr); + await expect(client.numberVariationDetail('flagKey')).rejects.toEqual(defValErr); + await expect(client.numberVariationDetail('flagKey', null)).rejects.toEqual(defValErr); + await expect(client.numberVariationDetail('flagKey', false)).rejects.toEqual(defValErr); + await expect(client.numberVariationDetail('flagKey', NaN)).rejects.toEqual(defValErr); + + expect(nativeMock.numberVariation).toHaveBeenCalledTimes(0); + expect(nativeMock.numberVariationDetail).toHaveBeenCalledTimes(0); + }); + + test('calls native', async () => { + nativeMock.numberVariation.mockImplementation((k, def, env) => Promise.resolve(def + 1.5)); + + await expect(client.numberVariation('key1', 0)).resolves.toEqual(1.5); + await expect(client.numberVariation('key2', 5, 'alt')).resolves.toEqual(6.5); + await expect(client.numberVariation('key3', 2.5, 5)).resolves.toEqual(4); + + expect(nativeMock.numberVariation).toHaveBeenCalledTimes(3); + expect(nativeMock.numberVariation).toHaveBeenNthCalledWith(1, 'key1', 0, 'default'); + expect(nativeMock.numberVariation).toHaveBeenNthCalledWith(2, 'key2', 5, 'alt'); + expect(nativeMock.numberVariation).toHaveBeenNthCalledWith(3, 'key3', 2.5, 'default'); + }); + + test('detailed calls native', async () => { + nativeMock.numberVariationDetail.mockImplementation((k, def, env) => Promise.resolve(def + 1.5)); + + await expect(client.numberVariationDetail('key1', 0)).resolves.toEqual(1.5); + await expect(client.numberVariationDetail('key2', 5, 'alt')).resolves.toEqual(6.5); + await expect(client.numberVariationDetail('key3', 2.5, 5)).resolves.toEqual(4); + + expect(nativeMock.numberVariationDetail).toHaveBeenCalledTimes(3); + expect(nativeMock.numberVariationDetail).toHaveBeenNthCalledWith(1, 'key1', 0, 'default'); + expect(nativeMock.numberVariationDetail).toHaveBeenNthCalledWith(2, 'key2', 5, 'alt'); + expect(nativeMock.numberVariationDetail).toHaveBeenNthCalledWith(3, 'key3', 2.5, 'default'); + }); +}); + +describe('stringVariation', () => { + test('validates defaultValue', async () => { + expect.assertions(7); + + await expect(client.stringVariation('flagKey', false)).rejects.toEqual(defValErr); + await expect(client.stringVariationDetail('flagKey', false)).rejects.toEqual(defValErr); + + expect(nativeMock.stringVariation).toHaveBeenCalledTimes(0); + expect(nativeMock.stringVariationDetail).toHaveBeenCalledTimes(0); + }); + + test('calls native', async () => { + nativeMock.stringVariation.mockImplementation((k, def, env) => Promise.resolve('foo'.concat(def))); + + await expect(client.stringVariation('key1', '1')).resolves.toEqual('foo1'); + await expect(client.stringVariation('key2', null, 'alt')).resolves.toEqual('foonull'); + await expect(client.stringVariation('key3', undefined, 5)).resolves.toEqual('foonull'); + + expect(nativeMock.stringVariation).toHaveBeenCalledTimes(3); + expect(nativeMock.stringVariation).toHaveBeenNthCalledWith(1, 'key1', '1', 'default'); + expect(nativeMock.stringVariation).toHaveBeenNthCalledWith(2, 'key2', null, 'alt'); + expect(nativeMock.stringVariation).toHaveBeenNthCalledWith(3, 'key3', null, 'default'); + }); + + test('detailed calls native', async () => { + nativeMock.stringVariationDetail.mockImplementation((k, def, env) => Promise.resolve('foo'.concat(def))); + + await expect(client.stringVariationDetail('key1', '1')).resolves.toEqual('foo1'); + await expect(client.stringVariationDetail('key2', null, 'alt')).resolves.toEqual('foonull'); + await expect(client.stringVariationDetail('key3', undefined, 5)).resolves.toEqual('foonull'); + + expect(nativeMock.stringVariationDetail).toHaveBeenCalledTimes(3); + expect(nativeMock.stringVariationDetail).toHaveBeenNthCalledWith(1, 'key1', '1', 'default'); + expect(nativeMock.stringVariationDetail).toHaveBeenNthCalledWith(2, 'key2', null, 'alt'); + expect(nativeMock.stringVariationDetail).toHaveBeenNthCalledWith(3, 'key3', null, 'default'); + }); +}); + +test('allFlags', () => { + nativeMock.allFlags.mockReturnValueOnce('pass1'); + expect(client.allFlags()).toBe('pass1'); + expect(nativeMock.allFlags).toHaveBeenCalledTimes(1); + expect(nativeMock.allFlags).toHaveBeenNthCalledWith(1, 'default'); + + nativeMock.allFlags.mockReturnValueOnce('pass2'); + expect(client.allFlags('alt')).toBe('pass2'); + expect(nativeMock.allFlags).toHaveBeenCalledTimes(2); + expect(nativeMock.allFlags).toHaveBeenNthCalledWith(2, 'alt'); +}); + +test('setOffline', () => { + nativeMock.setOffline.mockReturnValue('passthrough'); + expect(client.setOffline()).toBe('passthrough'); + expect(nativeMock.setOffline).toHaveBeenCalledTimes(1); +}); + +test('isOffline', () => { + nativeMock.isOffline.mockReturnValue(true); + expect(client.isOffline()).toBe(true); + expect(nativeMock.isOffline).toHaveBeenCalledTimes(1); +}); + +test('setOnline', () => { + nativeMock.setOnline.mockReturnValue('passthrough'); + expect(client.setOnline()).toBe('passthrough'); + expect(nativeMock.setOnline).toHaveBeenCalledTimes(1); +}); + +test('isInitialized', () => { + nativeMock.isInitialized.mockReturnValueOnce(false); + expect(client.isInitialized()).toBe(false); + nativeMock.isInitialized.mockReturnValueOnce(true); + expect(client.isInitialized('alt')).toBe(true); + + expect(nativeMock.isInitialized).toHaveBeenCalledTimes(2); + expect(nativeMock.isInitialized).toHaveBeenNthCalledWith(1, 'default'); + expect(nativeMock.isInitialized).toHaveBeenNthCalledWith(2, 'alt'); +}); + +test('flush', () => { + client.flush(); + expect(nativeMock.flush).toHaveBeenCalledTimes(1); + expect(nativeMock.flush).toHaveBeenNthCalledWith(1); +}); + +test('close', () => { + client.close(); + expect(nativeMock.close).toHaveBeenCalledTimes(1); + expect(nativeMock.close).toHaveBeenNthCalledWith(1); +}); + +test('identify', () => { + nativeMock.identify.mockReturnValueOnce('pass1'); + expect(client.identify({ name: 'John Smith' })).toBe('pass1'); + expect(nativeMock.identify).toHaveBeenCalledTimes(1); + expect(nativeMock.identify).toHaveBeenNthCalledWith(1, { name: 'John Smith' }); +}); + +test('alias', () => { + client.alias({ key: 'anon', anonymous: true }, { key: 'abc' }); + expect(nativeMock.alias).toHaveBeenCalledTimes(1); + expect(nativeMock.alias) + .toHaveBeenNthCalledWith(1, 'default', + { key: 'anon', anonymous: true }, + { key: 'abc' }); + + client.alias({ key: 'abc' }, { key: 'def' }, 'alt'); + expect(nativeMock.alias).toHaveBeenCalledTimes(2); + expect(nativeMock.alias) + .toHaveBeenNthCalledWith(2, 'alt', { key: 'abc' }, { key: 'def' }); +}); + +test('featureFlagListener', () => { + let clientListener = getClientFlagListener(); + let listener1 = jest.fn(); + let listener2 = jest.fn(); + let listener3 = jest.fn(); + client.registerFeatureFlagListener('a', listener1); + client.registerFeatureFlagListener('a', listener2, 'alt'); + + expect(listener1).toHaveBeenCalledTimes(0); + expect(listener2).toHaveBeenCalledTimes(0); + + expect(nativeMock.registerFeatureFlagListener).toHaveBeenCalledTimes(2); + expect(nativeMock.registerFeatureFlagListener).toHaveBeenNthCalledWith(1, 'a', 'default'); + expect(nativeMock.registerFeatureFlagListener).toHaveBeenNthCalledWith(2, 'a', 'alt'); + + client.registerFeatureFlagListener('a', listener3, 'default'); + // JS wrapper coalesces listeners for the same key and environment + expect(nativeMock.registerFeatureFlagListener).toHaveBeenCalledTimes(2); + expect(listener3).toHaveBeenCalledTimes(0); + + // Wrapper doesn't call listeners for differing key + clientListener({ flagKey: 'b', listenerId: 'default;b' }); + expect(listener1).toHaveBeenCalledTimes(0); + expect(listener2).toHaveBeenCalledTimes(0); + expect(listener3).toHaveBeenCalledTimes(0); + + // Wrapper calls single listener + clientListener({ flagKey: 'a', listenerId: 'alt;a' }); + expect(listener1).toHaveBeenCalledTimes(0); + expect(listener2).toHaveBeenCalledTimes(1); + expect(listener3).toHaveBeenCalledTimes(0); + expect(listener2).toHaveBeenNthCalledWith(1, 'a'); + + // Wrapper informs both coalesced listeners + clientListener({ flagKey: 'a', listenerId: 'default;a' }); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(1); + expect(listener3).toHaveBeenCalledTimes(1); + expect(listener1).toHaveBeenNthCalledWith(1, 'a'); + expect(listener3).toHaveBeenNthCalledWith(1, 'a'); + + // Remove single listener from coalesced, should not unregister from native + client.unregisterFeatureFlagListener('a', listener3); + expect(nativeMock.unregisterFeatureFlagListener).toHaveBeenCalledTimes(0); + + clientListener({ flagKey: 'a', listenerId: 'default;a' }); + expect(listener1).toHaveBeenCalledTimes(2); + expect(listener2).toHaveBeenCalledTimes(1); + expect(listener3).toHaveBeenCalledTimes(1); + expect(listener1).toHaveBeenNthCalledWith(2, 'a'); + + // Removing remaining listener should unregister on native + client.unregisterFeatureFlagListener('a', listener1, 'default'); + client.unregisterFeatureFlagListener('a', listener2, 'alt'); + expect(nativeMock.unregisterFeatureFlagListener).toHaveBeenCalledTimes(2); + expect(nativeMock.unregisterFeatureFlagListener).toHaveBeenNthCalledWith(1, 'a', 'default'); + expect(nativeMock.unregisterFeatureFlagListener).toHaveBeenNthCalledWith(2, 'a', 'alt'); + + // No longer calls listeners + clientListener({ flagKey: 'a', listenerId: 'default;a' }); + clientListener({ flagKey: 'a', listenerId: 'alt;a' }); + expect(listener1).toHaveBeenCalledTimes(2); + expect(listener2).toHaveBeenCalledTimes(1); + expect(listener3).toHaveBeenCalledTimes(1); +}); + +test('connectionModeListener', () => { + let clientListener = getClientConnectionListener(); + let listener1 = jest.fn(); + let listener2 = jest.fn(); + client.registerCurrentConnectionModeListener('a', listener1); + client.registerCurrentConnectionModeListener('b', listener2, 'alt'); + + expect(listener1).toHaveBeenCalledTimes(0); + expect(listener2).toHaveBeenCalledTimes(0); + + expect(nativeMock.registerCurrentConnectionModeListener).toHaveBeenCalledTimes(2); + expect(nativeMock.registerCurrentConnectionModeListener).toHaveBeenNthCalledWith(1, 'a', 'default'); + expect(nativeMock.registerCurrentConnectionModeListener).toHaveBeenNthCalledWith(2, 'b', 'alt'); + + clientListener({ connectionMode: ['abc'], listenerId: 'default;a' }); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener1).toHaveBeenNthCalledWith(1, ['abc']); + expect(listener2).toHaveBeenCalledTimes(0); + + clientListener({ connectionMode: ['def'], listenerId: 'alt;b' }); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenNthCalledWith(1, ['def']); + + client.unregisterCurrentConnectionModeListener('b', 'alt'); + + expect(nativeMock.unregisterCurrentConnectionModeListener).toHaveBeenCalledTimes(1); + expect(nativeMock.unregisterCurrentConnectionModeListener).toHaveBeenNthCalledWith(1, 'b', 'alt'); + + clientListener({ connectionMode: [], listenerId: 'alt;b' }); + clientListener({ connectionMode: [], listenerId: 'alt;a' }); + + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(1); + + client.unregisterCurrentConnectionModeListener('a', 'default'); + client.unregisterCurrentConnectionModeListener('b', 'alt'); + + expect(nativeMock.unregisterCurrentConnectionModeListener).toHaveBeenCalledTimes(2); + expect(nativeMock.unregisterCurrentConnectionModeListener).toHaveBeenNthCalledWith(2, 'a', 'default'); +}); + +test('allFlagsListener', () => { + let clientListener = getClientFlagsListener(); + let listener1 = jest.fn(); + let listener2 = jest.fn(); + client.registerAllFlagsListener('a', listener1); + client.registerAllFlagsListener('b', listener2, 'alt'); + + expect(listener1).toHaveBeenCalledTimes(0); + expect(listener2).toHaveBeenCalledTimes(0); + + expect(nativeMock.registerAllFlagsListener).toHaveBeenCalledTimes(2); + expect(nativeMock.registerAllFlagsListener).toHaveBeenNthCalledWith(1, 'a', 'default'); + expect(nativeMock.registerAllFlagsListener).toHaveBeenNthCalledWith(2, 'b', 'alt'); + + clientListener({ flagKeys: ['abc'], listenerId: 'default;a' }); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener1).toHaveBeenNthCalledWith(1, ['abc']); + expect(listener2).toHaveBeenCalledTimes(0); + + clientListener({ flagKeys: ['def'], listenerId: 'alt;b' }); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenNthCalledWith(1, ['def']); + + client.unregisterAllFlagsListener('b', 'alt'); + + expect(nativeMock.unregisterAllFlagsListener).toHaveBeenCalledTimes(1); + expect(nativeMock.unregisterAllFlagsListener).toHaveBeenNthCalledWith(1, 'b', 'alt'); + + clientListener({ flagKeys: [], listenerId: 'alt;b' }); + clientListener({ flagKeys: [], listenerId: 'alt;a' }); + + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(1); + + client.unregisterAllFlagsListener('a', 'default'); + client.unregisterAllFlagsListener('b', 'alt'); + + expect(nativeMock.unregisterAllFlagsListener).toHaveBeenCalledTimes(2); + expect(nativeMock.unregisterAllFlagsListener).toHaveBeenNthCalledWith(2, 'a', 'default'); +}); + +test('getConnectionMode', () => { + nativeMock.getConnectionMode.mockReturnValue('passthrough'); + + expect(client.getConnectionMode()).toBe('passthrough'); + expect(client.getConnectionMode('alt')).toBe('passthrough'); + + expect(nativeMock.getConnectionMode).toHaveBeenCalledTimes(2); + expect(nativeMock.getConnectionMode).toHaveBeenNthCalledWith(1, 'default'); + expect(nativeMock.getConnectionMode).toHaveBeenNthCalledWith(2, 'alt'); +}); + +test('getLastSuccessfulConnection', () => { + nativeMock.getLastSuccessfulConnection.mockReturnValue('passthrough'); + + expect(client.getLastSuccessfulConnection()).toBe('passthrough'); + expect(client.getLastSuccessfulConnection('alt')).toBe('passthrough'); + + expect(nativeMock.getLastSuccessfulConnection).toHaveBeenCalledTimes(2); + expect(nativeMock.getLastSuccessfulConnection).toHaveBeenNthCalledWith(1, 'default'); + expect(nativeMock.getLastSuccessfulConnection).toHaveBeenNthCalledWith(2, 'alt'); +}); + +test('getLastFailedConnection', () => { + nativeMock.getLastFailedConnection.mockReturnValue('passthrough'); + + expect(client.getLastFailedConnection()).toBe('passthrough'); + expect(client.getLastFailedConnection('alt')).toBe('passthrough'); + + expect(nativeMock.getLastFailedConnection).toHaveBeenCalledTimes(2); + expect(nativeMock.getLastFailedConnection).toHaveBeenNthCalledWith(1, 'default'); + expect(nativeMock.getLastFailedConnection).toHaveBeenNthCalledWith(2, 'alt'); +}); + +test('getLastFailure', () => { + nativeMock.getLastFailure.mockReturnValue('passthrough'); + + expect(client.getLastFailure()).toBe('passthrough'); + expect(client.getLastFailure('alt')).toBe('passthrough'); + + expect(nativeMock.getLastFailure).toHaveBeenCalledTimes(2); + expect(nativeMock.getLastFailure).toHaveBeenNthCalledWith(1, 'default'); + expect(nativeMock.getLastFailure).toHaveBeenNthCalledWith(2, 'alt'); +}); diff --git a/ios/LaunchdarklyReactNativeClient.podspec b/ios/LaunchdarklyReactNativeClient.podspec index 67246ae..453f4fb 100644 --- a/ios/LaunchdarklyReactNativeClient.podspec +++ b/ios/LaunchdarklyReactNativeClient.podspec @@ -16,6 +16,6 @@ Pod::Spec.new do |s| s.swift_version = "5.0" s.dependency "React" - s.dependency "LaunchDarkly", "5.4.1" + s.dependency "LaunchDarkly", "5.4.3" end diff --git a/ios/LaunchdarklyReactNativeClient.swift b/ios/LaunchdarklyReactNativeClient.swift index da7d816..b498551 100644 --- a/ios/LaunchdarklyReactNativeClient.swift +++ b/ios/LaunchdarklyReactNativeClient.swift @@ -1,4 +1,3 @@ - import Foundation import LaunchDarkly @@ -54,188 +53,80 @@ class LaunchdarklyReactNativeClient: RCTEventEmitter { } } } - - private func configBuild(config: NSDictionary) -> LDConfig? { - let mobileKey = config["mobileKey"] - - if mobileKey == nil || !(mobileKey is String) { - return nil - } - - let safeKey = mobileKey as! String - var ldConfig = LDConfig(mobileKey: safeKey) - - if config["pollUri"] != nil { - ldConfig.baseUrl = URL.init(string: config["pollUri"] as! String)! - } - - if config["eventsUri"] != nil { - ldConfig.eventsUrl = URL.init(string: config["eventsUri"] as! String)! - } - - if config["streamUri"] != nil { - ldConfig.streamUrl = URL.init(string: config["streamUri"] as! String)! - } - - if config["eventsCapacity"] != nil { - ldConfig.eventCapacity = config["eventsCapacity"] as! Int - } - - if config["eventsFlushIntervalMillis"] != nil { - ldConfig.eventFlushInterval = TimeInterval(config["eventsFlushIntervalMillis"] as! Float / 1000) - } - - if config["connectionTimeoutMillis"] != nil { - ldConfig.connectionTimeout = TimeInterval(config["connectionTimeoutMillis"] as! Float / 1000) - } - - if config["pollingIntervalMillis"] != nil { - ldConfig.flagPollingInterval = TimeInterval(config["pollingIntervalMillis"] as! Float / 1000) - } - - if config["backgroundPollingIntervalMillis"] != nil { - ldConfig.backgroundFlagPollingInterval = TimeInterval(config["backgroundPollingIntervalMillis"] as! Float / 1000) - } - - if config["useReport"] != nil { - ldConfig.useReport = config["useReport"] as! Bool - } - - if config["stream"] != nil { - ldConfig.streamingMode = (config["stream"] as! Bool) ? LDStreamingMode.streaming : LDStreamingMode.polling - } - - if config["disableBackgroundUpdating"] != nil { - ldConfig.enableBackgroundUpdates = !(config["disableBackgroundUpdating"] as! Bool) - } - - if config["offline"] != nil { - ldConfig.startOnline = !(config["offline"] as! Bool) - } - - if config["debugMode"] != nil { - ldConfig.isDebugMode = config["debugMode"] as! Bool - } - - if config["evaluationReasons"] != nil { - ldConfig.evaluationReasons = config["evaluationReasons"] as! Bool - } - - ldConfig.wrapperName = config["wrapperName"] as? String - ldConfig.wrapperVersion = config["wrapperVersion"] as? String - - if config["maxCachedUsers"] != nil { - ldConfig.maxCachedUsers = config["maxCachedUsers"] as! Int - } - - if config["diagnosticOptOut"] != nil { - ldConfig.diagnosticOptOut = config["diagnosticOptOut"] as! Bool - } - if config["diagnosticRecordingIntervalMillis"] != nil { - ldConfig.diagnosticRecordingInterval = TimeInterval(config["diagnosticRecordingIntervalMillis"] as! Float / 1000) + private func id(_ x: T) -> T { x } + private func millis(_ x: NSNumber) -> TimeInterval { TimeInterval(x.doubleValue / 1_000) } + private func url(_ x: String) -> URL { URL.init(string: x)! } + private func configField(_ field: inout T, _ value: Any?, _ transform: ((V) -> T?)) { + if let val = value as? V, let res = transform(val) { + field = res } - - if config["secondaryMobileKeys"] != nil { - try! ldConfig.setSecondaryMobileKeys(config["secondaryMobileKeys"] as! [String: String]) + } + + private func configBuild(config: NSDictionary) -> LDConfig? { + guard let mobileKey = config["mobileKey"] as? String + else { return nil } + + var ldConfig = LDConfig(mobileKey: mobileKey) + configField(&ldConfig.baseUrl, config["pollUri"], url) + configField(&ldConfig.eventsUrl, config["eventsUri"], url) + configField(&ldConfig.streamUrl, config["streamUri"], url) + configField(&ldConfig.eventCapacity, config["eventsCapacity"], { (x: NSNumber) in x.intValue }) + configField(&ldConfig.eventFlushInterval, config["eventsFlushIntervalMillis"], millis) + configField(&ldConfig.connectionTimeout, config["connectionTimeoutMillis"], millis) + configField(&ldConfig.flagPollingInterval, config["pollingIntervalMillis"], millis) + configField(&ldConfig.backgroundFlagPollingInterval, config["backgroundPollingIntervalMillis"], millis) + configField(&ldConfig.useReport, config["useReport"], id) + configField(&ldConfig.streamingMode, config["stream"], { $0 ? .streaming : .polling }) + configField(&ldConfig.enableBackgroundUpdates, config["disableBackgroundUpdating"], { !$0 }) + configField(&ldConfig.startOnline, config["offline"], { !$0 }) + configField(&ldConfig.isDebugMode, config["debugMode"], id) + configField(&ldConfig.evaluationReasons, config["evaluationReasons"], id) + configField(&ldConfig.wrapperName, config["wrapperName"], id) + configField(&ldConfig.wrapperVersion, config["wrapperVersion"], id) + configField(&ldConfig.maxCachedUsers, config["maxCachedUsers"], { (x: NSNumber) in x.intValue }) + configField(&ldConfig.diagnosticOptOut, config["diagnosticOptOut"], id) + configField(&ldConfig.diagnosticRecordingInterval, config["diagnosticRecordingIntervalMillis"], millis) + configField(&ldConfig.allUserAttributesPrivate, config["allUserAttributesPrivate"], id) + configField(&ldConfig.autoAliasingOptOut, config["autoAliasingOptOut"], id) + + if let val = config["secondaryMobileKeys"] as? [String: String] { + try! ldConfig.setSecondaryMobileKeys(val) } - if config["allUserAttributesPrivate"] != nil { - ldConfig.allUserAttributesPrivate = config["allUserAttributesPrivate"] as! Bool - } - - ldConfig.autoAliasingOptOut = true - return ldConfig } private func userBuild(userDict: NSDictionary) -> LDUser? { - var user = LDUser() - user.key = userDict["key"] as! String - - if userDict["secondary"] != nil { - user.secondary = userDict["secondary"] as? String - } - - if userDict["name"] != nil { - user.name = userDict["name"] as? String - } - - if userDict["firstName"] != nil { - user.firstName = userDict["firstName"] as? String - } - - if userDict["lastName"] != nil { - user.lastName = userDict["lastName"] as? String - } - - if userDict["email"] != nil { - user.email = userDict["email"] as? String - } - - if userDict["anonymous"] != nil { - user.isAnonymous = userDict["anonymous"] as! Bool - } - - if userDict["country"] != nil { - user.country = userDict["country"] as? String - } - - if userDict["ip"] != nil { - user.ipAddress = userDict["ip"] as? String - } - - if userDict["avatar"] != nil { - user.avatar = userDict["avatar"] as? String - } - - if userDict["privateAttributeNames"] != nil { - user.privateAttributes = userDict["privateAttributeNames"] as? [String] - } - - if let customAttributes = userDict["custom"] as! [String: Any]? { - user.custom = customAttributes - } - + guard let userKey = userDict["key"] as? String + else { return nil } + + var user = LDUser(key: userKey, isAnonymous: userDict["anonymous"] as? Bool) + user.secondary = userDict["secondary"] as? String + user.name = userDict["name"] as? String + user.firstName = userDict["firstName"] as? String + user.lastName = userDict["lastName"] as? String + user.email = userDict["email"] as? String + user.country = userDict["country"] as? String + user.ipAddress = userDict["ip"] as? String + user.avatar = userDict["avatar"] as? String + user.privateAttributes = userDict["privateAttributeNames"] as? [String] + user.custom = userDict["custom"] as? [String: Any] return user } - @objc func boolVariationDefaultValue(_ flagKey: String, defaultValue: ObjCBool, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { + @objc func boolVariation(_ flagKey: String, defaultValue: ObjCBool, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { resolve(LDClient.get(environment: environment)!.variation(forKey: flagKey, defaultValue: defaultValue.boolValue) as Bool) } - @objc func intVariationDefaultValue(_ flagKey: String, defaultValue: Int, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - resolve(LDClient.get(environment: environment)!.variation(forKey: flagKey, defaultValue: defaultValue) as Int) - } - - @objc func floatVariationDefaultValue(_ flagKey: String, defaultValue: CGFloat, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - resolve(LDClient.get(environment: environment)!.variation(forKey: flagKey, defaultValue: Double(defaultValue)) as Double) + @objc func numberVariation(_ flagKey: String, defaultValue: Double, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { + resolve(LDClient.get(environment: environment)!.variation(forKey: flagKey, defaultValue: defaultValue) as Double) } - @objc func stringVariationDefaultValue(_ flagKey: String, defaultValue: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { + @objc func stringVariation(_ flagKey: String, defaultValue: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { resolve(LDClient.get(environment: environment)!.variation(forKey: flagKey, defaultValue: defaultValue) as String) } - @objc func boolVariation(_ flagKey: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - let boolFlagValue: Bool? = LDClient.get(environment: environment)!.variation(forKey: flagKey) - resolve(boolFlagValue) - } - - @objc func intVariation(_ flagKey: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - let intFlagValue: Int? = LDClient.get(environment: environment)!.variation(forKey: flagKey) - resolve(intFlagValue) - } - - @objc func floatVariation(_ flagKey: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - let floatFlagValue: Double? = LDClient.get(environment: environment)!.variation(forKey: flagKey) - resolve(floatFlagValue) - } - - @objc func stringVariation(_ flagKey: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - let stringFlagValue: String? = LDClient.get(environment: environment)!.variation(forKey: flagKey) - resolve(stringFlagValue) - } - @objc func jsonVariationNone(_ flagKey: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { let jsonFlagValue: Dictionary? = LDClient.get(environment: environment)!.variation(forKey: flagKey) resolve(jsonFlagValue) @@ -261,7 +152,7 @@ class LaunchdarklyReactNativeClient: RCTEventEmitter { resolve(LDClient.get(environment: environment)!.variation(forKey: flagKey, defaultValue: defaultValue.swiftDictionary) as NSDictionary) } - @objc func boolVariationDetailDefaultValue(_ flagKey: String, defaultValue: ObjCBool, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { + @objc func boolVariationDetail(_ flagKey: String, defaultValue: ObjCBool, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { let detail = LDClient.get(environment: environment)!.variationDetail(forKey: flagKey, defaultValue: defaultValue.boolValue) let jsonObject: NSDictionary = [ "value": (detail.value as Any), @@ -271,7 +162,7 @@ class LaunchdarklyReactNativeClient: RCTEventEmitter { resolve(jsonObject) } - @objc func intVariationDetailDefaultValue(_ flagKey: String, defaultValue: Int, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { + @objc func numberVariationDetail(_ flagKey: String, defaultValue: Double, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { let detail = LDClient.get(environment: environment)!.variationDetail(forKey: flagKey, defaultValue: defaultValue) let jsonObject: NSDictionary = [ "value": (detail.value as Any), @@ -281,17 +172,7 @@ class LaunchdarklyReactNativeClient: RCTEventEmitter { resolve(jsonObject) } - @objc func floatVariationDetailDefaultValue(_ flagKey: String, defaultValue: CGFloat, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - let detail = LDClient.get(environment: environment)!.variationDetail(forKey: flagKey, defaultValue: Double(defaultValue)) - let jsonObject: NSDictionary = [ - "value": (detail.value as Any), - "variationIndex": (detail.variationIndex as Any), - "reason": (detail.reason as Any) - ] - resolve(jsonObject) - } - - @objc func stringVariationDetailDefaultValue(_ flagKey: String, defaultValue: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { + @objc func stringVariationDetail(_ flagKey: String, defaultValue: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { let detail = LDClient.get(environment: environment)!.variationDetail(forKey: flagKey, defaultValue: defaultValue) let jsonObject: NSDictionary = [ "value": (detail.value as Any), @@ -301,46 +182,6 @@ class LaunchdarklyReactNativeClient: RCTEventEmitter { resolve(jsonObject) } - @objc func boolVariationDetail(_ flagKey: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - let detail: LDEvaluationDetail = LDClient.get(environment: environment)!.variationDetail(forKey: flagKey) - let jsonObject: NSDictionary = [ - "value": (detail.value as Any), - "variationIndex": (detail.variationIndex as Any), - "reason": (detail.reason as Any) - ] - resolve(jsonObject) - } - - @objc func intVariationDetail(_ flagKey: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - let detail: LDEvaluationDetail = LDClient.get(environment: environment)!.variationDetail(forKey: flagKey) - let jsonObject: NSDictionary = [ - "value": (detail.value as Any), - "variationIndex": (detail.variationIndex as Any), - "reason": (detail.reason as Any) - ] - resolve(jsonObject) - } - - @objc func floatVariationDetail(_ flagKey: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - let detail: LDEvaluationDetail = LDClient.get(environment: environment)!.variationDetail(forKey: flagKey) - let jsonObject: NSDictionary = [ - "value": (detail.value as Any), - "variationIndex": (detail.variationIndex as Any), - "reason": (detail.reason as Any) - ] - resolve(jsonObject) - } - - @objc func stringVariationDetail(_ flagKey: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { - let detail: LDEvaluationDetail = LDClient.get(environment: environment)!.variationDetail(forKey: flagKey) - let jsonObject: NSDictionary = [ - "value": (detail.value as Any), - "variationIndex": (detail.variationIndex as Any), - "reason": (detail.reason as Any) - ] - resolve(jsonObject) - } - @objc func jsonVariationDetailNone(_ flagKey: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { let detail: LDEvaluationDetail?> = LDClient.get(environment: environment)!.variationDetail(forKey: flagKey) let jsonObject: NSDictionary = [ @@ -484,6 +325,17 @@ class LaunchdarklyReactNativeClient: RCTEventEmitter { reject(ERROR_IDENTIFY, "User could not be built using supplied configuration", nil) } } + + @objc func alias(_ environment: String, user: NSDictionary, previousUser: NSDictionary) -> Void { + let builtUser = userBuild(userDict: user) + let builtPreviousUser = userBuild(userDict: previousUser) + guard let user = builtUser, + let previousUser = builtPreviousUser + else { + return + } + LDClient.get(environment: environment)!.alias(context: user, previousContext: previousUser) + } @objc func allFlags(_ environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { var allFlagsDict: [String: Any] = [:] diff --git a/ios/LaunchdarklyReactNativeClientBridge.m b/ios/LaunchdarklyReactNativeClientBridge.m index 835e4b2..42ed580 100644 --- a/ios/LaunchdarklyReactNativeClientBridge.m +++ b/ios/LaunchdarklyReactNativeClientBridge.m @@ -7,21 +7,11 @@ @interface RCT_EXTERN_MODULE(LaunchdarklyReactNativeClient, RCTEventEmitter) RCT_EXTERN_METHOD(configureWithTimeout:(NSDictionary *)config user:(NSDictionary *)user timeout:(NSInteger *)timeout resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(boolVariationDefaultValue:(NSString *)flagKey defaultValue:(BOOL *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(boolVariation:(NSString *)flagKey defaultValue:(BOOL *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(intVariationDefaultValue:(NSString *)flagKey defaultValue:(NSInteger *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(numberVariation:(NSString *)flagKey defaultValue:(NSNumber *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(floatVariationDefaultValue:(NSString *)flagKey defaultValue:(CGFloat *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(stringVariationDefaultValue:(NSString *)flagKey defaultValue:(NSString *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(boolVariation:(NSString *)flagKey environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(intVariation:(NSString *)flagKey environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(floatVariation:(NSString *)flagKey environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(stringVariation:(NSString *)flagKey environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(stringVariation:(NSString *)flagKey defaultValue:(NSString *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(jsonVariationNone:(NSString *)flagKey environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) @@ -35,21 +25,11 @@ @interface RCT_EXTERN_MODULE(LaunchdarklyReactNativeClient, RCTEventEmitter) RCT_EXTERN_METHOD(jsonVariationObject:(NSString *)flagKey defaultValue:(NSDictionary *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(boolVariationDetailDefaultValue:(NSString *)flagKey defaultValue:(BOOL *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(intVariationDetailDefaultValue:(NSString *)flagKey defaultValue:(NSInteger *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(boolVariationDetail:(NSString *)flagKey defaultValue:(BOOL *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(floatVariationDetailDefaultValue:(NSString *)flagKey defaultValue:(CGFloat *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(numberVariationDetail:(NSString *)flagKey defaultValue:(NSNumber *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(stringVariationDetailDefaultValue:(NSString *)flagKey defaultValue:(NSString *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(boolVariationDetail:(NSString *)flagKey environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(intVariationDetail:(NSString *)flagKey environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(floatVariationDetail:(NSString *)flagKey environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(stringVariationDetail:(NSString *)flagKey environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(stringVariationDetail:(NSString *)flagKey defaultValue:(NSString *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(jsonVariationDetailNone:(NSString *)flagKey environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) @@ -99,6 +79,8 @@ @interface RCT_EXTERN_MODULE(LaunchdarklyReactNativeClient, RCTEventEmitter) RCT_EXTERN_METHOD(identify:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(alias:(NSString *)environment user:(NSDictionary *)user previousUser:(NSDictionary *)previousUser) + RCT_EXTERN_METHOD(allFlags:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(registerFeatureFlagListener:(NSString *)flagKey environment:(NSString *)environment) diff --git a/package.json b/package.json index 50b3f74..8ddfafe 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "types": "index.d.ts", "scripts": { "check-typescript": "node_modules/typescript/bin/tsc", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jest", + "test:junit": "jest --testResultsProcessor jest-junit" }, "repository": { "type": "git", @@ -25,7 +26,23 @@ "react-native": "~0.64" }, "devDependencies": { - "metro-react-native-babel-preset": "0.59.0", + "jest": "^26.6.3", + "jest-junit": "^11.1.0", "typescript": "^3.8.3" + }, + "jest": { + "preset": "react-native", + "setupFiles": [ + "./__mocks__/native.js" + ], + "transform": { + "^.+\\.js$": "/node_modules/react-native/jest/preprocessor.js" + }, + "testPathIgnorePatterns": [ + "/node_modules/" + ], + "transformIgnorePatterns": [ + "node_modules/(?!(jest-)?react-native|@react-native-community|@react-native)" + ] } } diff --git a/test-types.ts b/test-types.ts index 94b3b69..373c661 100644 --- a/test-types.ts +++ b/test-types.ts @@ -70,27 +70,23 @@ async function tests() { const identify: null = await client.identify(user); const boolFlagValue: boolean = await client.boolVariation('key', false); - const intFlagValue: number = await client.intVariation('key', 2); - const floatFlagValue: number = await client.floatVariation('key', 2.3); + const floatFlagValue: number = await client.numberVariation('key', 2.3); const stringFlagValue: string = await client.stringVariation('key', 'default'); const jsonFlagValue: Record = await client.jsonVariation('key', jsonObj); const boolDetail: LDEvaluationDetail = await client.boolVariationDetail('key', false); - const intDetail: LDEvaluationDetail = await client.intVariationDetail('key', 2); - const floatDetail: LDEvaluationDetail = await client.floatVariationDetail('key', 2.3); + const floatDetail: LDEvaluationDetail = await client.numberVariationDetail('key', 2.3); const stringDetail: LDEvaluationDetail = await client.stringVariationDetail('key', 'default'); const jsonDetail: LDEvaluationDetail> = await client.jsonVariationDetail('key', jsonObj); const boolDetailMulti: LDEvaluationDetail = await client.boolVariationDetail('key', false, 'test'); - const intDetailMulti: LDEvaluationDetail = await client.intVariationDetail('key', 2, 'test'); - const floatDetailMulti: LDEvaluationDetail = await client.floatVariationDetail('key', 2.3, 'test'); + const floatDetailMulti: LDEvaluationDetail = await client.numberVariationDetail('key', 2.3, 'test'); const stringDetailMulti: LDEvaluationDetail = await client.stringVariationDetail('key', 'default', 'test'); const jsonDetailMulti: LDEvaluationDetail> = await client.jsonVariationDetail('key', jsonObj, 'test'); const detailIndex: number | undefined = boolDetail.variationIndex; const detailReason: LDEvaluationReason = boolDetail.reason; const detailBoolValue: boolean = boolDetail.value; - const detailIntValue: number = intDetail.value; const detailFloatValue: number = floatDetail.value; const detailStringValue: string = stringDetail.value; const detailJsonValue: Record = jsonDetail.value; @@ -132,4 +128,4 @@ async function tests() { const version: String = client.getVersion(); }; -tests(); \ No newline at end of file +tests();