├── .gitattributes
├── ManualTestApp
├── .watchmanconfig
├── jest.config.js
├── .bundle
│ └── config
├── app.json
├── types
│ └── env.d.ts
├── .eslintrc.js
├── android
│ ├── app
│ │ ├── src
│ │ │ ├── main
│ │ │ │ ├── res
│ │ │ │ │ ├── values
│ │ │ │ │ │ ├── strings.xml
│ │ │ │ │ │ └── styles.xml
│ │ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ │ └── drawable
│ │ │ │ │ │ └── rn_edit_text_material.xml
│ │ │ │ ├── java
│ │ │ │ │ └── com
│ │ │ │ │ │ └── manualtestapp
│ │ │ │ │ │ ├── MainActivity.kt
│ │ │ │ │ │ └── MainApplication.kt
│ │ │ │ └── AndroidManifest.xml
│ │ │ └── debug
│ │ │ │ └── AndroidManifest.xml
│ │ ├── debug.keystore
│ │ ├── proguard-rules.pro
│ │ └── build.gradle
│ ├── gradle
│ │ └── wrapper
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ ├── settings.gradle
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradlew.bat
│ └── gradlew
├── ios
│ ├── ManualTestApp
│ │ ├── Images.xcassets
│ │ │ ├── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── AppDelegate.h
│ │ ├── main.m
│ │ ├── AppDelegate.mm
│ │ ├── Info.plist
│ │ └── LaunchScreen.storyboard
│ ├── ManualTestApp.xcworkspace
│ │ └── contents.xcworkspacedata
│ ├── .xcode.env
│ ├── ManualTestAppTests
│ │ ├── Info.plist
│ │ └── ManualTestAppTests.m
│ ├── Podfile
│ └── ManualTestApp.xcodeproj
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── ManualTestApp.xcscheme
├── tsconfig.json
├── .prettierrc.js
├── Gemfile
├── index.js
├── babel.config.js
├── metro.config.js
├── README.md
├── .gitignore
├── package.json
├── clean-all.sh
└── App.tsx
├── android
├── gradle.properties
├── settings.gradle
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── launchdarkly
│ │ │ └── reactnative
│ │ │ ├── LaunchdarklyReactNativeClientPackage.java
│ │ │ └── utils
│ │ │ └── LDUtil.java
│ └── test
│ │ └── java
│ │ └── LDUtilTest.java
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── consumer-proguard-rules.pro
├── build.gradle
├── gradlew.bat
└── gradlew
├── CODEOWNERS
├── babel.config.js
├── .prettierrc
├── .prettierignore
├── tsconfig.json
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.md
│ └── bug_report.md
├── workflows
│ ├── stale.yml
│ └── main.yml
└── pull_request_template.md
├── ios
├── LaunchdarklyReactNativeClient.xcworkspace
│ ├── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── contents.xcworkspacedata
├── LaunchdarklyReactNativeClient-Bridging-Header.h
├── Podfile
├── LaunchdarklyReactNativeClient.xcodeproj
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── Tests.xcscheme
│ │ └── LaunchdarklyReactNativeClient.xcscheme
├── LaunchdarklyReactNativeClientBridge.m
├── Tests
│ └── Tests.swift
└── LaunchdarklyReactNativeClient.swift
├── docs
└── typedoc.js
├── typedoc.js
├── .ldrelease
└── config.yml
├── SECURITY.md
├── modd-android.conf
├── LICENSE
├── modd-ios.conf
├── .gitignore
├── launchdarkly-react-native-client-sdk.podspec
├── modd.conf
├── src
├── contextUtils.ts
└── contextUtils.test.ts
├── link-dev.sh
├── CONTRIBUTING.md
├── clean-all.sh
├── __mocks__
└── native.js
├── package.json
├── .circleci
└── config.yml
├── README.md
├── test-types.ts
├── index.js
└── index.test.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
--------------------------------------------------------------------------------
/ManualTestApp/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | android.useAndroidX=true
2 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @yusinto @louis-launchdarkly @tanderson-ld
2 |
--------------------------------------------------------------------------------
/ManualTestApp/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'react-native',
3 | };
4 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | includeBuild('../node_modules/@react-native/gradle-plugin')
2 |
--------------------------------------------------------------------------------
/ManualTestApp/.bundle/config:
--------------------------------------------------------------------------------
1 | BUNDLE_PATH: "vendor/bundle"
2 | BUNDLE_FORCE_RUBY_PLATFORM: 1
3 |
--------------------------------------------------------------------------------
/ManualTestApp/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ManualTestApp",
3 | "displayName": "ManualTestApp"
4 | }
5 |
--------------------------------------------------------------------------------
/ManualTestApp/types/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@env' {
2 | export const MOBILE_KEY: string;
3 | }
4 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:@react-native/babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/ManualTestApp/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: '@react-native',
4 | };
5 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "printWidth": 120,
5 | "arrowParens": "always"
6 | }
7 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ManualTestApp
3 |
4 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestApp/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | CHANGELOG.md
2 | **/android/app/build
3 | **/ios/build
4 | **/ios/Pods
5 | **/Images.xcassets
6 | package-lock.json
7 | yarn.lock
8 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/debug.keystore
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestApp/AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface AppDelegate : RCTAppDelegate
5 |
6 | @end
7 |
--------------------------------------------------------------------------------
/ManualTestApp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@react-native/typescript-config/tsconfig.json",
3 | "compilerOptions": {
4 | "typeRoots": ["./types"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ManualTestApp/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/ManualTestApp/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | bracketSameLine: true,
4 | bracketSpacing: false,
5 | singleQuote: true,
6 | trailingComma: 'all',
7 | };
8 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/launchdarkly/react-native-client-sdk/HEAD/ManualTestApp/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/react-native/tsconfig.json",
3 | "compilerOptions": {
4 | "lib": ["es6"]
5 | },
6 | "include": ["src/**/*"],
7 | "files": ["index.d.ts", "test-types.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/ManualTestApp/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
4 | ruby ">= 2.6.10"
5 |
6 | gem 'cocoapods', '~> 1.13'
7 | gem 'activesupport', '>= 6.1.7.3', '< 7.1.0'
8 |
--------------------------------------------------------------------------------
/ManualTestApp/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import {AppRegistry} from 'react-native';
6 | import App from './App';
7 | import {name as appName} from './app.json';
8 |
9 | AppRegistry.registerComponent(appName, () => App);
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Support request
4 | url: https://support.launchdarkly.com/hc/en-us/requests/new
5 | about: File your support requests with LaunchDarkly's support team
6 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestApp/main.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char *argv[])
6 | {
7 | @autoreleasepool {
8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/ManualTestApp/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:@react-native/babel-preset'],
3 | plugins: [
4 | [
5 | 'module:react-native-dotenv',
6 | {
7 | safe: true,
8 | allowUndefined: false,
9 | },
10 | ],
11 | ],
12 | };
13 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jan 26 10:47:00 PST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: 'Close stale issues and PRs'
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | # Happen once per day at 1:30 AM
6 | - cron: '30 1 * * *'
7 |
8 | jobs:
9 | sdk-close-stale:
10 | uses: launchdarkly/gh-actions/.github/workflows/sdk-stale.yml@main
11 |
--------------------------------------------------------------------------------
/ManualTestApp/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'ManualTestApp'
2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
3 | include ':app'
4 | includeBuild('../node_modules/@react-native/gradle-plugin')
5 |
--------------------------------------------------------------------------------
/ManualTestApp/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestApp.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/LaunchdarklyReactNativeClient.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/LaunchdarklyReactNativeClient-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #import
6 | #import
7 | #import
8 | #import
9 | #import
10 |
--------------------------------------------------------------------------------
/ios/LaunchdarklyReactNativeClient.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ManualTestApp/metro.config.js:
--------------------------------------------------------------------------------
1 | const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
2 |
3 | /**
4 | * Metro configuration
5 | * https://facebook.github.io/metro/docs/configuration
6 | *
7 | * @type {import('metro-config').MetroConfig}
8 | */
9 | const config = {};
10 |
11 | module.exports = mergeConfig(getDefaultConfig(__dirname), config);
12 |
--------------------------------------------------------------------------------
/docs/typedoc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | out: '/tmp/project-releaser/project/docs/build/html',
3 | exclude: ['**/node_modules/**', 'test-types.ts'],
4 | name: 'LaunchDarkly Client-Side SDK for React Native (7.0.0)',
5 | readme: 'none', // don't add a home page with a copy of README.md
6 | entryPoints: '/tmp/project-releaser/project/index.d.ts',
7 | entryPointStrategy: 'expand',
8 | };
9 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
--------------------------------------------------------------------------------
/ManualTestApp/README.md:
--------------------------------------------------------------------------------
1 | ## Example app
2 |
3 | To run the example app:
4 |
5 | 1. Ensure `yarn doctor` are all green.
6 | 2. Create a `.env` file at the same level as this README
7 | 3. Add your mobile key to that `.env` file:
8 |
9 | ```shell
10 | MOBILE_KEY=mob-xxx
11 | ```
12 |
13 | 4. Finally
14 |
15 | ```shell
16 | # android
17 | yarn && yarn android
18 |
19 | # ios
20 | yarn && npx pod-install && yarn ios
21 | ```
22 |
--------------------------------------------------------------------------------
/typedoc.js:
--------------------------------------------------------------------------------
1 | let version = process.env.VERSION;
2 | if (!version) {
3 | const package = require('./package.json');
4 | version = package.version;
5 | }
6 |
7 | module.exports = {
8 | out: './docs/build/html',
9 | exclude: ['**/node_modules/**', 'test-types.ts'],
10 | name: 'launchdarkly-react-native-client-sdk (' + version + ')',
11 | readme: 'none', // don't add a home page with a copy of README.md
12 | entryPoints: 'index.d.ts',
13 | };
14 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
--------------------------------------------------------------------------------
/.ldrelease/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | branches:
4 | - name: main
5 | description: 9.x
6 | - name: 8.x
7 | - name: 7.x
8 | - name: 6.x
9 |
10 | publications:
11 | - url: https://www.npmjs.com/package/launchdarkly-react-native-client-sdk
12 | description: npm
13 |
14 | jobs:
15 | - docker:
16 | image: node:16-buster
17 | template:
18 | name: npm
19 |
20 | documentation:
21 | gitHubPages: true
22 | title: LaunchDarkly Client-Side SDK for React Native
23 |
24 | sdk:
25 | displayName: React Native
26 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/.xcode.env:
--------------------------------------------------------------------------------
1 | # This `.xcode.env` file is versioned and is used to source the environment
2 | # used when running script phases inside Xcode.
3 | # To customize your local environment, you can create an `.xcode.env.local`
4 | # file that is not versioned.
5 |
6 | # NODE_BINARY variable contains the PATH to the node executable.
7 | #
8 | # Customize the NODE_BINARY variable here.
9 | # For example, to use nvm with brew, add the following line
10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use
11 | export NODE_BINARY=$(command -v node)
12 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Reporting and Fixing Security Issues
2 |
3 | Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty.
4 |
5 | Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors.
6 |
--------------------------------------------------------------------------------
/android/consumer-proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in ~/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | -keep class com.launchdarkly.sdk.android.LDConfig$Builder { public ; }
13 |
--------------------------------------------------------------------------------
/modd-android.conf:
--------------------------------------------------------------------------------
1 | android/src/main/**/* index.js src/**/* ManualTestApp/**/*.ts* ManualTestApp/.env {
2 | prep: rsync -av android ManualTestApp/node_modules/launchdarkly-react-native-client-sdk --exclude .gradle --exclude .idea --exclude gradle --exclude src/test
3 | prep: rsync -av src ManualTestApp/node_modules/launchdarkly-react-native-client-sdk
4 | prep: rsync -aq index.js ManualTestApp/node_modules/launchdarkly-react-native-client-sdk
5 | prep: rsync -aq index.d.ts ManualTestApp/node_modules/launchdarkly-react-native-client-sdk
6 | prep: yarn --cwd ManualTestApp android
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Catamorphic, Co.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/ManualTestApp/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | buildToolsVersion = "34.0.0"
4 | minSdkVersion = 21
5 | compileSdkVersion = 34
6 | targetSdkVersion = 34
7 | ndkVersion = "25.1.8937393"
8 | kotlinVersion = "1.8.0"
9 | }
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | dependencies {
15 | classpath("com.android.tools.build:gradle")
16 | classpath("com.facebook.react:react-native-gradle-plugin")
17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
18 | }
19 | }
20 |
21 | apply plugin: "com.facebook.react.rootproject"
22 |
--------------------------------------------------------------------------------
/modd-ios.conf:
--------------------------------------------------------------------------------
1 | ios/LaunchdarklyReactNativeClient.swift index.js src/**/* ManualTestApp/**/*.ts* ManualTestApp/.env {
2 | prep: rsync -av ios ManualTestApp/node_modules/launchdarkly-react-native-client-sdk --exclude LaunchdarklyReactNativeClient.xcworkspace --exclude build --exclude Pods --exclude Tests --exclude Podfile --exclude Podfile.lock
3 | prep: rsync -av src ManualTestApp/node_modules/launchdarkly-react-native-client-sdk
4 | prep: rsync -aq index.js ManualTestApp/node_modules/launchdarkly-react-native-client-sdk
5 | prep: rsync -aq index.d.ts ManualTestApp/node_modules/launchdarkly-react-native-client-sdk
6 | prep: yarn --cwd ManualTestApp ios
7 | }
8 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | require_relative '../node_modules/react-native/scripts/react_native_pods'
2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
3 |
4 | platform :ios, '13.4'
5 |
6 | target 'LaunchdarklyReactNativeClient' do
7 | config = use_native_modules!
8 |
9 | use_react_native!(
10 | :path => config[:reactNativePath],
11 | # to enable hermes on iOS, change `false` to `true` and then install pods
12 | :hermes_enabled => false
13 | )
14 |
15 | post_install do |installer|
16 | react_native_post_install(installer)
17 | end
18 |
19 | pod 'LaunchDarkly', '9.11.0'
20 |
21 | target 'Tests' do
22 | inherit! :complete
23 | end
24 | end
25 |
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # OSX
3 | #
4 | .DS_Store
5 |
6 | # node.js
7 | #
8 | node_modules/
9 | npm-debug.log
10 | yarn-error.log
11 | target/
12 |
13 | # Xcode
14 | #
15 | build/
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 | xcuserdata
25 | *.xccheckout
26 | *.moved-aside
27 | DerivedData
28 | *.hmap
29 | *.ipa
30 | *.xcuserstate
31 | project.xcworkspace
32 | Pods
33 |
34 | # Android/IntelliJ
35 | .idea
36 | .gradle
37 | local.properties
38 | *.iml
39 | org.eclipse*
40 | android/.project
41 | android/.classpath
42 |
43 | # BUCK
44 | buck-out/
45 | \.buckd/
46 | *.keystore
47 |
48 | # Auto-generated
49 | test-types.js
50 |
51 | .env
52 |
--------------------------------------------------------------------------------
/launchdarkly-react-native-client-sdk.podspec:
--------------------------------------------------------------------------------
1 | require 'json'
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4 |
5 | Pod::Spec.new do |s|
6 | s.name = package["name"]
7 | s.version = package["version"]
8 | s.summary = package["description"]
9 | s.homepage = package["homepage"]
10 | s.license = { :type => "Apache-2.0", :file => "LICENSE" }
11 | s.author = { "author" => "support@launchdarkly.com" }
12 | s.platform = :ios, "13.4"
13 | s.source = { :git => "https://github.com/launchdarkly/react-native-client-sdk.git", :tag => s.version }
14 | s.source_files = "ios/**/*.{h,m,swift}"
15 | s.swift_version = "5.0"
16 |
17 | s.dependency "React-Core"
18 | s.dependency "LaunchDarkly", "9.11.0"
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | **Requirements**
2 |
3 | - [ ] I have added test coverage for new or changed functionality
4 | - [ ] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests)
5 | - [ ] I have validated my changes against all supported platform versions
6 |
7 | **Related issues**
8 |
9 | Provide links to any issues in this repository or elsewhere relating to this pull request.
10 |
11 | **Describe the solution you've provided**
12 |
13 | Provide a clear and concise description of what you expect to happen.
14 |
15 | **Describe alternatives you've considered**
16 |
17 | Provide a clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 |
21 | Add any other context about the pull request here.
22 |
--------------------------------------------------------------------------------
/modd.conf:
--------------------------------------------------------------------------------
1 | android/src/main/**/* ios/LaunchdarklyReactNativeClient.swift index.js src/**/* ManualTestApp/**/*.ts* ManualTestApp/.env {
2 | prep: rsync -av android ManualTestApp/node_modules/launchdarkly-react-native-client-sdk --exclude .gradle --exclude build --exclude .idea --exclude gradle --exclude src/test
3 | prep: rsync -av ios ManualTestApp/node_modules/launchdarkly-react-native-client-sdk --exclude LaunchdarklyReactNativeClient.xcworkspace --exclude build --exclude Pods --exclude Tests --exclude Podfile --exclude Podfile.lock
4 | prep: rsync -av src ManualTestApp/node_modules/launchdarkly-react-native-client-sdk
5 | prep: rsync -aq index.js ManualTestApp/node_modules/launchdarkly-react-native-client-sdk
6 | prep: rsync -aq index.d.ts ManualTestApp/node_modules/launchdarkly-react-native-client-sdk
7 | prep: yarn --cwd ManualTestApp android
8 | }
9 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestAppTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/contextUtils.ts:
--------------------------------------------------------------------------------
1 | import { LDContext } from 'launchdarkly-js-sdk-common';
2 |
3 | /**
4 | * Returns true if the argument has anonymous true and has no key.
5 | *
6 | * @param LDContext
7 | * @returns {boolean}
8 | */
9 | export function isAnonymousAndNoKey(context: LDContext) {
10 | const key = context.key?.trim() ?? '';
11 | const anonymousTrue = context.anonymous === true;
12 | const isKeySpecified = key !== '';
13 |
14 | return !isKeySpecified && anonymousTrue;
15 | }
16 |
17 | /**
18 | * A basic check to validate if a context is valid. This will be expanded
19 | * to be more thorough in the future.
20 | *
21 | * @param LDContext
22 | * @returns {boolean}
23 | */
24 | export function validateContext(context: LDContext) {
25 | if (!context || Object.keys(context).length === 0) {
26 | return false;
27 | }
28 |
29 | if (!('kind' in context)) {
30 | return false;
31 | }
32 |
33 | return true;
34 | }
35 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestApp/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 |
5 | @implementation AppDelegate
6 |
7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
8 | {
9 | self.moduleName = @"ManualTestApp";
10 | // You can add your custom initial props in the dictionary below.
11 | // They will be passed down to the ViewController used by React Native.
12 | self.initialProps = @{};
13 |
14 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
15 | }
16 |
17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
18 | {
19 | return [self getBundleURL];
20 | }
21 |
22 | - (NSURL *)getBundleURL
23 | {
24 | #if DEBUG
25 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
26 | #else
27 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
28 | #endif
29 | }
30 |
31 | @end
32 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/java/com/manualtestapp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.manualtestapp
2 |
3 | import com.facebook.react.ReactActivity
4 | import com.facebook.react.ReactActivityDelegate
5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
6 | import com.facebook.react.defaults.DefaultReactActivityDelegate
7 |
8 | class MainActivity : ReactActivity() {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | override fun getMainComponentName(): String = "ManualTestApp"
15 |
16 | /**
17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
19 | */
20 | override fun createReactActivityDelegate(): ReactActivityDelegate =
21 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
22 | }
23 |
--------------------------------------------------------------------------------
/link-dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "===== Installing all dependencies..."
4 | yarn
5 |
6 | declare -a examples=(ManualTestApp)
7 |
8 | for example in "${examples[@]}"
9 | do
10 | echo "===== Linking to $example"
11 | MODULES_DIR=$example/node_modules
12 | SDK_DIR=$MODULES_DIR/launchdarkly-react-native-client-sdk
13 |
14 | mkdir -p "$MODULES_DIR"
15 | rm -rf "$SDK_DIR"
16 | mkdir -p "$SDK_DIR"/node_modules
17 |
18 | rsync -aq package.json "$SDK_DIR"
19 | rsync -aq index.js "$SDK_DIR"
20 | rsync -aq index.d.ts "$SDK_DIR"
21 | rsync -aq launchdarkly-react-native-client-sdk.podspec "$SDK_DIR"
22 | rsync -aq LICENSE "$SDK_DIR"
23 | rsync -aq node_modules "$SDK_DIR"
24 | rsync -av ios "$SDK_DIR" --exclude LaunchdarklyReactNativeClient.xcworkspace --exclude build --exclude Pods --exclude Tests --exclude Podfile --exclude Podfile.lock
25 | rsync -av android "$SDK_DIR" --exclude .gradle --exclude build --exclude .idea --exclude gradle --exclude src/test
26 | rsync -av src "$SDK_DIR"
27 | done
28 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/android/src/main/java/com/launchdarkly/reactnative/LaunchdarklyReactNativeClientPackage.java:
--------------------------------------------------------------------------------
1 | package com.launchdarkly.reactnative;
2 |
3 | import com.facebook.react.ReactPackage;
4 | import com.facebook.react.bridge.JavaScriptModule;
5 | import com.facebook.react.bridge.NativeModule;
6 | import com.facebook.react.bridge.ReactApplicationContext;
7 | import com.facebook.react.uimanager.ViewManager;
8 |
9 | import java.util.Arrays;
10 | import java.util.Collections;
11 | import java.util.List;
12 |
13 | public class LaunchdarklyReactNativeClientPackage implements ReactPackage {
14 | @Override
15 | public List createNativeModules(ReactApplicationContext reactContext) {
16 | return Arrays.asList(new LaunchdarklyReactNativeClientModule(reactContext));
17 | }
18 |
19 | // Deprecated from RN 0.47
20 | public List> createJSModules() {
21 | return Collections.emptyList();
22 | }
23 |
24 | @Override
25 | public List createViewManagers(ReactApplicationContext reactContext) {
26 | return Collections.emptyList();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestApp/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/ManualTestApp/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | ios/.xcode.env.local
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 | *.hprof
33 | .cxx/
34 | *.keystore
35 | !debug.keystore
36 |
37 | # node.js
38 | #
39 | node_modules/
40 | npm-debug.log
41 | yarn-error.log
42 | # We use yarn for the manual test app
43 | package-lock.json
44 |
45 | # BUCK
46 | buck-out/
47 | \.buckd/
48 | *.keystore
49 | !debug.keystore
50 |
51 | # fastlane
52 | #
53 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
54 | # screenshots whenever they are needed.
55 | # For more information about the recommended setup visit:
56 | # https://docs.fastlane.tools/best-practices/source-control/
57 |
58 | **/fastlane/report.xml
59 | **/fastlane/Preview.html
60 | **/fastlane/screenshots
61 | **/fastlane/test_output
62 |
63 | # Bundle artifact
64 | *.jsbundle
65 |
66 | # Ruby / CocoaPods
67 | /ios/Pods/
68 | /vendor/bundle/
69 |
70 | # Temporary files created by Metro to check the health of the file watcher
71 | .metro-health-check*
72 |
73 | # testing
74 | /coverage
75 |
76 | .env
77 |
--------------------------------------------------------------------------------
/ManualTestApp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "manualtestapp",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "cc": "rimraf node_modules/.cache/babel-loader/*,",
7 | "android": "yarn run cc && react-native run-android --terminal a",
8 | "android-release": "yarn run cc && react-native run-android --terminal --variant release",
9 | "ios": "yarn run cc && react-native run-ios --terminal a",
10 | "start": "yarn run cc && react-native start --reset-cache",
11 | "postinstall": "cd ../ && yarn link-dev",
12 | "lint": "eslint .",
13 | "doctor": "react-native doctor",
14 | "clean-all": "./clean-all.sh"
15 | },
16 | "dependencies": {
17 | "@react-native-picker/picker": "^2.2.1",
18 | "launchdarkly-react-native-client-sdk": "^9.0.0",
19 | "react": "18.2.0",
20 | "react-native": "0.73.4"
21 | },
22 | "devDependencies": {
23 | "@babel/core": "^7.20.0",
24 | "@babel/preset-env": "^7.20.0",
25 | "@babel/runtime": "^7.20.0",
26 | "@react-native/babel-preset": "^0.73.18",
27 | "@react-native/eslint-config": "^0.73.1",
28 | "@react-native/metro-config": "^0.73.2",
29 | "@react-native/typescript-config": "^0.73.1",
30 | "@types/react": "^18.2.6",
31 | "@types/react-test-renderer": "^18.0.0",
32 | "babel-jest": "^29.6.3",
33 | "eslint": "^8.19.0",
34 | "jest": "^29.6.3",
35 | "prettier": "2.8.8",
36 | "react-native-dotenv": "3.4.9",
37 | "react-test-renderer": "18.2.0",
38 | "typescript": "5.0.4"
39 | },
40 | "engines": {
41 | "node": ">=18"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 |
7 | dependencies {
8 | classpath("com.android.tools.build:gradle")
9 | classpath("com.facebook.react:react-native-gradle-plugin")
10 | }
11 | }
12 |
13 | apply plugin: 'com.android.library'
14 |
15 | android {
16 | compileSdkVersion = 33
17 | buildToolsVersion = "30.0.3"
18 |
19 | namespace "com.launchdarkly.reactnative"
20 | defaultConfig {
21 | minSdkVersion(21)
22 | targetSdkVersion(33)
23 | versionCode = 1
24 | versionName = "1.0"
25 | consumerProguardFiles("consumer-proguard-rules.pro")
26 | }
27 |
28 | lintOptions {
29 | abortOnError false
30 | }
31 |
32 | compileOptions {
33 | sourceCompatibility JavaVersion.VERSION_1_8
34 | targetCompatibility JavaVersion.VERSION_1_8
35 | }
36 | }
37 |
38 |
39 | repositories {
40 | mavenLocal()
41 | google()
42 | mavenCentral()
43 | maven {
44 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
45 | url "$rootDir/../node_modules/react-native/android"
46 | }
47 | }
48 |
49 | dependencies {
50 | // The version of react-native is set by the React Native Gradle Plugin
51 | implementation("com.facebook.react:react-android:0.72.7")
52 | implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.4.0")
53 | implementation("com.jakewharton.timber:timber:5.0.1")
54 | implementation("com.google.code.gson:gson:2.10.1")
55 |
56 | testImplementation "junit:junit:4.13.2"
57 | testImplementation 'com.google.code.gson:gson:2.10.1'
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/src/contextUtils.test.ts:
--------------------------------------------------------------------------------
1 | import { LDContext, LDMultiKindContext, LDSingleKindContext } from 'launchdarkly-js-sdk-common';
2 | import { isAnonymousAndNoKey, validateContext } from './contextUtils';
3 |
4 | describe('contextUtils', () => {
5 | describe('isAnonymousAndNoKey', () => {
6 | test('anonymous no key', () => {
7 | const c = { kind: 'car', anonymous: true };
8 | expect(isAnonymousAndNoKey(c)).toBeTruthy();
9 | });
10 |
11 | test('anonymous with key', () => {
12 | const c = { kind: 'car', key: 'blr34F', anonymous: true };
13 | expect(isAnonymousAndNoKey(c)).toBeFalsy();
14 | });
15 |
16 | test('known user', () => {
17 | const c = { kind: 'car', key: 'blr34F' };
18 | expect(isAnonymousAndNoKey(c)).toBeFalsy();
19 | });
20 | });
21 |
22 | describe('validateContext', () => {
23 | test('undefined, null, empty context', () => {
24 | expect(validateContext(undefined as any as LDContext)).toBeFalsy();
25 | expect(validateContext(null as any as LDContext)).toBeFalsy();
26 | expect(validateContext({})).toBeFalsy();
27 | });
28 |
29 | test('inject key for anonymous single context', () => {
30 | const c = { kind: 'car', key: 'blr34F', anonymous: true };
31 | const isValid = validateContext(c);
32 |
33 | expect(c.key).toEqual('blr34F');
34 | expect(isValid).toBeTruthy();
35 | });
36 |
37 | test('inject key for multi anonymous context', () => {
38 | // @ts-ignore
39 | const c: LDMultiKindContext = { kind: 'multi', car: { anonymous: true }, bus: { key: 'bbb-555' } };
40 | const isValid = validateContext(c);
41 |
42 | expect(isValid).toBeTruthy();
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | ManualTestApp
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSAllowsArbitraryLoads
30 |
31 | NSAllowsLocalNetworking
32 |
33 |
34 | NSLocationWhenInUseUsageDescription
35 |
36 | UILaunchStoryboardName
37 | LaunchScreen
38 | UIRequiredDeviceCapabilities
39 |
40 | armv7
41 |
42 | UISupportedInterfaceOrientations
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationLandscapeLeft
46 | UIInterfaceOrientationLandscapeRight
47 |
48 | UIViewControllerBasedStatusBarAppearance
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is this a support request?**
11 | This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the SDK code. If you're not sure whether the problem you are having is specifically related to the SDK, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/hc/en-us/requests/new) or by emailing support@launchdarkly.com.
12 |
13 | Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on your issues. If your problem is specific to your account, you should submit a support request as described above.
14 |
15 | **Describe the bug**
16 | A clear and concise description of what the bug is.
17 |
18 | **To reproduce**
19 | Steps to reproduce the behavior.
20 |
21 | **Expected behavior**
22 | A clear and concise description of what you expected to happen.
23 |
24 | **Logs**
25 | If applicable, add any log output related to your problem.
26 |
27 | **SDK version**
28 | The version of this SDK that you are using.
29 |
30 | **Language version, developer tools**
31 | For instance, Go 1.11 or Ruby 2.5.3. If you are using a language that requires a separate compiler, such as C, please include the name and version of the compiler too.
32 |
33 | **OS/platform**
34 | For instance, Ubuntu 16.04, Windows 10, or Android 4.0.3. If your code is running in a browser, please also include the browser type and version.
35 |
36 | **Additional context**
37 | Add any other context about the problem here.
38 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing to the LaunchDarkly Client-Side SDK for React Native
2 | ================================================
3 |
4 | LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK.
5 |
6 | Submitting bug reports and feature requests
7 | ------------------
8 |
9 | The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/react-native-client-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days.
10 |
11 | Submitting pull requests
12 | ------------------
13 |
14 | We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days.
15 |
16 | Build instructions
17 | ------------------
18 |
19 | ### Prerequisites
20 |
21 | Follow the [React Native development environment setup guide](https://reactnative.dev/docs/environment-setup) to install all required tools for contributing to the project.
22 |
23 | ### Building and testing
24 |
25 | First, install the dependencies by running:
26 | ```
27 | npm install
28 | ```
29 |
30 | To run tests of the JavaScript portion of the implementation:
31 | ```
32 | npm test
33 | ```
34 |
35 | To validate the TypeScript module definition, run:
36 | ```
37 | npm run check-typescript
38 | ```
39 |
40 | Testing the native module implementation must be done by integrating the SDK into an application, such as one created with `npx react-native init`.
41 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/java/com/manualtestapp/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package com.manualtestapp
2 |
3 | import android.app.Application
4 | import com.facebook.react.PackageList
5 | import com.facebook.react.ReactApplication
6 | import com.facebook.react.ReactHost
7 | import com.facebook.react.ReactNativeHost
8 | import com.facebook.react.ReactPackage
9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
11 | import com.facebook.react.defaults.DefaultReactNativeHost
12 | import com.facebook.react.flipper.ReactNativeFlipper
13 | import com.facebook.soloader.SoLoader
14 |
15 | class MainApplication : Application(), ReactApplication {
16 |
17 | override val reactNativeHost: ReactNativeHost =
18 | object : DefaultReactNativeHost(this) {
19 | override fun getPackages(): List {
20 | // Packages that cannot be autolinked yet can be added manually here, for example:
21 | // packages.add(new MyReactNativePackage());
22 | return PackageList(this).packages
23 | }
24 |
25 | override fun getJSMainModuleName(): String = "index"
26 |
27 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
28 |
29 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
30 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
31 | }
32 |
33 | override val reactHost: ReactHost
34 | get() = getDefaultReactHost(this.applicationContext, reactNativeHost)
35 |
36 | override fun onCreate() {
37 | super.onCreate()
38 | SoLoader.init(this, false)
39 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
40 | // If you opted-in for the New Architecture, we load the native entry point for this app.
41 | load()
42 | }
43 | ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ManualTestApp/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Use this property to specify which architecture you want to build.
28 | # You can also override it from the CLI using
29 | # ./gradlew -PreactNativeArchitectures=x86_64
30 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
31 |
32 | # Use this property to enable support to the new architecture.
33 | # This will allow you to use TurboModules and the Fabric render in
34 | # your application. You should enable this flag either if you want
35 | # to write custom TurboModules/Fabric components OR use libraries that
36 | # are providing them.
37 | newArchEnabled=false
38 |
39 | # Use this property to enable or disable the Hermes JS engine.
40 | # If set to false, you will be using JSC instead.
41 | hermesEnabled=true
42 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Resolve react_native_pods.rb with node to allow for hoisting
2 | require Pod::Executable.execute_command('node', ['-p',
3 | 'require.resolve(
4 | "react-native/scripts/react_native_pods.rb",
5 | {paths: [process.argv[1]]},
6 | )', __dir__]).strip
7 |
8 | platform :ios, min_ios_version_supported
9 | prepare_react_native_project!
10 |
11 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set.
12 | # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded
13 | #
14 | # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js`
15 | # ```js
16 | # module.exports = {
17 | # dependencies: {
18 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}),
19 | # ```
20 | flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled
21 |
22 | linkage = ENV['USE_FRAMEWORKS']
23 | if linkage != nil
24 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
25 | use_frameworks! :linkage => linkage.to_sym
26 | end
27 |
28 | target 'ManualTestApp' do
29 | config = use_native_modules!
30 |
31 | use_react_native!(
32 | :path => config[:reactNativePath],
33 | # Enables Flipper.
34 | #
35 | # Note that if you have use_frameworks! enabled, Flipper will not work and
36 | # you should disable the next line.
37 | :flipper_configuration => flipper_config,
38 | # An absolute path to your application root.
39 | :app_path => "#{Pod::Config.instance.installation_root}/.."
40 | )
41 |
42 | target 'ManualTestAppTests' do
43 | inherit! :complete
44 | # Pods for testing
45 | end
46 |
47 | post_install do |installer|
48 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
49 | react_native_post_install(
50 | installer,
51 | config[:reactNativePath],
52 | :mac_catalyst_enabled => false
53 | )
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/ios/LaunchdarklyReactNativeClient.xcodeproj/xcshareddata/xcschemes/Tests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
14 |
15 |
18 |
24 |
25 |
26 |
27 |
28 |
38 |
39 |
45 |
46 |
48 |
49 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/ManualTestApp/clean-all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Ripped from: https://gist.githubusercontent.com/townofdon/087c7c0bb773adb158f20339c7e13408/raw/53ccb3430cf870c3cdeecead82bc12c3644f2f53/react-native-nuke.sh
3 | # ReactNative script to clean all the things
4 | # usage:
5 | # - add each item below as a separate script in package.json
6 | # - add one final script:
7 | # - "clean": "yarn clean-node-modules && yarn clean-pods && yarn clean-ios && yarn clean-android && yarn clean-rn-cache"
8 | # - alternatively, copy this shell script and add the following cmd to package.json:
9 | # - "clean": "./react-native-clean-sh"
10 | # - you may need to run `sudo chmod 777 ./react-native-clean-sh before this script can run`
11 |
12 | echo " ____ "
13 | echo " __,-~~/~ \`---. "
14 | echo " _/_,---( , ) "
15 | echo " __ / < / ) \___ "
16 | echo " ====------------------===;;;== "
17 | echo " \/ ~\"~\"~\"~\"~\"~\~\"~)~\",1/ "
18 | echo " (_ ( \ ( > \) "
19 | echo " \_( _ < >_>' "
20 | echo " ~ \`-i' ::>|--\" "
21 | echo " I;|.|.| "
22 | echo " <|i::|i|> "
23 | echo " |[::|.| "
24 | echo " ||: | "
25 | echo "______________________REACT NATIVE CLEAN ALL________________ "
26 |
27 | # clean-node-modules
28 | rm -rf node_modules && yarn
29 |
30 | # clean-pods
31 | cd ios/ && pod cache clean --all && pod deintegrate && rm -rf Pods && rm -rf Podfile.lock && pod install && cd ../
32 |
33 | # clean-ios
34 | rm -rf ios/build && rm -rf ~/Library/Developer/Xcode/DerivedData && rm -rf ./ios/DerivedData
35 |
36 | # clean-android
37 | cd android && ./gradlew clean && cd ..
38 |
39 | # clean-rn-cache
40 | rm -rf $TMPDIR/react-* && rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-* && yarn cache clean && watchman watch-del-all
41 |
42 | react-native start --reset-cache
43 |
--------------------------------------------------------------------------------
/clean-all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Ripped from: https://gist.githubusercontent.com/townofdon/087c7c0bb773adb158f20339c7e13408/raw/53ccb3430cf870c3cdeecead82bc12c3644f2f53/react-native-nuke.sh
3 | # ReactNative script to clean all the things
4 | # usage:
5 | # - add each item below as a separate script in package.json
6 | # - add one final script:
7 | # - "clean": "yarn clean-node-modules && yarn clean-pods && yarn clean-ios && yarn clean-android && yarn clean-rn-cache"
8 | # - alternatively, copy this shell script and add the following cmd to package.json:
9 | # - "clean": "./react-native-clean-sh"
10 | # - you may need to run `sudo chmod 777 ./react-native-clean-sh before this script can run`
11 |
12 | echo " ____ "
13 | echo " __,-~~/~ \`---. "
14 | echo " _/_,---( , ) "
15 | echo " __ / < / ) \___ "
16 | echo " ====------------------===;;;== "
17 | echo " \/ ~\"~\"~\"~\"~\"~\~\"~)~\",1/ "
18 | echo " (_ ( \ ( > \) "
19 | echo " \_( _ < >_>' "
20 | echo " ~ \`-i' ::>|--\" "
21 | echo " I;|.|.| "
22 | echo " <|i::|i|> "
23 | echo " |[::|.| "
24 | echo " ||: | "
25 | echo "______________________REACT NATIVE CLEAN ALL________________ "
26 |
27 | # clean-node-modules
28 | rm -rf node_modules && yarn
29 |
30 | # clean-pods
31 | cd ios/ && pod cache clean --all && pod deintegrate && rm -rf Pods && rm -rf Podfile.lock && pod install && cd ../
32 |
33 | # clean-ios
34 | rm -rf ios/build && rm -rf ~/Library/Developer/Xcode/DerivedData && rm -rf ./ios/DerivedData
35 |
36 | # clean-android
37 | cd android && ./gradlew clean && cd ..
38 |
39 | # clean-rn-cache
40 | rm -rf $TMPDIR/react-* && rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-* && yarn cache clean && watchman watch-del-all
41 |
42 | cd ManualTestApp && react-native start --reset-cache && cd ..
43 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestAppTests/ManualTestAppTests.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #import
5 | #import
6 |
7 | #define TIMEOUT_SECONDS 600
8 | #define TEXT_TO_LOOK_FOR @"Welcome to React"
9 |
10 | @interface ManualTestAppTests : XCTestCase
11 |
12 | @end
13 |
14 | @implementation ManualTestAppTests
15 |
16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test
17 | {
18 | if (test(view)) {
19 | return YES;
20 | }
21 | for (UIView *subview in [view subviews]) {
22 | if ([self findSubviewInView:subview matching:test]) {
23 | return YES;
24 | }
25 | }
26 | return NO;
27 | }
28 |
29 | - (void)testRendersWelcomeScreen
30 | {
31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
33 | BOOL foundElement = NO;
34 |
35 | __block NSString *redboxError = nil;
36 | #ifdef DEBUG
37 | RCTSetLogFunction(
38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
39 | if (level >= RCTLogLevelError) {
40 | redboxError = message;
41 | }
42 | });
43 | #endif
44 |
45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
48 |
49 | foundElement = [self findSubviewInView:vc.view
50 | matching:^BOOL(UIView *view) {
51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
52 | return YES;
53 | }
54 | return NO;
55 | }];
56 | }
57 |
58 | #ifdef DEBUG
59 | RCTSetLogFunction(RCTDefaultLogFunction);
60 | #endif
61 |
62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
64 | }
65 |
66 | @end
67 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on: [push]
3 |
4 | jobs:
5 | tsc:
6 | runs-on: ubuntu-latest
7 | name: Typescript
8 | steps:
9 | - uses: actions/checkout@v3
10 | - uses: actions/setup-node@v3
11 | with:
12 | node-version: '18.x'
13 | cache: 'yarn'
14 | - run: yarn && yarn tsc
15 |
16 | js-tests:
17 | runs-on: ubuntu-latest
18 | name: JS tests
19 | steps:
20 | - uses: actions/checkout@v3
21 | - uses: actions/setup-node@v3
22 | with:
23 | node-version: '18.x'
24 | cache: 'yarn'
25 | - run: yarn && yarn test
26 |
27 | android-tests:
28 | runs-on: ubuntu-latest
29 | name: Android tests
30 | steps:
31 | - uses: actions/checkout@v3
32 | - uses: actions/setup-node@v3
33 | with:
34 | node-version: '18.x'
35 | cache: 'yarn'
36 | - uses: actions/setup-java@v4
37 | with:
38 | distribution: 'temurin' # See 'Supported distributions' for available options
39 | java-version: '17'
40 | - run: yarn && cd android && ./gradlew test
41 |
42 | # ripped from these two places:
43 | # https://vmois.dev/xcode-github-actions/
44 | # https://gist.github.com/ricardopereira/10198e68f27c14601d77ebc7a8352da1
45 | ios-tests:
46 | runs-on: macos-12
47 | strategy:
48 | matrix:
49 | destination: ['platform=iOS Simulator,name=iPhone 13,OS=16.2']
50 | name: iOS tests
51 | steps:
52 | - uses: actions/checkout@v3
53 | - uses: actions/setup-node@v3
54 | with:
55 | node-version: '18.x'
56 | cache: 'yarn'
57 | - uses: actions/cache@v3
58 | id: cocoapods-cache
59 | with:
60 | path: ios/Pods
61 | key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock', 'package-lock.json', 'yarn.lock') }}
62 | restore-keys: |
63 | ${{ runner.os }}-pods-
64 | - name: CocoaPods
65 | run: cd ios && yarn && pod install
66 | - name: Select Xcode
67 | run: sudo xcode-select -switch /Applications/Xcode_14.2.app && /usr/bin/xcodebuild -version
68 | - name: Run tests
69 | run: cd ios && xcodebuild -quiet -workspace LaunchdarklyReactNativeClient.xcworkspace -scheme LaunchdarklyReactNativeClient -sdk iphonesimulator -destination "${destination}" test | xcpretty && exit ${PIPESTATUS[0]}
70 | env:
71 | destination: ${{ matrix.destination }}
72 |
--------------------------------------------------------------------------------
/__mocks__/native.js:
--------------------------------------------------------------------------------
1 | const mockNativeModule = {
2 | FLAG_PREFIX: 'test-flag-prefix',
3 | ALL_FLAGS_PREFIX: 'test-all-flags-prefix',
4 | CONNECTION_MODE_PREFIX: 'test-connection-mode-prefix',
5 |
6 | configure: jest.fn(),
7 | configureWithTimeout: jest.fn(),
8 |
9 | boolVariation: jest.fn(),
10 | boolVariationDefaultValue: jest.fn(),
11 | numberVariation: jest.fn(),
12 | numberVariationDefaultValue: jest.fn(),
13 | stringVariation: jest.fn(),
14 | stringVariationDefaultValue: jest.fn(),
15 | jsonVariationNone: jest.fn(),
16 | jsonVariationNumber: jest.fn(),
17 | jsonVariationBool: jest.fn(),
18 | jsonVariationString: jest.fn(),
19 | jsonVariationArray: jest.fn(),
20 | jsonVariationObject: jest.fn(),
21 |
22 | boolVariationDetail: jest.fn(),
23 | boolVariationDetailDefaultValue: jest.fn(),
24 | numberVariationDetail: jest.fn(),
25 | numberVariationDetailDefaultValue: jest.fn(),
26 | stringVariationDetail: jest.fn(),
27 | stringVariationDetailDefaultValue: jest.fn(),
28 | jsonVariationDetailNone: jest.fn(),
29 | jsonVariationDetailNumber: jest.fn(),
30 | jsonVariationDetailBool: jest.fn(),
31 | jsonVariationDetailString: jest.fn(),
32 | jsonVariationDetailArray: jest.fn(),
33 | jsonVariationDetailObject: jest.fn(),
34 |
35 | allFlags: jest.fn(),
36 |
37 | trackNumber: jest.fn(),
38 | trackBool: jest.fn(),
39 | trackString: jest.fn(),
40 | trackArray: jest.fn(),
41 | trackObject: jest.fn(),
42 | track: jest.fn(),
43 |
44 | trackNumberMetricValue: jest.fn(),
45 | trackBoolMetricValue: jest.fn(),
46 | trackStringMetricValue: jest.fn(),
47 | trackArrayMetricValue: jest.fn(),
48 | trackObjectMetricValue: jest.fn(),
49 | trackMetricValue: jest.fn(),
50 |
51 | setOffline: jest.fn(),
52 | isOffline: jest.fn(),
53 | setOnline: jest.fn(),
54 | isInitialized: jest.fn(),
55 | flush: jest.fn(),
56 | close: jest.fn(),
57 | identify: jest.fn(),
58 | getConnectionMode: jest.fn(),
59 | getLastSuccessfulConnection: jest.fn(),
60 | getLastFailedConnection: jest.fn(),
61 | getLastFailure: jest.fn(),
62 |
63 | registerFeatureFlagListener: jest.fn(),
64 | unregisterFeatureFlagListener: jest.fn(),
65 | registerCurrentConnectionModeListener: jest.fn(),
66 | unregisterCurrentConnectionModeListener: jest.fn(),
67 | registerAllFlagsListener: jest.fn(),
68 | unregisterAllFlagsListener: jest.fn(),
69 | };
70 |
71 | jest.mock(
72 | 'react-native',
73 | () => {
74 | return {
75 | NativeModules: {
76 | LaunchdarklyReactNativeClient: mockNativeModule,
77 | },
78 | NativeEventEmitter: jest.fn().mockImplementation(() => {
79 | return { addListener: jest.fn() };
80 | }),
81 | };
82 | },
83 | { virtual: true },
84 | );
85 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "launchdarkly-react-native-client-sdk",
3 | "version": "9.4.2",
4 | "description": "LaunchDarkly Client-side SDK for React Native",
5 | "main": "index.js",
6 | "types": "index.d.ts",
7 | "files": [
8 | "android/src/main",
9 | "android/build.gradle",
10 | "android/consumer-proguard-rules.pro",
11 | "android/gradle.properties",
12 | "ios/LaunchdarklyReactNativeClient.xcodeproj",
13 | "ios/LaunchdarklyReactNativeClient.swift",
14 | "ios/LaunchdarklyReactNativeClient-Bridging-Header.h",
15 | "ios/LaunchdarklyReactNativeClientBridge.m",
16 | "src",
17 | "babel.config.js",
18 | "index.d.ts",
19 | "index.js",
20 | "launchdarkly-react-native-client-sdk.podspec",
21 | "test-types.ts",
22 | "tsconfig.json"
23 | ],
24 | "scripts": {
25 | "check-typescript": "node_modules/typescript/bin/tsc",
26 | "test": "jest",
27 | "test:junit": "jest --testResultsProcessor jest-junit",
28 | "link-dev": "./link-dev.sh",
29 | "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)'",
30 | "doctor": "react-native doctor",
31 | "yarn-all": "yarn && yarn --cwd ManualTestApp",
32 | "modd-ios": "modd -f modd-ios.conf",
33 | "modd-android": "modd -f modd-android.conf",
34 | "modd": "modd",
35 | "dev-ios": "yarn yarn-all && yarn modd-ios",
36 | "dev-android": "yarn yarn-all && yarn modd-android",
37 | "clean-all": "./clean-all.sh"
38 | },
39 | "repository": {
40 | "type": "git",
41 | "url": "https://github.com/launchdarkly/react-native-client-sdk.git"
42 | },
43 | "keywords": [
44 | "react-native"
45 | ],
46 | "author": "LaunchDarkly",
47 | "license": "Apache-2.0",
48 | "bugs": {
49 | "url": "https://github.com/launchdarkly/react-native-client-sdk/issues"
50 | },
51 | "homepage": "https://docs.launchdarkly.com/sdk/client-side/react-native",
52 | "dependencies": {
53 | "launchdarkly-js-sdk-common": "5.0.3"
54 | },
55 | "peerDependencies": {
56 | "react-native": ">=0.69.0 <0.74.0"
57 | },
58 | "devDependencies": {
59 | "@react-native/babel-preset": "^0.73.18",
60 | "@tsconfig/react-native": "2.0.3",
61 | "@types/jest": "29.4.0",
62 | "@types/react": "18.2.6",
63 | "@types/react-test-renderer": "18.0.0",
64 | "jest": "^29.4.2",
65 | "jest-junit": "^15.0.0",
66 | "prettier": "^2.8.8",
67 | "react-native": "^0.73.2",
68 | "typedoc-plugin-rename-defaults": "^0.6.4",
69 | "typescript": ">=4.5.0"
70 | },
71 | "jest": {
72 | "preset": "react-native",
73 | "setupFiles": [
74 | "./__mocks__/native.js"
75 | ],
76 | "modulePathIgnorePatterns": [
77 | "/example/node_modules",
78 | "/lib/"
79 | ]
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/ManualTestApp/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestApp.xcodeproj/xcshareddata/xcschemes/ManualTestApp.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/ios/LaunchdarklyReactNativeClient.xcodeproj/xcshareddata/xcschemes/LaunchdarklyReactNativeClient.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
67 |
68 |
74 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/ManualTestApp/ios/ManualTestApp/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/ios/LaunchdarklyReactNativeClientBridge.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import "React/RCTEventEmitter.h"
3 |
4 | @interface RCT_EXTERN_MODULE(LaunchdarklyReactNativeClient, RCTEventEmitter)
5 |
6 | RCT_EXTERN_METHOD(configure:(NSDictionary *)config context:(NSDictionary *)context resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
7 |
8 | RCT_EXTERN_METHOD(configureWithTimeout:(NSDictionary *)config context:(NSDictionary *)context timeout:(NSInteger *)timeout resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
9 |
10 | RCT_EXTERN_METHOD(boolVariation:(NSString *)flagKey defaultValue:(BOOL *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
11 |
12 | RCT_EXTERN_METHOD(numberVariation:(NSString *)flagKey defaultValue:(double)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
13 |
14 | RCT_EXTERN_METHOD(stringVariation:(NSString *)flagKey defaultValue:(NSString *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
15 |
16 | RCT_EXTERN_METHOD(jsonVariation:(NSString *)flagKey defaultValue:(id *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
17 |
18 | RCT_EXTERN_METHOD(boolVariationDetail:(NSString *)flagKey defaultValue:(BOOL *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
19 |
20 | RCT_EXTERN_METHOD(numberVariationDetail:(NSString *)flagKey defaultValue:(double)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
21 |
22 | RCT_EXTERN_METHOD(stringVariationDetail:(NSString *)flagKey defaultValue:(NSString *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
23 |
24 | RCT_EXTERN_METHOD(jsonVariationDetail:(NSString *)flagKey defaultValue:(id *)defaultValue environment:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
25 |
26 | RCT_EXTERN_METHOD(trackData:(NSString *)eventName data:(id *)data environment:(NSString *)environment)
27 |
28 | RCT_EXTERN_METHOD(trackMetricValue:(NSString *)eventName data:(id *)data metricValue:(NSNumber * _Nonnull)metricValue environment:(NSString *)environment)
29 |
30 | RCT_EXTERN_METHOD(setOffline:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
31 |
32 | RCT_EXTERN_METHOD(isOffline:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
33 |
34 | RCT_EXTERN_METHOD(setOnline:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
35 |
36 | RCT_EXTERN_METHOD(flush)
37 |
38 | RCT_EXTERN_METHOD(close:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
39 |
40 | RCT_EXTERN_METHOD(identify:(NSDictionary *)context resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
41 |
42 | RCT_EXTERN_METHOD(allFlags:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
43 |
44 | RCT_EXTERN_METHOD(registerFeatureFlagListener:(NSString *)flagKey environment:(NSString *)environment)
45 |
46 | RCT_EXTERN_METHOD(unregisterFeatureFlagListener:(NSString *)flagKey environment:(NSString *)environment)
47 |
48 | RCT_EXTERN_METHOD(registerCurrentConnectionModeListener:(NSString *)listenerId environment:(NSString *)environment)
49 |
50 | RCT_EXTERN_METHOD(unregisterCurrentConnectionModeListener:(NSString *)listenerId environment:(NSString *)environment)
51 |
52 | RCT_EXTERN_METHOD(registerAllFlagsListener:(NSString *)listenerId environment:(NSString *)environment)
53 |
54 | RCT_EXTERN_METHOD(unregisterAllFlagsListener:(NSString *)listenerId environment:(NSString *)environment)
55 |
56 | RCT_EXTERN_METHOD(isInitialized:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
57 |
58 | RCT_EXTERN_METHOD(getConnectionMode:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
59 |
60 | RCT_EXTERN_METHOD(getLastSuccessfulConnection:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
61 |
62 | RCT_EXTERN_METHOD(getLastFailedConnection:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
63 |
64 | RCT_EXTERN_METHOD(getLastFailure:(NSString *)environment resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
65 |
66 | @end
67 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | jobs:
4 | # This job simulates integrating the SDK into a freshly created React Native project template and
5 | # then builds Android and iOS applications using the template.
6 | build-applications-using-template:
7 | parameters:
8 | rn-version:
9 | description: The React Native project template version
10 | type: string
11 | xcode-version:
12 | description: The Xcode version to build with
13 | type: string
14 |
15 | macos:
16 | xcode: <>
17 |
18 | environment:
19 | ANDROID_SDK_ROOT: '/tmp/Android'
20 |
21 | steps:
22 | - checkout
23 |
24 | - run:
25 | name: Download Android command line tools
26 | command: |
27 | mkdir -p $ANDROID_SDK_ROOT/cmdline-tools/latest
28 | curl https://dl.google.com/android/repository/commandlinetools-mac-8092744_latest.zip -o cmdline-tools.zip
29 | unzip cmdline-tools.zip
30 | mv cmdline-tools/* $ANDROID_SDK_ROOT/cmdline-tools/latest/
31 | yes | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null || true
32 |
33 | - run:
34 | name: Setup Android debug keystore
35 | command: |
36 | keytool -genkey -v -keystore debug.keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "cn=Unknown, ou=Unknown, o=Unknown, c=Unknown"
37 | mkdir -p ~/.android
38 | cp debug.keystore ~/.android/
39 |
40 | - restore_cache:
41 | name: Restore RN project template from cache
42 | key: v1-rn-template-cache-<>
43 |
44 | - run:
45 | name: Create CI test application for RN <>
46 | command: |
47 | cd ..
48 | mkdir -p test
49 | cd test
50 | [ -d "CITest" ] || npx react-native@<> init CITest --version <> --skip-install
51 |
52 | # use macos default ruby version
53 | cd CITest
54 | rm -rf .ruby-version
55 |
56 | - save_cache:
57 | name: Save RN project template to cache
58 | key: v1-rn-template-cache-<>
59 | paths:
60 | - ../test/CITest
61 |
62 | - run:
63 | name: Add LaunchDarkly dependency
64 | command: |
65 | cd ../test/CITest && npx yarn add file:../../project
66 | cd node_modules/launchdarkly-react-native-client-sdk/ios
67 | rm -rf LaunchdarklyReactNativeClient.xcworkspace
68 | rm -rf build
69 | rm -rf Pods
70 | rm -rf Tests
71 | rm -rf Podfile
72 | rm -rf Podfile.lock
73 |
74 | - restore_cache:
75 | name: Restore gem cache
76 | key: v1-gem-cache-<>-
77 |
78 | # Newer cocoapods fixes Swift library auto-linking errors
79 | - run:
80 | name: Update CocoaPods
81 | command: |
82 | sudo gem install cocoapods
83 | sudo gem cleanup
84 | # Used as cache key to prevent storing redundant caches
85 | gem list > /tmp/cache-key.txt
86 |
87 | - save_cache:
88 | name: Save gem cache
89 | key: v1-gem-cache-<>-{{ checksum "/tmp/cache-key.txt" }}
90 | paths:
91 | - ~/.gem
92 |
93 | - run:
94 | name: Install iOS Pods
95 | command: cd ../test/CITest/ios && pod install
96 |
97 | - run:
98 | name: Build application for iOS (Release)
99 | command: |
100 | mkdir -p artifacts
101 | cd ../test/CITest/ios
102 | xcodebuild -workspace CITest.xcworkspace -scheme CITest build -configuration Release -destination "generic/platform=iOS" CODE_SIGNING_ALLOWED=NO GCC_WARN_INHIBIT_ALL_WARNINGS=YES | tee '../../../project/artifacts/xcb-<>.txt' | xcpretty
103 |
104 | - when:
105 | # only build android once
106 | condition:
107 | and:
108 | - equal: [14.3.1, << parameters.xcode-version >>]
109 | - equal: [0.73.2, << parameters.rn-version >>]
110 | steps:
111 | - run:
112 | name: Build application for Android
113 | command: cd ../test/CITest/android && ./gradlew packageRelease
114 |
115 | - store_artifacts:
116 | path: artifacts
117 |
118 | workflows:
119 | version: 2
120 | install-sdk-build-app:
121 | jobs:
122 | - build-applications-using-template:
123 | name: rn<>-xc<>-build-apps-using-template
124 | matrix:
125 | parameters:
126 | rn-version: ['0.73.2']
127 | xcode-version: ['14.3.1', '15.1']
128 |
--------------------------------------------------------------------------------
/ManualTestApp/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 | apply plugin: "org.jetbrains.kotlin.android"
3 | apply plugin: "com.facebook.react"
4 |
5 | /**
6 | * This is the configuration block to customize your React Native Android app.
7 | * By default you don't need to apply any configuration, just uncomment the lines you need.
8 | */
9 | react {
10 | /* Folders */
11 | // The root of your project, i.e. where "package.json" lives. Default is '..'
12 | // root = file("../")
13 | // The folder where the react-native NPM package is. Default is ../node_modules/react-native
14 | // reactNativeDir = file("../node_modules/react-native")
15 | // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
16 | // codegenDir = file("../node_modules/@react-native/codegen")
17 | // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
18 | // cliFile = file("../node_modules/react-native/cli.js")
19 |
20 | /* Variants */
21 | // The list of variants to that are debuggable. For those we're going to
22 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'.
23 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
24 | // debuggableVariants = ["liteDebug", "prodDebug"]
25 |
26 | /* Bundling */
27 | // A list containing the node command and its flags. Default is just 'node'.
28 | // nodeExecutableAndArgs = ["node"]
29 | //
30 | // The command to run when bundling. By default is 'bundle'
31 | // bundleCommand = "ram-bundle"
32 | //
33 | // The path to the CLI configuration file. Default is empty.
34 | // bundleConfig = file(../rn-cli.config.js)
35 | //
36 | // The name of the generated asset file containing your JS bundle
37 | // bundleAssetName = "MyApplication.android.bundle"
38 | //
39 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
40 | // entryFile = file("../js/MyApplication.android.js")
41 | //
42 | // A list of extra flags to pass to the 'bundle' commands.
43 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
44 | // extraPackagerArgs = []
45 |
46 | /* Hermes Commands */
47 | // The hermes compiler command to run. By default it is 'hermesc'
48 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
49 | //
50 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
51 | // hermesFlags = ["-O", "-output-source-map"]
52 | }
53 |
54 | /**
55 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
56 | */
57 | def enableProguardInReleaseBuilds = false
58 |
59 | /**
60 | * The preferred build flavor of JavaScriptCore (JSC)
61 | *
62 | * For example, to use the international variant, you can use:
63 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
64 | *
65 | * The international variant includes ICU i18n library and necessary data
66 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
67 | * give correct results when using with locales other than en-US. Note that
68 | * this variant is about 6MiB larger per architecture than default.
69 | */
70 | def jscFlavor = 'org.webkit:android-jsc:+'
71 |
72 | android {
73 | ndkVersion rootProject.ext.ndkVersion
74 | buildToolsVersion rootProject.ext.buildToolsVersion
75 | compileSdk rootProject.ext.compileSdkVersion
76 |
77 | namespace "com.manualtestapp"
78 | defaultConfig {
79 | applicationId "com.manualtestapp"
80 | minSdkVersion rootProject.ext.minSdkVersion
81 | targetSdkVersion rootProject.ext.targetSdkVersion
82 | versionCode 1
83 | versionName "1.0"
84 | }
85 | signingConfigs {
86 | debug {
87 | storeFile file('debug.keystore')
88 | storePassword 'android'
89 | keyAlias 'androiddebugkey'
90 | keyPassword 'android'
91 | }
92 | }
93 | buildTypes {
94 | debug {
95 | signingConfig signingConfigs.debug
96 | }
97 | release {
98 | // Caution! In production, you need to generate your own keystore file.
99 | // see https://reactnative.dev/docs/signed-apk-android.
100 | signingConfig signingConfigs.debug
101 | minifyEnabled enableProguardInReleaseBuilds
102 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
103 | }
104 | }
105 | }
106 |
107 | dependencies {
108 | // The version of react-native is set by the React Native Gradle Plugin
109 | implementation("com.facebook.react:react-android")
110 | implementation("com.facebook.react:flipper-integration")
111 |
112 | if (hermesEnabled.toBoolean()) {
113 | implementation("com.facebook.react:hermes-android")
114 | } else {
115 | implementation jscFlavor
116 | }
117 | }
118 |
119 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LaunchDarkly Client-Side SDK for React Native
2 |
3 | > [!IMPORTANT]
4 | > As mentioned in the [repository changelog](https://github.com/launchdarkly/react-native-client-sdk/blob/main/CHANGELOG.md), the `launchdarkly-react-native-client-sdk` project has been renamed to `@launchdarkly/react-native-client-sdk`. All future releases will be made from the [new repository](https://github.com/launchdarkly/js-core/tree/main/packages/sdk/react-native). Please consider upgrading and filing potential requests in that repository's [issue tracker](https://github.com/launchdarkly/js-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+sdk%2Freact-native%22+sort%3Aupdated-desc).
5 |
6 | ## v9.x readme
7 |
8 | [](https://www.npmjs.com/package/launchdarkly-react-native-client-sdk)
9 | [](https://circleci.com/gh/launchdarkly/react-native-client-sdk)
10 | [](https://launchdarkly.github.io/react-native-client-sdk)
11 |
12 | ## LaunchDarkly overview
13 |
14 | [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today!
15 |
16 | [](https://twitter.com/intent/follow?screen_name=launchdarkly)
17 |
18 | ## Supported versions
19 | [React Native SDK version 10](https://github.com/launchdarkly/js-core/tree/main/packages/sdk/react-native) is now released.
20 |
21 | The LaunchDarkly React Native SDK version 10 is written in pure JavaScript and is compatible with Expo. It supports hot reloading. For customers using React Native 0.72.x - 0.73.x, use the latest 10.x release. If you cannot upgrade to the 10.x yet, use RN 8.0.x and 9.0.x respectively.
22 |
23 | For React Native 0.71.x, use the latest 8.0.x release.
24 |
25 | For React Native 0.69.x - 0.70.x support, use the latest 7.1.x release.
26 |
27 | ## Getting started
28 |
29 | 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.
30 |
31 | ## Learn more
32 |
33 | Read 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).
34 |
35 | ## Testing
36 |
37 | We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly.
38 |
39 | ## Contributing
40 |
41 | We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK.
42 |
43 | ## About LaunchDarkly
44 |
45 | - LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:
46 | - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.
47 | - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
48 | - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
49 | - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
50 | - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
51 | - Explore LaunchDarkly
52 | - [launchdarkly.com](https://www.launchdarkly.com/ 'LaunchDarkly Main Website') for more information
53 | - [docs.launchdarkly.com](https://docs.launchdarkly.com/ 'LaunchDarkly Documentation') for our documentation and SDK reference guides
54 | - [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ 'LaunchDarkly API Documentation') for our API documentation
55 | - [blog.launchdarkly.com](https://blog.launchdarkly.com/ 'LaunchDarkly Blog Documentation') for the latest product updates
56 |
57 | ## Developing this SDK
58 |
59 | - Run `yarn doctor` in both the root and ManualTestApp directories and make sure everything is green
60 | - If watchman fails, you can try installing it manually `brew reinstall watchman`
61 | - Make sure you have [modd](https://github.com/cortesi/modd#install) installed so native code changes are hot reloaded
62 |
63 | * For ios run `yarn dev-ios`
64 | * For android run `yarn dev-android`
65 |
--------------------------------------------------------------------------------
/android/src/test/java/LDUtilTest.java:
--------------------------------------------------------------------------------
1 | import static com.launchdarkly.reactnative.utils.LDUtil.configureContext;
2 | import static com.launchdarkly.reactnative.utils.LDUtil.createSingleContext;
3 | import static com.launchdarkly.reactnative.utils.LDUtil.validateConfig;
4 | import static org.junit.Assert.assertEquals;
5 | import static org.junit.Assert.assertFalse;
6 | import static org.junit.Assert.assertTrue;
7 |
8 | import com.facebook.react.bridge.JavaOnlyArray;
9 | import com.facebook.react.bridge.JavaOnlyMap;
10 | import com.facebook.react.bridge.ReadableType;
11 | import com.launchdarkly.sdk.LDContext;
12 |
13 | import org.junit.Test;
14 |
15 | public class LDUtilTest {
16 | private JavaOnlyMap createAnonymousNoKey() {
17 | JavaOnlyMap configMap = new JavaOnlyMap();
18 | configMap.putString("kind", "employee");
19 | configMap.putBoolean("anonymous", true);
20 | configMap.putString("name", "Yus");
21 |
22 | return configMap;
23 | }
24 |
25 | private JavaOnlyMap createEmployeeContextMap() {
26 | JavaOnlyMap configMap = new JavaOnlyMap();
27 | configMap.putString("kind", "employee");
28 | configMap.putString("key", "blr123");
29 | configMap.putBoolean("anonymous", true);
30 | configMap.putString("name", "Yus");
31 | configMap.putInt("employeeNumber", 55);
32 | configMap.putBoolean("isActive", true);
33 |
34 | JavaOnlyMap addressMap = new JavaOnlyMap();
35 | addressMap.putString("street", "Sunset Blvd");
36 | addressMap.putInt("number", 321);
37 | configMap.putMap("address", addressMap);
38 |
39 | return configMap;
40 | }
41 |
42 | private JavaOnlyMap createOrgContextMap() {
43 | JavaOnlyMap configMap = new JavaOnlyMap();
44 | configMap.putString("kind", "org");
45 | configMap.putString("key", "qf32");
46 | configMap.putString("name", "Qantas");
47 | configMap.putInt("employeeCount", 10000);
48 | configMap.putBoolean("isInternational", true);
49 |
50 | JavaOnlyMap addressMap = new JavaOnlyMap();
51 | addressMap.putString("street", "Bourke St");
52 | addressMap.putInt("number", 22);
53 | addressMap.putString("country", "Australia");
54 | configMap.putMap("address", addressMap);
55 |
56 | return configMap;
57 | }
58 |
59 | private JavaOnlyMap createMultiContextMap() {
60 | JavaOnlyMap multi = new JavaOnlyMap();
61 | multi.putString("kind", "multi");
62 | multi.putMap("employee", createEmployeeContextMap());
63 | multi.putMap("org", createOrgContextMap());
64 | return multi;
65 | }
66 |
67 | @Test
68 | public void testValidateConfig() {
69 | JavaOnlyMap configMap = createEmployeeContextMap();
70 |
71 | assertFalse(validateConfig("stream", configMap, ReadableType.Boolean));
72 | assertTrue(validateConfig("kind", configMap, ReadableType.String));
73 | assertTrue(validateConfig("key", configMap, ReadableType.String));
74 | }
75 |
76 | @Test
77 | public void testCreateSingleContextSuccess() {
78 | JavaOnlyMap configMap = createEmployeeContextMap();
79 | LDContext c = createSingleContext(configMap, "employee");
80 |
81 | assertTrue(c.isValid());
82 | assertEquals("employee", c.getKind().toString());
83 | assertEquals("blr123", c.getKey());
84 | assertTrue(c.isAnonymous());
85 | assertEquals("Yus", c.getName());
86 | assertEquals(55, c.getValue("employeeNumber").intValue());
87 | assertTrue(c.getValue("isActive").booleanValue());
88 | assertEquals("{\"kind\":\"employee\",\"key\":\"blr123\",\"name\":\"Yus\",\"anonymous\":true,\"address\":{\"number\":321,\"street\":\"Sunset Blvd\"},\"isActive\":true,\"employeeNumber\":55}", c.toString());
89 | }
90 |
91 | @Test
92 | public void testCreateSingleContextNoKey() {
93 | JavaOnlyMap configMap = new JavaOnlyMap();
94 | LDContext c = createSingleContext(configMap, "employee");
95 |
96 | assertFalse(c.isValid());
97 | }
98 |
99 | @Test
100 | public void testAnonymousNoKey() {
101 | LDContext c = createSingleContext(createAnonymousNoKey(), "employee");
102 |
103 | assertTrue(c.isValid());
104 | assertTrue(c.isAnonymous());
105 | assertEquals("__LD_PLACEHOLDER_KEY__", c.getKey());
106 | }
107 |
108 | @Test
109 | public void testCreateSingleContextWithMeta() {
110 | JavaOnlyMap configMap = createEmployeeContextMap();
111 |
112 | JavaOnlyMap metaMap = new JavaOnlyMap();
113 | JavaOnlyArray arr = new JavaOnlyArray();
114 | arr.pushString("employeeNumber");
115 | arr.pushString("address");
116 | metaMap.putArray("privateAttributes", arr);
117 | configMap.putMap("_meta", metaMap);
118 |
119 | LDContext c = createSingleContext(configMap, "employee");
120 |
121 | assertTrue(c.isValid());
122 | assertEquals(2, c.getPrivateAttributeCount());
123 | assertEquals("{\"kind\":\"employee\",\"key\":\"blr123\",\"name\":\"Yus\",\"anonymous\":true,\"address\":{\"number\":321,\"street\":\"Sunset Blvd\"},\"isActive\":true,\"employeeNumber\":55,\"_meta\":{\"privateAttributes\":[\"employeeNumber\",\"address\"]}}", c.toString());
124 | }
125 |
126 | @Test
127 | public void testMultiContextSuccess() {
128 | LDContext c = configureContext(createMultiContextMap());
129 |
130 | assertTrue(c.isValid());
131 | assertEquals("{\"kind\":\"multi\",\"employee\":{\"key\":\"blr123\",\"name\":\"Yus\",\"anonymous\":true,\"address\":{\"number\":321,\"street\":\"Sunset Blvd\"},\"isActive\":true,\"employeeNumber\":55},\"org\":{\"key\":\"qf32\",\"name\":\"Qantas\",\"address\":{\"number\":22,\"country\":\"Australia\",\"street\":\"Bourke St\"},\"isInternational\":true,\"employeeCount\":10000}}", c.toString());
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/test-types.ts:
--------------------------------------------------------------------------------
1 | // This file exists only so that we can run the TypeScript compiler in the CI build
2 | // to validate our index.d.ts file. The code will not actually be run.
3 |
4 | import LDClient, {
5 | LDConnectionMode,
6 | LDConfig,
7 | LDEvaluationDetail,
8 | LDEvaluationReason,
9 | LDFailureReason,
10 | LDFlagSet,
11 | LDContext,
12 | } from 'launchdarkly-react-native-client-sdk';
13 |
14 | async function tests() {
15 | const jsonObj: Record = {
16 | a: 's',
17 | b: true,
18 | c: 3,
19 | d: ['x', 'y'],
20 | e: [true, false],
21 | f: [1, 2],
22 | };
23 |
24 | const jsonArr: any[] = ['a', 1, null, false, { a: 3 }, []];
25 |
26 | const configWithKeyOnly: LDConfig = {
27 | mobileKey: '',
28 | enableAutoEnvAttributes: true,
29 | };
30 | const configWithAllOptions: LDConfig = {
31 | application: {
32 | id: 'rn-unit-test',
33 | version: '0.0.1',
34 | name: 'RN app with LD',
35 | versionName: 'Beta release 0.0.1',
36 | },
37 | mobileKey: '',
38 | enableAutoEnvAttributes: true,
39 | pollUrl: '',
40 | streamUrl: '',
41 | eventsUrl: '',
42 | eventCapacity: 1,
43 | flushInterval: 1,
44 | connectionTimeout: 1,
45 | pollingInterval: 1,
46 | backgroundPollingInterval: 1,
47 | useReport: true,
48 | stream: true,
49 | disableBackgroundUpdating: true,
50 | offline: true,
51 | debugMode: true,
52 | evaluationReasons: true,
53 | secondaryMobileKeys: { test: 'fake_key' },
54 | maxCachedContexts: 6,
55 | diagnosticOptOut: true,
56 | diagnosticRecordingInterval: 100000,
57 | allAttributesPrivate: true,
58 | privateAttributes: ['abc', 'def'],
59 | };
60 | const userEmpty: LDContext = {};
61 | const userWithKeyOnly: LDContext = { kind: 'user', key: 'test-user-1' };
62 | const user: LDContext = {
63 | kind: 'user',
64 | key: 'test-user-2',
65 | name: 'name',
66 | firstName: 'first',
67 | lastName: 'last',
68 | email: 'test@example.com',
69 | anonymous: true,
70 | country: 'us',
71 | privateAttributeNames: ['name', 'email'],
72 | custom: jsonObj,
73 | avatar: 'avatar',
74 | ip: '192.0.2.1',
75 | };
76 | const client: LDClient = new LDClient();
77 | const timeoutClient: LDClient = new LDClient();
78 |
79 | const configure: null = await client.configure(configWithAllOptions, user);
80 | const configureWithTimeout: null = await timeoutClient.configure(configWithAllOptions, userWithKeyOnly, 10);
81 | const identify: null = await client.identify(user);
82 |
83 | const boolFlagValue: boolean = await client.boolVariation('key', false);
84 | const floatFlagValue: number = await client.numberVariation('key', 2.3);
85 | const stringFlagValue: string = await client.stringVariation('key', 'default');
86 | const jsonObjFlagValue: any = await client.jsonVariation('key', jsonObj);
87 | const jsonArrFlagValue: any = await client.jsonVariation('key', jsonArr);
88 | const jsonSimpleFlagValue: any = await client.jsonVariation('key', 3);
89 |
90 | const boolDetail: LDEvaluationDetail = await client.boolVariationDetail('key', false);
91 | const floatDetail: LDEvaluationDetail = await client.numberVariationDetail('key', 2.3);
92 | const stringDetail: LDEvaluationDetail = await client.stringVariationDetail('key', 'default');
93 | const jsonDetail: LDEvaluationDetail = await client.jsonVariationDetail('key', jsonObj);
94 |
95 | const boolDetailMulti: LDEvaluationDetail = await client.boolVariationDetail('key', false, 'test');
96 | const floatDetailMulti: LDEvaluationDetail = await client.numberVariationDetail('key', 2.3, 'test');
97 | const stringDetailMulti: LDEvaluationDetail = await client.stringVariationDetail('key', 'default', 'test');
98 | const jsonDetailMulti: LDEvaluationDetail = await client.jsonVariationDetail('key', jsonObj, 'test');
99 |
100 | const detailIndex: number | undefined = boolDetail.variationIndex;
101 | const detailReason: LDEvaluationReason = boolDetail.reason;
102 | const detailBoolValue: boolean = boolDetail.value;
103 | const detailFloatValue: number = floatDetail.value;
104 | const detailStringValue: string = stringDetail.value;
105 | const detailJsonValue: Record = jsonDetail.value;
106 |
107 | const flagSet: LDFlagSet = await client.allFlags();
108 | const flagSetValue: any = flagSet['key'];
109 |
110 | const track1: void = await client.track('eventname');
111 | const track2: void = await client.track('eventname', undefined);
112 | const track3: void = await client.track('eventname', true);
113 | const track4: void = await client.track('eventname', 2);
114 | const track5: void = await client.track('eventname', 2.3);
115 | const track6: void = await client.track('eventname', 'something');
116 | const track7: void = await client.track('eventname', [2, 3]);
117 | const track8: void = await client.track('eventname', { foo: 2 });
118 | const track9: void = await client.track('eventname', { foo: 2 }, 4);
119 |
120 | const setOffline: boolean = await client.setOffline();
121 | const setOnline: boolean = await client.setOnline();
122 | const isOffline: boolean = await client.isOffline();
123 | const isInitialized: boolean = await client.isInitialized();
124 |
125 | const callback = function (_: string): void {};
126 | const registerFeatureFlagListener: void = client.registerFeatureFlagListener('key', callback);
127 | const unregisterFeatureFlagListener: void = client.unregisterFeatureFlagListener('key', callback);
128 | const registerAllFlagsListener: void = client.registerAllFlagsListener('id', (flags) => flags);
129 | const unregisterAllFlagsListener: void = client.unregisterAllFlagsListener('id');
130 | const registerCurrentConnectionModeListener: void = client.registerCurrentConnectionModeListener('id', callback);
131 | const unregisterCurrentConnectionModeListener: void = client.unregisterCurrentConnectionModeListener('id');
132 |
133 | const getConnectionMode: LDConnectionMode = await client.getConnectionMode();
134 | const getSuccessfulConnection: number | null = await client.getLastSuccessfulConnection();
135 | const getFailedConnection: number | null = await client.getLastFailedConnection();
136 | const getFailureReason: LDFailureReason | null = await client.getLastFailure();
137 |
138 | const flush: void = await client.flush();
139 | const close: void = await client.close();
140 |
141 | const version: String = client.getVersion();
142 | }
143 |
144 | tests();
145 |
--------------------------------------------------------------------------------
/ios/Tests/Tests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tests.swift
3 | // Tests
4 | //
5 | // Created by Yusinto Ngadiman on 2/15/23.
6 | // Copyright © 2023 Facebook. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import LaunchDarkly
11 | @testable import LaunchdarklyReactNativeClient
12 |
13 | final class Tests: XCTestCase {
14 | let anonymousNoKey = #"""
15 | {
16 | "kind": "employee",
17 | "anonymous": true,
18 | "name": "Yus"
19 | }
20 | """#
21 |
22 | let employeeJson = #"""
23 | {
24 | "kind": "employee",
25 | "key": "blr123",
26 | "anonymous": true,
27 | "name": "Yus",
28 | "employeeNumber": 55,
29 | "isActive": true,
30 | "address": {
31 | "street": "Sunset Blvd",
32 | "number": 321
33 | },
34 | "_meta": {
35 | "privateAttributes": ["address", "employeeNumber"]
36 | }
37 | }
38 | """#
39 |
40 | let multiJson = #"""
41 | {
42 | "kind": "multi",
43 | "employee": {
44 | "key": "blr123",
45 | "anonymous": true,
46 | "name": "Yus",
47 | "employeeNumber": 55,
48 | "isActive": true,
49 | "address": {
50 | "street": "Sunset Blvd",
51 | "number": 321
52 | },
53 | "_meta": {
54 | "privateAttributes": ["address", "employeeNumber"]
55 | }
56 | },
57 | "org": {
58 | "key": "qf32",
59 | "name": "Qantas",
60 | "employeeCount": 10000,
61 | "isInternational": true,
62 | "address": {
63 | "street": "Bourke St",
64 | "number": 22,
65 | "country": "Australia"
66 | }
67 | }
68 | }
69 | """#
70 |
71 | let config = #"""
72 | {
73 | "mobileKey": "mob-abc",
74 | "enableAutoEnvAttributes": true,
75 | "debugMode": true,
76 | "application": {
77 | "id": "rn-unit-test",
78 | "version": "x.y.z"
79 | },
80 | "pollUrl": "https://poll-url",
81 | "eventsUrl": "https://events-url",
82 | "streamUrl": "https://stream-url",
83 | "eventCapacity": 11,
84 | "flushInterval": 22,
85 | "connectionTimeout": 33,
86 | "pollingInterval": 44,
87 | "backgroundPollingInterval": 55,
88 | "maxCachedContexts": 66,
89 | "diagnosticRecordingInterval": 1200000,
90 | "allAttributesPrivate": true,
91 | "privateAttributes": ["address", "email", "username"]
92 | }
93 | """#
94 | private func jsonToDict(_ json: String) -> NSDictionary {
95 | let data = try json.data(using: .utf8)
96 | return try! (JSONSerialization.jsonObject(with: data!, options: []) as? [String : Any])! as NSDictionary
97 | }
98 |
99 | private func createEmployeeContext() -> NSDictionary {
100 | return jsonToDict(employeeJson)
101 | }
102 |
103 | private func createMultiContext() -> NSDictionary {
104 | return jsonToDict(multiJson)
105 | }
106 |
107 | func testCreateSingleContext() throws {
108 | let dict = createEmployeeContext()
109 | let context = try LaunchdarklyReactNativeClient().createSingleContext(dict as NSDictionary, dict["kind"] as! String)
110 | let anonymous = context.getValue(Reference("anonymous"))!
111 | let name = context.getValue(Reference("name"))!
112 |
113 | XCTAssertEqual(context.kind, Kind.custom("employee"))
114 | XCTAssertEqual(context.contextKeys(), ["employee": "blr123"])
115 | XCTAssertEqual(true, anonymous)
116 | XCTAssertEqual(name, "Yus")
117 | XCTAssertEqual(context.attributes, ["employeeNumber": LDValue.number(55.0), "address": LDValue.object(["number": LDValue.number(321.0), "street": LDValue.string("Sunset Blvd")]), "isActive": LDValue.bool(true)])
118 | XCTAssertEqual(context.privateAttributes, [Reference("address"), Reference("employeeNumber")])
119 | XCTAssertFalse(context.isMulti())
120 |
121 | let expected = try JSONDecoder().decode(LDContext.self, from: Data(employeeJson.utf8))
122 | XCTAssertEqual(context, expected)
123 | }
124 |
125 | func testAnonymousNoKey() throws {
126 | let dict = jsonToDict(anonymousNoKey)
127 | let context = try LaunchdarklyReactNativeClient().createSingleContext(dict as NSDictionary, dict["kind"] as! String)
128 | let anonymous = context.getValue(Reference("anonymous"))!
129 | let uuid = context.getValue(Reference("key"))!
130 | XCTAssertEqual(true, anonymous)
131 | XCTAssertNotEqual(LDValue.null, uuid)
132 | }
133 |
134 | func testCreateMultiContext() throws {
135 | let dict = createMultiContext()
136 | let context = try LaunchdarklyReactNativeClient().contextBuild(dict as NSDictionary)
137 |
138 | XCTAssertTrue(context.isMulti())
139 | XCTAssertEqual(context.contextKeys(), ["employee": "blr123", "org": "qf32"])
140 |
141 | let expected = try JSONDecoder().decode(LDContext.self, from: Data(multiJson.utf8))
142 | XCTAssertEqual(context, expected)
143 | }
144 |
145 | func testConfigBuild() throws {
146 | let configDict = jsonToDict(config)
147 | let config = try LaunchdarklyReactNativeClient().configBuild(config: configDict)!
148 |
149 | XCTAssertEqual(config.applicationInfo?.buildTag(), "application-id/rn-unit-test application-version/x.y.z")
150 | XCTAssertEqual(config.baseUrl.absoluteString, configDict["pollUrl"] as! String)
151 | XCTAssertEqual(config.eventsUrl.absoluteString, configDict["eventsUrl"] as! String)
152 | XCTAssertEqual(config.streamUrl.absoluteString, configDict["streamUrl"] as! String)
153 | XCTAssertEqual(config.eventCapacity, configDict["eventCapacity"] as! Int)
154 | XCTAssertEqual(config.eventFlushInterval as Double * 1000, configDict["flushInterval"] as! Double)
155 | XCTAssertEqual(config.connectionTimeout as Double * 1000, configDict["connectionTimeout"] as! Double)
156 | XCTAssertEqual(config.flagPollingInterval as Double * 1000, configDict["pollingInterval"] as! Double)
157 | XCTAssertEqual(config.backgroundFlagPollingInterval as Double * 1000, configDict["backgroundPollingInterval"] as! Double)
158 | XCTAssertEqual(config.maxCachedContexts, configDict["maxCachedContexts"] as! Int)
159 | XCTAssertEqual(config.diagnosticRecordingInterval as Double * 1000, configDict["diagnosticRecordingInterval"] as! Double)
160 | XCTAssertTrue(config.allContextAttributesPrivate)
161 | XCTAssertEqual(config.privateContextAttributes, [Reference("address"), Reference("email"), Reference("username")])
162 | XCTAssertEqual(config.autoEnvAttributes, configDict["enableAutoEnvAttributes"] as! Bool)
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/ManualTestApp/App.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-bitwise,react-native/no-inline-styles */
2 | import React, {useState, useEffect, ReactNode} from 'react';
3 | import {
4 | SafeAreaView,
5 | ScrollView,
6 | StyleSheet,
7 | Text,
8 | View,
9 | Button,
10 | TextInput,
11 | Alert,
12 | Switch,
13 | } from 'react-native';
14 | import {MOBILE_KEY} from '@env';
15 | import {Picker} from '@react-native-picker/picker';
16 | import LDClient, {
17 | LDConfig,
18 | LDMultiKindContext,
19 | } from 'launchdarkly-react-native-client-sdk';
20 | import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue';
21 |
22 | const Wrapper = ({children}: {children: ReactNode}) => {
23 | const styles = {
24 | scroll: {backgroundColor: '#fff', padding: 10},
25 | area: {backgroundColor: '#fff', flex: 1},
26 | };
27 | return (
28 |
29 | {children}
30 |
31 | );
32 | };
33 |
34 | const Body = () => {
35 | const [client, setClient] = useState(null);
36 | const [flagKey, setFlagKey] = useState('dev-test-flag');
37 | const [flagType, setFlagType] = useState('bool');
38 | const [isOffline, setIsOffline] = useState(false);
39 | const [contextKey, setContextKey] = useState('context-key');
40 | const [listenerKey, setListenerKey] = useState('');
41 | const [listeners, setListeners] = useState({});
42 |
43 | useEffect(() => {
44 | async function initializeClient() {
45 | let ldClient = new LDClient();
46 | let config: LDConfig = {
47 | mobileKey: MOBILE_KEY,
48 | enableAutoEnvAttributes: true,
49 | debugMode: true,
50 | application: {
51 | id: 'rn-manual-test-app',
52 | version: '0.0.1',
53 | },
54 | };
55 | const userContext = {
56 | kind: 'user',
57 | key: 'test-key',
58 | };
59 | const multiContext: LDMultiKindContext = {
60 | kind: 'multi',
61 | user: userContext,
62 | org: {
63 | key: 'org-key',
64 | name: 'Example organization name',
65 | _meta: {
66 | privateAttributes: ['address', 'phone'],
67 | },
68 | address: {
69 | street: 'sunset blvd',
70 | postcode: 94105,
71 | },
72 | phone: 5551234,
73 | },
74 | };
75 |
76 | try {
77 | await ldClient.configure(config, multiContext);
78 | } catch (err) {
79 | console.error(err);
80 | }
81 | setClient(ldClient);
82 | }
83 |
84 | if (client == null) {
85 | initializeClient().then(() =>
86 | console.log('ld client initialized successfully'),
87 | );
88 | }
89 | });
90 |
91 | const evalFlag = async () => {
92 | let res;
93 | if (flagType === 'bool') {
94 | res = await client?.boolVariation(flagKey, false);
95 | } else if (flagType === 'string') {
96 | res = await client?.stringVariation(flagKey, '');
97 | } else if (flagType === 'number') {
98 | res = await client?.numberVariationDetail(flagKey, 33);
99 | } else if (flagType === 'json') {
100 | res = await client?.jsonVariation(flagKey, null);
101 | }
102 |
103 | Alert.alert('LD Server Response', JSON.stringify(res));
104 | };
105 |
106 | const track = () => {
107 | client?.track(flagKey, false);
108 | };
109 |
110 | const identify = () => {
111 | client?.identify({kind: 'user', key: contextKey});
112 | };
113 |
114 | const listen = () => {
115 | if (listeners.hasOwnProperty(listenerKey)) {
116 | return;
117 | }
118 | let listener = (value: string | undefined) =>
119 | Alert.alert('Listener Callback', value);
120 | client?.registerFeatureFlagListener(listenerKey, listener);
121 | setListeners({...listeners, ...{[listenerKey]: listener}});
122 | };
123 |
124 | const removeListener = () => {
125 | // @ts-ignore
126 | client?.unregisterFeatureFlagListener(listenerKey, listeners[listenerKey]);
127 | // @ts-ignore
128 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
129 | let {[listenerKey]: omit, ...newListeners} = listeners;
130 | setListeners(newListeners);
131 | };
132 |
133 | const flush = () => {
134 | client?.flush();
135 | };
136 |
137 | const setOffline = (offline: boolean) => {
138 | if (offline) {
139 | client?.setOffline();
140 | } else {
141 | client?.setOnline();
142 | }
143 |
144 | setIsOffline(offline);
145 | };
146 |
147 | return (
148 | <>
149 | Feature Key:
150 |
156 |
157 |
158 | ) =>
162 | setFlagType(itemValue)
163 | }>
164 |
165 |
166 |
167 |
168 |
169 | Offline
170 |
171 |
172 | Context key:
173 |
179 |
180 |
181 |
182 |
183 |
184 | Feature Flag Listener Key:
185 |
191 |
192 |
193 |
194 |
195 | >
196 | );
197 | };
198 |
199 | const styles = StyleSheet.create({
200 | row: {
201 | flexDirection: 'row',
202 | paddingVertical: 10,
203 | alignItems: 'center',
204 | },
205 | input: {
206 | height: 40,
207 | borderColor: 'gray',
208 | borderWidth: 1,
209 | },
210 | });
211 |
212 | const App = () => {
213 | return (
214 |
215 |
216 |
217 | );
218 | };
219 |
220 | MessageQueue.spy(msg => {
221 | if (
222 | msg.module !== 'LaunchdarklyReactNativeClient' &&
223 | typeof msg.method === 'string' &&
224 | !msg.method.includes('LaunchdarklyReactNativeClient')
225 | ) {
226 | return;
227 | }
228 | let logMsg = msg.type === 0 ? 'N->JS: ' : 'JS->N: ';
229 | if (typeof msg.method !== 'number') {
230 | logMsg += msg.method.replace('LaunchdarklyReactNativeClient.', '');
231 | }
232 |
233 | let params = [...msg.args];
234 | if (params.length >= 2) {
235 | let cbIdSucc = params[params.length - 1];
236 | let cbIdFail = params[params.length - 2];
237 | if (
238 | Number.isInteger(cbIdSucc) &&
239 | Number.isInteger(cbIdFail) &&
240 | (cbIdSucc & 1) === 1 &&
241 | (cbIdFail & 1) === 0 &&
242 | cbIdSucc >>> 1 === cbIdFail >>> 1
243 | ) {
244 | params.splice(-2, 2, '');
245 | }
246 | }
247 |
248 | logMsg += '(' + params.map(p => JSON.stringify(p)).join(', ') + ')';
249 | console.log(logMsg);
250 | });
251 |
252 | export default App;
253 |
--------------------------------------------------------------------------------
/android/src/main/java/com/launchdarkly/reactnative/utils/LDUtil.java:
--------------------------------------------------------------------------------
1 | package com.launchdarkly.reactnative.utils;
2 |
3 | import com.facebook.react.bridge.Dynamic;
4 | import com.facebook.react.bridge.ReadableArray;
5 | import com.facebook.react.bridge.ReadableMap;
6 | import com.facebook.react.bridge.ReadableMapKeySetIterator;
7 | import com.facebook.react.bridge.ReadableType;
8 | import com.facebook.react.bridge.WritableArray;
9 | import com.facebook.react.bridge.WritableMap;
10 | import com.facebook.react.bridge.WritableNativeArray;
11 | import com.facebook.react.bridge.WritableNativeMap;
12 | import com.launchdarkly.sdk.ArrayBuilder;
13 | import com.launchdarkly.sdk.ContextBuilder;
14 | import com.launchdarkly.sdk.ContextKind;
15 | import com.launchdarkly.sdk.ContextMultiBuilder;
16 | import com.launchdarkly.sdk.LDContext;
17 | import com.launchdarkly.sdk.LDValue;
18 | import com.launchdarkly.sdk.ObjectBuilder;
19 |
20 | import java.lang.reflect.Method;
21 | import java.util.ArrayList;
22 |
23 | public class LDUtil {
24 | public static boolean validateConfig(String key, ReadableMap config, ReadableType type) {
25 | return config.hasKey(key) && config.getType(key) == type;
26 | }
27 |
28 | public static LDContext configureContext(ReadableMap map) {
29 | String kind = null;
30 | if (validateConfig("kind", map, ReadableType.String)) {
31 | kind = map.getString("kind");
32 | }
33 |
34 | if (kind.equals("multi")) {
35 | ContextMultiBuilder b = LDContext.multiBuilder();
36 | ReadableMapKeySetIterator mapKeys = map.keySetIterator();
37 |
38 | while (mapKeys.hasNextKey()) {
39 | String k = mapKeys.nextKey();
40 | if (!k.equals("kind")) {
41 | b.add(createSingleContext(map.getMap(k), k));
42 | }
43 | }
44 |
45 | return b.build();
46 | } else { // single
47 | return createSingleContext(map, kind);
48 | }
49 | }
50 |
51 | public static LDContext createSingleContext(ReadableMap map, String kind) {
52 | Boolean anonymous = null;
53 | if (validateConfig("anonymous", map, ReadableType.Boolean)) {
54 | anonymous = map.getBoolean("anonymous");
55 | }
56 |
57 | String key = null;
58 | if (validateConfig("key", map, ReadableType.String)) {
59 | key = map.getString("key");
60 | }
61 |
62 | // force a placeholder key if anonymous and none is specified
63 | if (anonymous != null && anonymous && key == null) {
64 | key = "__LD_PLACEHOLDER_KEY__";
65 | }
66 |
67 | ContextBuilder b = LDContext.builder(ContextKind.of(kind), key);
68 |
69 | if (anonymous != null) {
70 | b.anonymous(anonymous);
71 | }
72 |
73 | String name = null;
74 | if (validateConfig("name", map, ReadableType.String)) {
75 | name = map.getString("name");
76 | b.name(name);
77 | }
78 |
79 | if (validateConfig("_meta", map, ReadableType.Map)) {
80 | b.privateAttributes(getPrivateAttributesArray(map.getMap("_meta")));
81 | }
82 |
83 | // arbitrary attributes
84 | ReadableMapKeySetIterator mapKeys = map.keySetIterator();
85 | while (mapKeys.hasNextKey()) {
86 | String k = mapKeys.nextKey();
87 |
88 | // ignore built-in attributes
89 | if (!k.equals("kind") && !k.equals("key") && !k.equals("name") && !k.equals("anonymous") && !k.equals("_meta")) {
90 | LDValue v = toLDValue(map.getDynamic(k));
91 | b.set(k, v);
92 | }
93 | }
94 |
95 | return b.build();
96 | }
97 |
98 | public static String[] getPrivateAttributesArray(ReadableMap map) {
99 | ArrayList list = new ArrayList<>();
100 | if (validateConfig("privateAttributes", map, ReadableType.Array)) {
101 | ReadableArray arr = map.getArray("privateAttributes");
102 | for (int i = 0; i < arr.size(); i++) {
103 | if (arr.getType(i) == ReadableType.String) {
104 | list.add(arr.getString(i));
105 | }
106 | }
107 | }
108 |
109 | return list.toArray(new String[list.size()]);
110 | }
111 |
112 |
113 | public static LDValue toLDValue(Dynamic data) {
114 | if (data == null) {
115 | return LDValue.ofNull();
116 | }
117 | switch (data.getType()) {
118 | case Boolean:
119 | return LDValue.of(data.asBoolean());
120 | case Number:
121 | return LDValue.of(data.asDouble());
122 | case String:
123 | return LDValue.of(data.asString());
124 | case Array:
125 | return toLDValue(data.asArray());
126 | case Map:
127 | return toLDValue(data.asMap());
128 | default:
129 | return LDValue.ofNull();
130 | }
131 | }
132 |
133 | public static LDValue toLDValue(ReadableArray readableArray) {
134 | ArrayBuilder array = LDValue.buildArray();
135 | for (int i = 0; i < readableArray.size(); i++) {
136 | array.add(toLDValue(readableArray.getDynamic(i)));
137 | }
138 | return array.build();
139 | }
140 |
141 | public static LDValue toLDValue(ReadableMap readableMap) {
142 | ObjectBuilder object = LDValue.buildObject();
143 | ReadableMapKeySetIterator iter = readableMap.keySetIterator();
144 | while (iter.hasNextKey()) {
145 | String key = iter.nextKey();
146 | object.put(key, toLDValue(readableMap.getDynamic(key)));
147 | }
148 | return object.build();
149 | }
150 |
151 | public static Object ldValueToBridge(LDValue value) {
152 | switch (value.getType()) {
153 | case BOOLEAN:
154 | return value.booleanValue();
155 | case NUMBER:
156 | return value.doubleValue();
157 | case STRING:
158 | return value.stringValue();
159 | case ARRAY:
160 | return ldValueToArray(value);
161 | case OBJECT:
162 | return ldValueToMap(value);
163 | default:
164 | return null;
165 | }
166 | }
167 |
168 | public static WritableArray ldValueToArray(LDValue value) {
169 | WritableArray result = new WritableNativeArray();
170 | for (LDValue val : value.values()) {
171 | switch (val.getType()) {
172 | case NULL:
173 | result.pushNull();
174 | break;
175 | case BOOLEAN:
176 | result.pushBoolean(val.booleanValue());
177 | break;
178 | case NUMBER:
179 | result.pushDouble(val.doubleValue());
180 | break;
181 | case STRING:
182 | result.pushString(val.stringValue());
183 | break;
184 | case ARRAY:
185 | result.pushArray(ldValueToArray(val));
186 | break;
187 | case OBJECT:
188 | result.pushMap(ldValueToMap(val));
189 | break;
190 | }
191 | }
192 | return result;
193 | }
194 |
195 | public static WritableMap ldValueToMap(LDValue value) {
196 | WritableMap result = new WritableNativeMap();
197 | for (String key : value.keys()) {
198 | LDValue val = value.get(key);
199 | switch (val.getType()) {
200 | case NULL:
201 | result.putNull(key);
202 | break;
203 | case BOOLEAN:
204 | result.putBoolean(key, val.booleanValue());
205 | break;
206 | case NUMBER:
207 | result.putDouble(key, val.doubleValue());
208 | break;
209 | case STRING:
210 | result.putString(key, val.stringValue());
211 | break;
212 | case ARRAY:
213 | result.putArray(key, ldValueToArray(val));
214 | break;
215 | case OBJECT:
216 | result.putMap(key, ldValueToMap(val));
217 | break;
218 | }
219 | }
220 | return result;
221 | }
222 |
223 | public static Method findSetter(Class cls, String methodName) {
224 | for (Method method : cls.getMethods()) {
225 | if (method.getName().equals(methodName) && method.getParameterTypes().length == 1)
226 | return method;
227 | }
228 | return null;
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/ManualTestApp/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command;
206 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
207 | # shell script including quotes and variable substitutions, so put them in
208 | # double quotes to make sure that they get re-expanded; and
209 | # * put everything else in single quotes, so that it's not re-expanded.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import { NativeModules, NativeEventEmitter } from 'react-native';
2 | import { version } from './package.json';
3 | import { validateContext } from './src/contextUtils';
4 | import { LDInvalidUserError } from 'launchdarkly-js-sdk-common/src/errors';
5 | import { invalidContext } from 'launchdarkly-js-sdk-common/src/messages';
6 |
7 | let LaunchdarklyReactNativeClient = NativeModules.LaunchdarklyReactNativeClient;
8 |
9 | export default class LDClient {
10 | constructor() {
11 | this.eventEmitter = new NativeEventEmitter(LaunchdarklyReactNativeClient);
12 | this.flagListeners = {};
13 | this.allFlagsListeners = {};
14 | this.connectionModeListeners = {};
15 | this.eventEmitter.addListener(LaunchdarklyReactNativeClient.FLAG_PREFIX, (body) => this._flagUpdateListener(body));
16 | this.eventEmitter.addListener(LaunchdarklyReactNativeClient.ALL_FLAGS_PREFIX, (body) =>
17 | this._allFlagsUpdateListener(body),
18 | );
19 | this.eventEmitter.addListener(LaunchdarklyReactNativeClient.CONNECTION_MODE_PREFIX, (body) =>
20 | this._connectionModeUpdateListener(body),
21 | );
22 | }
23 |
24 | getVersion() {
25 | return String(version);
26 | }
27 |
28 | configure(config, context, timeout) {
29 | if (validateContext(context)) {
30 | return LaunchdarklyReactNativeClient.isInitialized('default').then(
31 | (ignored) => {
32 | throw new Error('LaunchDarkly SDK already initialized');
33 | },
34 | () => {
35 | const configWithOverriddenDefaults = Object.assign(
36 | {
37 | enableAutoEnvAttributes: false,
38 | backgroundPollingInterval: 3600000, // the iOS SDK defaults this to 900000
39 | disableBackgroundUpdating: false, // the iOS SDK defaults this to true
40 | wrapperName: 'react-native-client-sdk',
41 | wrapperVersion: this.getVersion(),
42 | },
43 | config,
44 | );
45 |
46 | if (timeout == undefined) {
47 | return LaunchdarklyReactNativeClient.configure(configWithOverriddenDefaults, context);
48 | } else {
49 | return LaunchdarklyReactNativeClient.configureWithTimeout(configWithOverriddenDefaults, context, timeout);
50 | }
51 | },
52 | );
53 | } else {
54 | return Promise.reject(new LDInvalidUserError(invalidContext(), null));
55 | }
56 | }
57 |
58 | _validateDefault(defaultType, defaultValue, validator) {
59 | if (typeof defaultValue !== defaultType || (typeof validator === 'function' && !validator(defaultValue))) {
60 | return Promise.reject(new Error('Missing or invalid defaultValue for variation call'));
61 | }
62 | return Promise.resolve();
63 | }
64 |
65 | _normalizeEnv(environment) {
66 | if (typeof environment !== 'string') {
67 | return 'default';
68 | }
69 | return environment;
70 | }
71 |
72 | boolVariation(flagKey, defaultValue, environment) {
73 | return this._validateDefault('boolean', defaultValue).then(() =>
74 | LaunchdarklyReactNativeClient.boolVariation(flagKey, defaultValue, this._normalizeEnv(environment)),
75 | );
76 | }
77 |
78 | numberVariation(flagKey, defaultValue, environment) {
79 | return this._validateDefault('number', defaultValue, (val) => !isNaN(val)).then(() =>
80 | LaunchdarklyReactNativeClient.numberVariation(flagKey, defaultValue, this._normalizeEnv(environment)),
81 | );
82 | }
83 |
84 | stringVariation(flagKey, defaultValue, environment) {
85 | if (defaultValue != null && typeof defaultValue !== 'string') {
86 | return Promise.reject(new Error('Missing or invalid defaultValue for variation call'));
87 | } else if (defaultValue === undefined) {
88 | defaultValue = null;
89 | }
90 | return LaunchdarklyReactNativeClient.stringVariation(flagKey, defaultValue, this._normalizeEnv(environment));
91 | }
92 |
93 | jsonVariation(flagKey, defaultValue, environment) {
94 | if (defaultValue === undefined) {
95 | defaultValue = null;
96 | }
97 | return LaunchdarklyReactNativeClient.jsonVariation(flagKey, defaultValue, this._normalizeEnv(environment));
98 | }
99 |
100 | boolVariationDetail(flagKey, defaultValue, environment) {
101 | return this._validateDefault('boolean', defaultValue).then(() =>
102 | LaunchdarklyReactNativeClient.boolVariationDetail(flagKey, defaultValue, this._normalizeEnv(environment)),
103 | );
104 | }
105 |
106 | numberVariationDetail(flagKey, defaultValue, environment) {
107 | return this._validateDefault('number', defaultValue, (val) => !isNaN(val)).then(() =>
108 | LaunchdarklyReactNativeClient.numberVariationDetail(flagKey, defaultValue, this._normalizeEnv(environment)),
109 | );
110 | }
111 |
112 | stringVariationDetail(flagKey, defaultValue, environment) {
113 | if (defaultValue != null && typeof defaultValue !== 'string') {
114 | return Promise.reject(new Error('Missing or invalid defaultValue for variation call'));
115 | } else if (defaultValue === undefined) {
116 | defaultValue = null;
117 | }
118 | return LaunchdarklyReactNativeClient.stringVariationDetail(flagKey, defaultValue, this._normalizeEnv(environment));
119 | }
120 |
121 | jsonVariationDetail(flagKey, defaultValue, environment) {
122 | if (defaultValue === undefined) {
123 | defaultValue = null;
124 | }
125 | return LaunchdarklyReactNativeClient.jsonVariationDetail(flagKey, defaultValue, this._normalizeEnv(environment));
126 | }
127 |
128 | allFlags(environment) {
129 | return LaunchdarklyReactNativeClient.allFlags(this._normalizeEnv(environment));
130 | }
131 |
132 | track(eventName, data, metricValue, environment) {
133 | if (data === undefined) {
134 | data = null;
135 | }
136 | if (typeof metricValue === 'number') {
137 | LaunchdarklyReactNativeClient.trackMetricValue(eventName, data, metricValue, this._normalizeEnv(environment));
138 | } else {
139 | LaunchdarklyReactNativeClient.trackData(eventName, data, this._normalizeEnv(environment));
140 | }
141 | }
142 |
143 | setOffline() {
144 | return LaunchdarklyReactNativeClient.setOffline();
145 | }
146 |
147 | isOffline() {
148 | return LaunchdarklyReactNativeClient.isOffline();
149 | }
150 |
151 | setOnline() {
152 | return LaunchdarklyReactNativeClient.setOnline();
153 | }
154 |
155 | isInitialized(environment) {
156 | return LaunchdarklyReactNativeClient.isInitialized(this._normalizeEnv(environment));
157 | }
158 |
159 | flush() {
160 | LaunchdarklyReactNativeClient.flush();
161 | }
162 |
163 | close() {
164 | LaunchdarklyReactNativeClient.close();
165 | }
166 |
167 | identify(context) {
168 | if (validateContext(context)) {
169 | return LaunchdarklyReactNativeClient.identify(context);
170 | } else {
171 | return Promise.reject(new LDInvalidUserError(invalidContext(), null));
172 | }
173 | }
174 |
175 | _flagUpdateListener(changedFlag) {
176 | const flagKey = changedFlag.flagKey;
177 | const listenerId = changedFlag.listenerId;
178 | if (this.flagListeners.hasOwnProperty(listenerId)) {
179 | let listeners = this.flagListeners[listenerId];
180 | for (const listener of listeners) {
181 | listener(flagKey);
182 | }
183 | }
184 | }
185 |
186 | _allFlagsUpdateListener(changedFlags) {
187 | const flagKeys = changedFlags.flagKeys;
188 | const listenerId = changedFlags.listenerId;
189 | if (this.allFlagsListeners.hasOwnProperty(listenerId)) {
190 | this.allFlagsListeners[listenerId](flagKeys);
191 | }
192 | }
193 |
194 | _connectionModeUpdateListener(connectionStatus) {
195 | const connectionMode = connectionStatus.connectionMode;
196 | const listenerId = connectionStatus.listenerId;
197 | if (this.connectionModeListeners.hasOwnProperty(listenerId)) {
198 | this.connectionModeListeners[listenerId](connectionMode);
199 | }
200 | }
201 |
202 | _envConcat(env, flagKey) {
203 | return env.concat(';', flagKey);
204 | }
205 |
206 | registerFeatureFlagListener(flagKey, callback, environment) {
207 | if (typeof callback !== 'function') {
208 | return;
209 | }
210 | const env = this._normalizeEnv(environment);
211 | const multiFlagKey = this._envConcat(env, flagKey);
212 |
213 | if (this.flagListeners.hasOwnProperty(multiFlagKey)) {
214 | this.flagListeners[multiFlagKey].push(callback);
215 | } else {
216 | this.flagListeners[multiFlagKey] = [callback];
217 |
218 | LaunchdarklyReactNativeClient.registerFeatureFlagListener(flagKey, env);
219 | }
220 | }
221 |
222 | unregisterFeatureFlagListener(flagKey, callback, environment) {
223 | const env = this._normalizeEnv(environment);
224 | const multiFlagKey = this._envConcat(env, flagKey);
225 | if (!this.flagListeners.hasOwnProperty(multiFlagKey)) {
226 | return;
227 | }
228 |
229 | this.flagListeners[multiFlagKey] = this.flagListeners[multiFlagKey].filter((listener) => listener != callback);
230 |
231 | if (this.flagListeners[multiFlagKey].length == 0) {
232 | LaunchdarklyReactNativeClient.unregisterFeatureFlagListener(flagKey, env);
233 | delete this.flagListeners[multiFlagKey];
234 | }
235 | }
236 |
237 | registerCurrentConnectionModeListener(listenerId, callback, environment) {
238 | if (typeof callback !== 'function') {
239 | return;
240 | }
241 | const env = this._normalizeEnv(environment);
242 | const multiListenerId = this._envConcat(env, listenerId);
243 |
244 | this.connectionModeListeners[multiListenerId] = callback;
245 | LaunchdarklyReactNativeClient.registerCurrentConnectionModeListener(listenerId, env);
246 | }
247 |
248 | unregisterCurrentConnectionModeListener(listenerId, environment) {
249 | const env = this._normalizeEnv(environment);
250 | const multiListenerId = this._envConcat(env, listenerId);
251 | if (!this.connectionModeListeners.hasOwnProperty(multiListenerId)) {
252 | return;
253 | }
254 |
255 | LaunchdarklyReactNativeClient.unregisterCurrentConnectionModeListener(listenerId, env);
256 | delete this.connectionModeListeners[multiListenerId];
257 | }
258 |
259 | registerAllFlagsListener(listenerId, callback, environment) {
260 | if (typeof callback !== 'function') {
261 | return;
262 | }
263 | const env = this._normalizeEnv(environment);
264 | const multiListenerId = this._envConcat(env, listenerId);
265 |
266 | this.allFlagsListeners[multiListenerId] = callback;
267 | LaunchdarklyReactNativeClient.registerAllFlagsListener(listenerId, env);
268 | }
269 |
270 | unregisterAllFlagsListener(listenerId, environment) {
271 | const env = this._normalizeEnv(environment);
272 | const multiListenerId = this._envConcat(env, listenerId);
273 | if (!this.allFlagsListeners.hasOwnProperty(multiListenerId)) {
274 | return;
275 | }
276 |
277 | LaunchdarklyReactNativeClient.unregisterAllFlagsListener(listenerId, env);
278 | delete this.allFlagsListeners[multiListenerId];
279 | }
280 |
281 | getConnectionMode(environment) {
282 | return LaunchdarklyReactNativeClient.getConnectionMode(this._normalizeEnv(environment));
283 | }
284 |
285 | getLastSuccessfulConnection(environment) {
286 | return LaunchdarklyReactNativeClient.getLastSuccessfulConnection(this._normalizeEnv(environment));
287 | }
288 |
289 | getLastFailedConnection(environment) {
290 | return LaunchdarklyReactNativeClient.getLastFailedConnection(this._normalizeEnv(environment));
291 | }
292 |
293 | getLastFailure(environment) {
294 | return LaunchdarklyReactNativeClient.getLastFailure(this._normalizeEnv(environment));
295 | }
296 | }
297 |
--------------------------------------------------------------------------------
/index.test.js:
--------------------------------------------------------------------------------
1 | import { NativeModules, NativeEventEmitter } from 'react-native';
2 | import LDClient from './index.js';
3 |
4 | const defValErr = new Error('Missing or invalid defaultValue for variation call');
5 |
6 | let client;
7 | let addListenerMock;
8 | let nativeMock = NativeModules.LaunchdarklyReactNativeClient;
9 |
10 | function getClientFlagListener() {
11 | return addListenerMock.calls[0][1];
12 | }
13 |
14 | function getClientFlagsListener() {
15 | return addListenerMock.calls[1][1];
16 | }
17 |
18 | function getClientConnectionListener() {
19 | return addListenerMock.calls[2][1];
20 | }
21 |
22 | beforeEach(() => {
23 | Object.values(nativeMock).forEach((v) => {
24 | if (typeof v === 'function') {
25 | v.mockClear();
26 | }
27 | });
28 | NativeEventEmitter.mockClear();
29 |
30 | client = new LDClient();
31 | expect(NativeEventEmitter).toHaveBeenCalledTimes(1);
32 | expect(NativeEventEmitter.mock.calls[0].length).toBe(1);
33 | expect(Object.is(NativeEventEmitter.mock.calls[0][0], NativeModules.LaunchdarklyReactNativeClient)).toBe(true);
34 |
35 | addListenerMock = NativeEventEmitter.mock.results[0].value.addListener.mock;
36 | });
37 |
38 | test('constructor', () => {
39 | expect(addListenerMock.calls.length).toBe(3);
40 | expect(addListenerMock.calls[0].length).toBe(2);
41 | expect(addListenerMock.calls[1].length).toBe(2);
42 | expect(addListenerMock.calls[2].length).toBe(2);
43 | expect(addListenerMock.calls[0][0]).toBe(nativeMock.FLAG_PREFIX);
44 | expect(addListenerMock.calls[1][0]).toBe(nativeMock.ALL_FLAGS_PREFIX);
45 | expect(addListenerMock.calls[2][0]).toBe(nativeMock.CONNECTION_MODE_PREFIX);
46 | });
47 |
48 | describe('boolVariation', () => {
49 | test('validates defaultValue', async () => {
50 | expect.assertions(11);
51 |
52 | await expect(client.boolVariation('flagKey')).rejects.toEqual(defValErr);
53 | await expect(client.boolVariation('flagKey', null)).rejects.toEqual(defValErr);
54 | await expect(client.boolVariation('flagKey', 5)).rejects.toEqual(defValErr);
55 | await expect(client.boolVariationDetail('flagKey')).rejects.toEqual(defValErr);
56 | await expect(client.boolVariationDetail('flagKey', null)).rejects.toEqual(defValErr);
57 | await expect(client.boolVariationDetail('flagKey', 5)).rejects.toEqual(defValErr);
58 |
59 | expect(nativeMock.boolVariation).toHaveBeenCalledTimes(0);
60 | expect(nativeMock.boolVariationDetail).toHaveBeenCalledTimes(0);
61 | });
62 |
63 | test('calls native', async () => {
64 | nativeMock.boolVariation.mockImplementation((k, def, env) => Promise.resolve(!def));
65 |
66 | await expect(client.boolVariation('key1', true)).resolves.toEqual(false);
67 | await expect(client.boolVariation('key2', false, 'alt')).resolves.toEqual(true);
68 | await expect(client.boolVariation('key3', false, 5)).resolves.toEqual(true);
69 |
70 | expect(nativeMock.boolVariation).toHaveBeenCalledTimes(3);
71 | expect(nativeMock.boolVariation).toHaveBeenNthCalledWith(1, 'key1', true, 'default');
72 | expect(nativeMock.boolVariation).toHaveBeenNthCalledWith(2, 'key2', false, 'alt');
73 | expect(nativeMock.boolVariation).toHaveBeenNthCalledWith(3, 'key3', false, 'default');
74 | });
75 |
76 | test('detailed calls native', async () => {
77 | nativeMock.boolVariationDetail.mockImplementation((k, def, env) => Promise.resolve(!def));
78 |
79 | await expect(client.boolVariationDetail('key1', true)).resolves.toEqual(false);
80 | await expect(client.boolVariationDetail('key2', false, 'alt')).resolves.toEqual(true);
81 | await expect(client.boolVariationDetail('key3', false, 5)).resolves.toEqual(true);
82 |
83 | expect(nativeMock.boolVariationDetail).toHaveBeenCalledTimes(3);
84 | expect(nativeMock.boolVariationDetail).toHaveBeenNthCalledWith(1, 'key1', true, 'default');
85 | expect(nativeMock.boolVariationDetail).toHaveBeenNthCalledWith(2, 'key2', false, 'alt');
86 | expect(nativeMock.boolVariationDetail).toHaveBeenNthCalledWith(3, 'key3', false, 'default');
87 | });
88 | });
89 |
90 | describe('numberVariation', () => {
91 | test('validates defaultValue', async () => {
92 | expect.assertions(13);
93 |
94 | await expect(client.numberVariation('flagKey')).rejects.toEqual(defValErr);
95 | await expect(client.numberVariation('flagKey', null)).rejects.toEqual(defValErr);
96 | await expect(client.numberVariation('flagKey', false)).rejects.toEqual(defValErr);
97 | await expect(client.numberVariation('flagKey', NaN)).rejects.toEqual(defValErr);
98 | await expect(client.numberVariationDetail('flagKey')).rejects.toEqual(defValErr);
99 | await expect(client.numberVariationDetail('flagKey', null)).rejects.toEqual(defValErr);
100 | await expect(client.numberVariationDetail('flagKey', false)).rejects.toEqual(defValErr);
101 | await expect(client.numberVariationDetail('flagKey', NaN)).rejects.toEqual(defValErr);
102 |
103 | expect(nativeMock.numberVariation).toHaveBeenCalledTimes(0);
104 | expect(nativeMock.numberVariationDetail).toHaveBeenCalledTimes(0);
105 | });
106 |
107 | test('calls native', async () => {
108 | nativeMock.numberVariation.mockImplementation((k, def, env) => Promise.resolve(def + 1.5));
109 |
110 | await expect(client.numberVariation('key1', 0)).resolves.toEqual(1.5);
111 | await expect(client.numberVariation('key2', 5, 'alt')).resolves.toEqual(6.5);
112 | await expect(client.numberVariation('key3', 2.5, 5)).resolves.toEqual(4);
113 |
114 | expect(nativeMock.numberVariation).toHaveBeenCalledTimes(3);
115 | expect(nativeMock.numberVariation).toHaveBeenNthCalledWith(1, 'key1', 0, 'default');
116 | expect(nativeMock.numberVariation).toHaveBeenNthCalledWith(2, 'key2', 5, 'alt');
117 | expect(nativeMock.numberVariation).toHaveBeenNthCalledWith(3, 'key3', 2.5, 'default');
118 | });
119 |
120 | test('detailed calls native', async () => {
121 | nativeMock.numberVariationDetail.mockImplementation((k, def, env) => Promise.resolve(def + 1.5));
122 |
123 | await expect(client.numberVariationDetail('key1', 0)).resolves.toEqual(1.5);
124 | await expect(client.numberVariationDetail('key2', 5, 'alt')).resolves.toEqual(6.5);
125 | await expect(client.numberVariationDetail('key3', 2.5, 5)).resolves.toEqual(4);
126 |
127 | expect(nativeMock.numberVariationDetail).toHaveBeenCalledTimes(3);
128 | expect(nativeMock.numberVariationDetail).toHaveBeenNthCalledWith(1, 'key1', 0, 'default');
129 | expect(nativeMock.numberVariationDetail).toHaveBeenNthCalledWith(2, 'key2', 5, 'alt');
130 | expect(nativeMock.numberVariationDetail).toHaveBeenNthCalledWith(3, 'key3', 2.5, 'default');
131 | });
132 | });
133 |
134 | describe('stringVariation', () => {
135 | test('validates defaultValue', async () => {
136 | expect.assertions(7);
137 |
138 | await expect(client.stringVariation('flagKey', false)).rejects.toEqual(defValErr);
139 | await expect(client.stringVariationDetail('flagKey', false)).rejects.toEqual(defValErr);
140 |
141 | expect(nativeMock.stringVariation).toHaveBeenCalledTimes(0);
142 | expect(nativeMock.stringVariationDetail).toHaveBeenCalledTimes(0);
143 | });
144 |
145 | test('calls native', async () => {
146 | nativeMock.stringVariation.mockImplementation((k, def, env) => Promise.resolve('foo'.concat(def)));
147 |
148 | await expect(client.stringVariation('key1', '1')).resolves.toEqual('foo1');
149 | await expect(client.stringVariation('key2', null, 'alt')).resolves.toEqual('foonull');
150 | await expect(client.stringVariation('key3', undefined, 5)).resolves.toEqual('foonull');
151 |
152 | expect(nativeMock.stringVariation).toHaveBeenCalledTimes(3);
153 | expect(nativeMock.stringVariation).toHaveBeenNthCalledWith(1, 'key1', '1', 'default');
154 | expect(nativeMock.stringVariation).toHaveBeenNthCalledWith(2, 'key2', null, 'alt');
155 | expect(nativeMock.stringVariation).toHaveBeenNthCalledWith(3, 'key3', null, 'default');
156 | });
157 |
158 | test('detailed calls native', async () => {
159 | nativeMock.stringVariationDetail.mockImplementation((k, def, env) => Promise.resolve('foo'.concat(def)));
160 |
161 | await expect(client.stringVariationDetail('key1', '1')).resolves.toEqual('foo1');
162 | await expect(client.stringVariationDetail('key2', null, 'alt')).resolves.toEqual('foonull');
163 | await expect(client.stringVariationDetail('key3', undefined, 5)).resolves.toEqual('foonull');
164 |
165 | expect(nativeMock.stringVariationDetail).toHaveBeenCalledTimes(3);
166 | expect(nativeMock.stringVariationDetail).toHaveBeenNthCalledWith(1, 'key1', '1', 'default');
167 | expect(nativeMock.stringVariationDetail).toHaveBeenNthCalledWith(2, 'key2', null, 'alt');
168 | expect(nativeMock.stringVariationDetail).toHaveBeenNthCalledWith(3, 'key3', null, 'default');
169 | });
170 | });
171 |
172 | test('allFlags', () => {
173 | nativeMock.allFlags.mockReturnValueOnce('pass1');
174 | expect(client.allFlags()).toBe('pass1');
175 | expect(nativeMock.allFlags).toHaveBeenCalledTimes(1);
176 | expect(nativeMock.allFlags).toHaveBeenNthCalledWith(1, 'default');
177 |
178 | nativeMock.allFlags.mockReturnValueOnce('pass2');
179 | expect(client.allFlags('alt')).toBe('pass2');
180 | expect(nativeMock.allFlags).toHaveBeenCalledTimes(2);
181 | expect(nativeMock.allFlags).toHaveBeenNthCalledWith(2, 'alt');
182 | });
183 |
184 | test('setOffline', () => {
185 | nativeMock.setOffline.mockReturnValue('passthrough');
186 | expect(client.setOffline()).toBe('passthrough');
187 | expect(nativeMock.setOffline).toHaveBeenCalledTimes(1);
188 | });
189 |
190 | test('isOffline', () => {
191 | nativeMock.isOffline.mockReturnValue(true);
192 | expect(client.isOffline()).toBe(true);
193 | expect(nativeMock.isOffline).toHaveBeenCalledTimes(1);
194 | });
195 |
196 | test('setOnline', () => {
197 | nativeMock.setOnline.mockReturnValue('passthrough');
198 | expect(client.setOnline()).toBe('passthrough');
199 | expect(nativeMock.setOnline).toHaveBeenCalledTimes(1);
200 | });
201 |
202 | test('isInitialized', () => {
203 | nativeMock.isInitialized.mockReturnValueOnce(false);
204 | expect(client.isInitialized()).toBe(false);
205 | nativeMock.isInitialized.mockReturnValueOnce(true);
206 | expect(client.isInitialized('alt')).toBe(true);
207 |
208 | expect(nativeMock.isInitialized).toHaveBeenCalledTimes(2);
209 | expect(nativeMock.isInitialized).toHaveBeenNthCalledWith(1, 'default');
210 | expect(nativeMock.isInitialized).toHaveBeenNthCalledWith(2, 'alt');
211 | });
212 |
213 | test('flush', () => {
214 | client.flush();
215 | expect(nativeMock.flush).toHaveBeenCalledTimes(1);
216 | expect(nativeMock.flush).toHaveBeenNthCalledWith(1);
217 | });
218 |
219 | test('close', () => {
220 | client.close();
221 | expect(nativeMock.close).toHaveBeenCalledTimes(1);
222 | expect(nativeMock.close).toHaveBeenNthCalledWith(1);
223 | });
224 |
225 | test('identify', () => {
226 | nativeMock.identify.mockReturnValueOnce('pass1');
227 | const testContext = { kind: 'user', key: 'j-smith-key', name: 'John Smith' };
228 | expect(client.identify(testContext)).toBe('pass1');
229 | expect(nativeMock.identify).toHaveBeenCalledTimes(1);
230 | expect(nativeMock.identify).toHaveBeenNthCalledWith(1, testContext);
231 | });
232 |
233 | test('featureFlagListener', () => {
234 | let clientListener = getClientFlagListener();
235 | let listener1 = jest.fn();
236 | let listener2 = jest.fn();
237 | let listener3 = jest.fn();
238 | client.registerFeatureFlagListener('a', listener1);
239 | client.registerFeatureFlagListener('a', listener2, 'alt');
240 |
241 | expect(listener1).toHaveBeenCalledTimes(0);
242 | expect(listener2).toHaveBeenCalledTimes(0);
243 |
244 | expect(nativeMock.registerFeatureFlagListener).toHaveBeenCalledTimes(2);
245 | expect(nativeMock.registerFeatureFlagListener).toHaveBeenNthCalledWith(1, 'a', 'default');
246 | expect(nativeMock.registerFeatureFlagListener).toHaveBeenNthCalledWith(2, 'a', 'alt');
247 |
248 | client.registerFeatureFlagListener('a', listener3, 'default');
249 | // JS wrapper coalesces listeners for the same key and environment
250 | expect(nativeMock.registerFeatureFlagListener).toHaveBeenCalledTimes(2);
251 | expect(listener3).toHaveBeenCalledTimes(0);
252 |
253 | // Wrapper doesn't call listeners for differing key
254 | clientListener({ flagKey: 'b', listenerId: 'default;b' });
255 | expect(listener1).toHaveBeenCalledTimes(0);
256 | expect(listener2).toHaveBeenCalledTimes(0);
257 | expect(listener3).toHaveBeenCalledTimes(0);
258 |
259 | // Wrapper calls single listener
260 | clientListener({ flagKey: 'a', listenerId: 'alt;a' });
261 | expect(listener1).toHaveBeenCalledTimes(0);
262 | expect(listener2).toHaveBeenCalledTimes(1);
263 | expect(listener3).toHaveBeenCalledTimes(0);
264 | expect(listener2).toHaveBeenNthCalledWith(1, 'a');
265 |
266 | // Wrapper informs both coalesced listeners
267 | clientListener({ flagKey: 'a', listenerId: 'default;a' });
268 | expect(listener1).toHaveBeenCalledTimes(1);
269 | expect(listener2).toHaveBeenCalledTimes(1);
270 | expect(listener3).toHaveBeenCalledTimes(1);
271 | expect(listener1).toHaveBeenNthCalledWith(1, 'a');
272 | expect(listener3).toHaveBeenNthCalledWith(1, 'a');
273 |
274 | // Remove single listener from coalesced, should not unregister from native
275 | client.unregisterFeatureFlagListener('a', listener3);
276 | expect(nativeMock.unregisterFeatureFlagListener).toHaveBeenCalledTimes(0);
277 |
278 | clientListener({ flagKey: 'a', listenerId: 'default;a' });
279 | expect(listener1).toHaveBeenCalledTimes(2);
280 | expect(listener2).toHaveBeenCalledTimes(1);
281 | expect(listener3).toHaveBeenCalledTimes(1);
282 | expect(listener1).toHaveBeenNthCalledWith(2, 'a');
283 |
284 | // Removing remaining listener should unregister on native
285 | client.unregisterFeatureFlagListener('a', listener1, 'default');
286 | client.unregisterFeatureFlagListener('a', listener2, 'alt');
287 | expect(nativeMock.unregisterFeatureFlagListener).toHaveBeenCalledTimes(2);
288 | expect(nativeMock.unregisterFeatureFlagListener).toHaveBeenNthCalledWith(1, 'a', 'default');
289 | expect(nativeMock.unregisterFeatureFlagListener).toHaveBeenNthCalledWith(2, 'a', 'alt');
290 |
291 | // No longer calls listeners
292 | clientListener({ flagKey: 'a', listenerId: 'default;a' });
293 | clientListener({ flagKey: 'a', listenerId: 'alt;a' });
294 | expect(listener1).toHaveBeenCalledTimes(2);
295 | expect(listener2).toHaveBeenCalledTimes(1);
296 | expect(listener3).toHaveBeenCalledTimes(1);
297 | });
298 |
299 | test('connectionModeListener', () => {
300 | let clientListener = getClientConnectionListener();
301 | let listener1 = jest.fn();
302 | let listener2 = jest.fn();
303 | client.registerCurrentConnectionModeListener('a', listener1);
304 | client.registerCurrentConnectionModeListener('b', listener2, 'alt');
305 |
306 | expect(listener1).toHaveBeenCalledTimes(0);
307 | expect(listener2).toHaveBeenCalledTimes(0);
308 |
309 | expect(nativeMock.registerCurrentConnectionModeListener).toHaveBeenCalledTimes(2);
310 | expect(nativeMock.registerCurrentConnectionModeListener).toHaveBeenNthCalledWith(1, 'a', 'default');
311 | expect(nativeMock.registerCurrentConnectionModeListener).toHaveBeenNthCalledWith(2, 'b', 'alt');
312 |
313 | clientListener({ connectionMode: ['abc'], listenerId: 'default;a' });
314 | expect(listener1).toHaveBeenCalledTimes(1);
315 | expect(listener1).toHaveBeenNthCalledWith(1, ['abc']);
316 | expect(listener2).toHaveBeenCalledTimes(0);
317 |
318 | clientListener({ connectionMode: ['def'], listenerId: 'alt;b' });
319 | expect(listener1).toHaveBeenCalledTimes(1);
320 | expect(listener2).toHaveBeenCalledTimes(1);
321 | expect(listener2).toHaveBeenNthCalledWith(1, ['def']);
322 |
323 | client.unregisterCurrentConnectionModeListener('b', 'alt');
324 |
325 | expect(nativeMock.unregisterCurrentConnectionModeListener).toHaveBeenCalledTimes(1);
326 | expect(nativeMock.unregisterCurrentConnectionModeListener).toHaveBeenNthCalledWith(1, 'b', 'alt');
327 |
328 | clientListener({ connectionMode: [], listenerId: 'alt;b' });
329 | clientListener({ connectionMode: [], listenerId: 'alt;a' });
330 |
331 | expect(listener1).toHaveBeenCalledTimes(1);
332 | expect(listener2).toHaveBeenCalledTimes(1);
333 |
334 | client.unregisterCurrentConnectionModeListener('a', 'default');
335 | client.unregisterCurrentConnectionModeListener('b', 'alt');
336 |
337 | expect(nativeMock.unregisterCurrentConnectionModeListener).toHaveBeenCalledTimes(2);
338 | expect(nativeMock.unregisterCurrentConnectionModeListener).toHaveBeenNthCalledWith(2, 'a', 'default');
339 | });
340 |
341 | test('allFlagsListener', () => {
342 | let clientListener = getClientFlagsListener();
343 | let listener1 = jest.fn();
344 | let listener2 = jest.fn();
345 | client.registerAllFlagsListener('a', listener1);
346 | client.registerAllFlagsListener('b', listener2, 'alt');
347 |
348 | expect(listener1).toHaveBeenCalledTimes(0);
349 | expect(listener2).toHaveBeenCalledTimes(0);
350 |
351 | expect(nativeMock.registerAllFlagsListener).toHaveBeenCalledTimes(2);
352 | expect(nativeMock.registerAllFlagsListener).toHaveBeenNthCalledWith(1, 'a', 'default');
353 | expect(nativeMock.registerAllFlagsListener).toHaveBeenNthCalledWith(2, 'b', 'alt');
354 |
355 | clientListener({ flagKeys: ['abc'], listenerId: 'default;a' });
356 | expect(listener1).toHaveBeenCalledTimes(1);
357 | expect(listener1).toHaveBeenNthCalledWith(1, ['abc']);
358 | expect(listener2).toHaveBeenCalledTimes(0);
359 |
360 | clientListener({ flagKeys: ['def'], listenerId: 'alt;b' });
361 | expect(listener1).toHaveBeenCalledTimes(1);
362 | expect(listener2).toHaveBeenCalledTimes(1);
363 | expect(listener2).toHaveBeenNthCalledWith(1, ['def']);
364 |
365 | client.unregisterAllFlagsListener('b', 'alt');
366 |
367 | expect(nativeMock.unregisterAllFlagsListener).toHaveBeenCalledTimes(1);
368 | expect(nativeMock.unregisterAllFlagsListener).toHaveBeenNthCalledWith(1, 'b', 'alt');
369 |
370 | clientListener({ flagKeys: [], listenerId: 'alt;b' });
371 | clientListener({ flagKeys: [], listenerId: 'alt;a' });
372 |
373 | expect(listener1).toHaveBeenCalledTimes(1);
374 | expect(listener2).toHaveBeenCalledTimes(1);
375 |
376 | client.unregisterAllFlagsListener('a', 'default');
377 | client.unregisterAllFlagsListener('b', 'alt');
378 |
379 | expect(nativeMock.unregisterAllFlagsListener).toHaveBeenCalledTimes(2);
380 | expect(nativeMock.unregisterAllFlagsListener).toHaveBeenNthCalledWith(2, 'a', 'default');
381 | });
382 |
383 | test('getConnectionMode', () => {
384 | nativeMock.getConnectionMode.mockReturnValue('passthrough');
385 |
386 | expect(client.getConnectionMode()).toBe('passthrough');
387 | expect(client.getConnectionMode('alt')).toBe('passthrough');
388 |
389 | expect(nativeMock.getConnectionMode).toHaveBeenCalledTimes(2);
390 | expect(nativeMock.getConnectionMode).toHaveBeenNthCalledWith(1, 'default');
391 | expect(nativeMock.getConnectionMode).toHaveBeenNthCalledWith(2, 'alt');
392 | });
393 |
394 | test('getLastSuccessfulConnection', () => {
395 | nativeMock.getLastSuccessfulConnection.mockReturnValue('passthrough');
396 |
397 | expect(client.getLastSuccessfulConnection()).toBe('passthrough');
398 | expect(client.getLastSuccessfulConnection('alt')).toBe('passthrough');
399 |
400 | expect(nativeMock.getLastSuccessfulConnection).toHaveBeenCalledTimes(2);
401 | expect(nativeMock.getLastSuccessfulConnection).toHaveBeenNthCalledWith(1, 'default');
402 | expect(nativeMock.getLastSuccessfulConnection).toHaveBeenNthCalledWith(2, 'alt');
403 | });
404 |
405 | test('getLastFailedConnection', () => {
406 | nativeMock.getLastFailedConnection.mockReturnValue('passthrough');
407 |
408 | expect(client.getLastFailedConnection()).toBe('passthrough');
409 | expect(client.getLastFailedConnection('alt')).toBe('passthrough');
410 |
411 | expect(nativeMock.getLastFailedConnection).toHaveBeenCalledTimes(2);
412 | expect(nativeMock.getLastFailedConnection).toHaveBeenNthCalledWith(1, 'default');
413 | expect(nativeMock.getLastFailedConnection).toHaveBeenNthCalledWith(2, 'alt');
414 | });
415 |
416 | test('getLastFailure', () => {
417 | nativeMock.getLastFailure.mockReturnValue('passthrough');
418 |
419 | expect(client.getLastFailure()).toBe('passthrough');
420 | expect(client.getLastFailure('alt')).toBe('passthrough');
421 |
422 | expect(nativeMock.getLastFailure).toHaveBeenCalledTimes(2);
423 | expect(nativeMock.getLastFailure).toHaveBeenNthCalledWith(1, 'default');
424 | expect(nativeMock.getLastFailure).toHaveBeenNthCalledWith(2, 'alt');
425 | });
426 |
--------------------------------------------------------------------------------
/ios/LaunchdarklyReactNativeClient.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import LaunchDarkly
3 |
4 | @objc(LaunchdarklyReactNativeClient)
5 | class LaunchdarklyReactNativeClient: RCTEventEmitter {
6 | private let FLAG_PREFIX = "LaunchDarkly-Flag-"
7 | private let ALL_FLAGS_PREFIX = "LaunchDarkly-All-Flags-"
8 | private let CONNECTION_MODE_PREFIX = "LaunchDarkly-Connection-Mode-"
9 | private let ERROR_INIT = "E_INITIALIZE"
10 | private let ERROR_IDENTIFY = "E_IDENTIFY"
11 | private let ERROR_UNKNOWN = "E_UNKNOWN"
12 |
13 | private var flagListenerOwners: [String: ObserverOwner] = [:]
14 | private var allFlagsListenerOwners: [String: ObserverOwner] = [:]
15 | private var connectionModeListenerOwners: [String: ObserverOwner] = [:]
16 |
17 | override func supportedEvents() -> [String]! {
18 | return [FLAG_PREFIX, ALL_FLAGS_PREFIX, CONNECTION_MODE_PREFIX]
19 | }
20 |
21 | override func constantsToExport() -> [AnyHashable: Any] {
22 | return ["FLAG_PREFIX": FLAG_PREFIX, "ALL_FLAGS_PREFIX": ALL_FLAGS_PREFIX, "CONNECTION_MODE_PREFIX": CONNECTION_MODE_PREFIX]
23 | }
24 |
25 | override static func requiresMainQueueSetup() -> Bool {
26 | return false
27 | }
28 |
29 | @objc func configure(_ config: NSDictionary, context: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
30 | internalConfigure(config: config, context: context, timeout: nil, resolve: resolve, reject: reject)
31 | }
32 |
33 | @objc func configureWithTimeout(_ config: NSDictionary, context: NSDictionary, timeout: Int, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
34 | internalConfigure(config: config, context: context, timeout: timeout, resolve: resolve, reject: reject)
35 | }
36 |
37 | private func getLDClient(environment: String) -> LDClient? {
38 | if let client = LDClient.get(environment: environment) {
39 | return client
40 | } else {
41 | NSLog("%@", "WARNING: LDClient is nil for env: '\(environment)'")
42 | return nil
43 | }
44 | }
45 |
46 | private func internalConfigure(config: NSDictionary, context: NSDictionary, timeout: Int?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
47 | let config = configBuild(config: config)
48 |
49 | if let config = config {
50 | do {
51 | if let timeoutUnwrapped = timeout {
52 | let startWaitSeconds: TimeInterval = Double(timeoutUnwrapped)
53 |
54 | LDClient.start(config: config, context: try contextBuild(context), startWaitSeconds: startWaitSeconds) { timedOut in
55 | if timedOut {
56 | reject(self.ERROR_INIT, "SDK initialization timed out", nil)
57 | } else {
58 | resolve(nil)
59 | }
60 | }
61 | } else {
62 |
63 | LDClient.start(config: config, context: try contextBuild(context), completion: {() -> Void in
64 | resolve(nil)
65 | })
66 |
67 | }
68 | }
69 | catch {
70 | NSLog("LDClient init failed: \(error)")
71 | }
72 | }
73 | }
74 |
75 | private func id(_ x: T) -> T { x }
76 | private func millis(_ x: NSNumber) -> TimeInterval { TimeInterval(x.doubleValue / 1_000) }
77 | private func url(_ x: String) -> URL { URL.init(string: x)! }
78 | private func configField(_ field: inout T, _ value: Any?, _ transform: ((V) -> T?)) {
79 | if let val = value as? V, let res = transform(val) {
80 | field = res
81 | }
82 | }
83 |
84 | internal func configBuild(config: NSDictionary) -> LDConfig? {
85 | guard let mobileKey = config["mobileKey"] as? String
86 | else { return nil }
87 |
88 | var enableAutoEnvAttributes: Bool = false
89 | configField(&enableAutoEnvAttributes, config["enableAutoEnvAttributes"], { $0 })
90 |
91 | var ldConfig = LDConfig(mobileKey: mobileKey, autoEnvAttributes: enableAutoEnvAttributes ? .enabled : .disabled)
92 | configField(&ldConfig.baseUrl, config["pollUrl"], url)
93 | configField(&ldConfig.eventsUrl, config["eventsUrl"], url)
94 | configField(&ldConfig.streamUrl, config["streamUrl"], url)
95 | configField(&ldConfig.eventCapacity, config["eventCapacity"], { (x: NSNumber) in x.intValue })
96 | configField(&ldConfig.eventFlushInterval, config["flushInterval"], millis)
97 | configField(&ldConfig.connectionTimeout, config["connectionTimeout"], millis)
98 | configField(&ldConfig.flagPollingInterval, config["pollingInterval"], millis)
99 | configField(&ldConfig.backgroundFlagPollingInterval, config["backgroundPollingInterval"], millis)
100 | configField(&ldConfig.useReport, config["useReport"], id)
101 | configField(&ldConfig.streamingMode, config["stream"], { $0 ? .streaming : .polling })
102 | configField(&ldConfig.enableBackgroundUpdates, config["disableBackgroundUpdating"], { !$0 })
103 | configField(&ldConfig.startOnline, config["offline"], { !$0 })
104 | configField(&ldConfig.isDebugMode, config["debugMode"], id)
105 | configField(&ldConfig.evaluationReasons, config["evaluationReasons"], id)
106 | configField(&ldConfig.wrapperName, config["wrapperName"], id)
107 | configField(&ldConfig.wrapperVersion, config["wrapperVersion"], id)
108 | configField(&ldConfig.maxCachedContexts, config["maxCachedContexts"], { (x: NSNumber) in x.intValue })
109 | configField(&ldConfig.diagnosticOptOut, config["diagnosticOptOut"], id)
110 | configField(&ldConfig.diagnosticRecordingInterval, config["diagnosticRecordingInterval"], millis)
111 | configField(&ldConfig.allContextAttributesPrivate, config["allAttributesPrivate"], id)
112 | configField(&ldConfig.privateContextAttributes, config["privateAttributes"], { (x: [String]) in x.map { Reference($0) }})
113 |
114 | if let val = config["secondaryMobileKeys"] as? [String: String] {
115 | try! ldConfig.setSecondaryMobileKeys(val)
116 | }
117 |
118 | if let c = config["application"] as? [String: String] {
119 | var applicationInfo = ApplicationInfo()
120 |
121 | if let applicationId = c["id"], !applicationId.isEmpty {
122 | applicationInfo.applicationIdentifier(applicationId)
123 | }
124 |
125 | if let applicationVersion = c["version"], !applicationVersion.isEmpty {
126 | applicationInfo.applicationVersion(applicationVersion)
127 | }
128 |
129 | if let name = c["name"], !name.isEmpty {
130 | applicationInfo.applicationName(name)
131 | }
132 |
133 | if let versionName = c["versionName"], !versionName.isEmpty {
134 | applicationInfo.applicationVersionName(versionName)
135 | }
136 |
137 | ldConfig.applicationInfo = applicationInfo
138 | }
139 |
140 | return ldConfig
141 | }
142 |
143 | internal func createSingleContext(_ contextDict: NSDictionary, _ kind: String) throws -> LDContext {
144 | var b = LDContextBuilder()
145 | b.kind(kind)
146 |
147 | if let key = contextDict["key"] as? String {
148 | b.key(key)
149 | }
150 |
151 | if let meta = contextDict["_meta"] as? NSDictionary {
152 | if let privateAttributes = meta["privateAttributes"] as? [String] {
153 | privateAttributes.forEach {
154 | b.addPrivateAttribute(Reference($0))
155 | }
156 | }
157 | }
158 |
159 | // set name, anonymous and arbitrary attributes
160 | for (k, value) in contextDict as! [String: Any] {
161 | if (k != "kind" && k != "key" && k != "_meta") {
162 | b.trySetValue(k, LDValue.fromBridge(value))
163 |
164 | }
165 | }
166 |
167 | return try b.build().get()
168 | }
169 |
170 | internal func contextBuild(_ contextDict: NSDictionary) throws -> LDContext {
171 | let kind = contextDict["kind"] as! String
172 |
173 | if (kind == "multi") {
174 | var b = LDMultiContextBuilder()
175 |
176 | try contextDict.allKeys.forEach {
177 | let kk = $0 as! String
178 | if (kk != "kind") {
179 | let v = contextDict[kk] as! NSDictionary
180 | b.addContext(try createSingleContext(v, kk))
181 | }
182 | }
183 |
184 | let c = try b.build().get()
185 | return c
186 | } else {
187 | let c = try createSingleContext(contextDict, kind)
188 | return c
189 | }
190 | }
191 |
192 | @objc func boolVariation(_ flagKey: String, defaultValue: ObjCBool, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
193 | if let ldClient = getLDClient(environment: environment) {
194 | resolve(ldClient.boolVariation(forKey: flagKey, defaultValue: defaultValue.boolValue))
195 | } else {
196 | NSLog("%@", "evaluation failed because LDClient is nil. Returning default value.")
197 | resolve(defaultValue.boolValue)
198 | }
199 | }
200 |
201 | @objc func numberVariation(_ flagKey: String, defaultValue: Double, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
202 | if let ldClient = getLDClient(environment: environment) {
203 | resolve(ldClient.doubleVariation(forKey: flagKey, defaultValue: defaultValue))
204 | } else {
205 | NSLog("%@", "evaluation failed because LDClient is nil. Returning default value.")
206 | resolve(defaultValue)
207 | }
208 | }
209 |
210 | @objc func stringVariation(_ flagKey: String, defaultValue: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
211 | if let ldClient = getLDClient(environment: environment) {
212 | resolve(ldClient.stringVariation(forKey: flagKey, defaultValue: defaultValue))
213 | } else {
214 | NSLog("%@", "evaluation failed because LDClient is nil. Returning default value.")
215 | resolve(defaultValue)
216 | }
217 | }
218 |
219 | @objc func jsonVariation(_ flagKey: String, defaultValue: Any, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
220 | if let ldClient = getLDClient(environment: environment) {
221 | resolve(ldClient.jsonVariation(forKey: flagKey, defaultValue: LDValue.fromBridge(defaultValue)).toBridge())
222 | } else {
223 | NSLog("%@", "evaluation failed because LDClient is nil. Returning default value.")
224 | resolve(LDValue.fromBridge(defaultValue))
225 | }
226 | }
227 |
228 | @objc func boolVariationDetail(_ flagKey: String, defaultValue: ObjCBool, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
229 | let detail = LDClient.get(environment: environment)?.boolVariationDetail(forKey: flagKey, defaultValue: defaultValue.boolValue)
230 | resolve(bridgeDetail(detail, id, defaultValue.boolValue))
231 | }
232 |
233 | @objc func numberVariationDetail(_ flagKey: String, defaultValue: Double, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
234 | let detail = getLDClient(environment: environment)?.doubleVariationDetail(forKey: flagKey, defaultValue: defaultValue)
235 | resolve(bridgeDetail(detail, id, defaultValue))
236 | }
237 |
238 | @objc func stringVariationDetail(_ flagKey: String, defaultValue: String, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
239 | let detail = getLDClient(environment: environment)?.stringVariationDetail(forKey: flagKey, defaultValue: defaultValue)
240 | resolve(bridgeDetail(detail, id, defaultValue))
241 | }
242 |
243 | @objc func jsonVariationDetail(_ flagKey: String, defaultValue: Any, environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
244 | let detail = getLDClient(environment: environment)?.jsonVariationDetail(forKey: flagKey, defaultValue: LDValue.fromBridge(defaultValue))
245 | resolve(bridgeDetail(detail, { $0.toBridge() }, LDValue.fromBridge(defaultValue)))
246 | }
247 |
248 | private func bridgeDetail(_ detail: LDEvaluationDetail? = nil, _ converter: ((T) -> Any), _ defaultValue: T) -> NSDictionary {
249 | if let detail = detail {
250 | return [ "value": converter(detail.value)
251 | , "variationIndex": (detail.variationIndex as Any)
252 | , "reason": ((detail.reason?.mapValues { $0.toBridge() }) as Any)
253 | ]
254 | }
255 |
256 | NSLog("%@", "WARNING: evaluation failed because LDClient is nil")
257 | return [ "value": converter(defaultValue)
258 | , "reason": ["error": LDValue(stringLiteral: "evaluation failed because LDClient is nil. Returning default value.")]
259 | ]
260 | }
261 |
262 | @objc func trackData(_ eventName: String, data: Any, environment: String) {
263 | getLDClient(environment: environment)?.track(key: eventName, data: LDValue.fromBridge(data))
264 | }
265 |
266 | @objc func trackMetricValue(_ eventName: String, data: Any, metricValue: NSNumber, environment: String) {
267 | getLDClient(environment: environment)?.track(key: eventName, data: LDValue.fromBridge(data), metricValue: Double(truncating: metricValue))
268 | }
269 |
270 | @objc func setOffline(_ resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
271 | LDClient.get()?.setOnline(false) {
272 | resolve(true)
273 | }
274 | }
275 |
276 | @objc func isOffline(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
277 | resolve(!(LDClient.get()?.isOnline ?? false))
278 | }
279 |
280 | @objc func setOnline(_ resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
281 | LDClient.get()?.setOnline(true) {
282 | resolve(true)
283 | }
284 | }
285 |
286 | @objc func flush() {
287 | LDClient.get()?.flush()
288 | }
289 |
290 | @objc func close(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
291 | LDClient.get()?.close()
292 | resolve(true)
293 | }
294 |
295 | @objc func identify(_ context: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
296 | do {
297 | LDClient.get()?.identify(context: try contextBuild(context)) {
298 | resolve(nil)
299 | }
300 |
301 | }
302 | catch {
303 | NSLog("LDClient identify failed: \(error)")
304 | }
305 | }
306 |
307 | @objc func allFlags(_ environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
308 | resolve(getLDClient(environment: environment)?.allFlags?.mapValues { $0.toBridge() } ?? [:] as NSDictionary)
309 | }
310 |
311 | @objc func getConnectionMode(_ environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
312 | if let connectionInformation = getLDClient(environment: environment)?.getConnectionInformation() {
313 | var connectionMode: String
314 | switch connectionInformation.currentConnectionMode {
315 | case .streaming:
316 | connectionMode = "STREAMING"
317 | case .polling:
318 | connectionMode = "POLLING"
319 | case .offline:
320 | connectionMode = "OFFLINE"
321 | case .establishingStreamingConnection:
322 | connectionMode = "ESTABLISHING_STREAMING_CONNECTION"
323 | }
324 | resolve(connectionMode)
325 | } else {
326 | resolve(nil)
327 | }
328 | }
329 |
330 | // lastKnownFlagValidity is nil if either no connection has ever been successfully made or if the SDK has an active streaming connection. It will have a value if 1) in polling mode and at least one poll has completed successfully, or 2) if in streaming mode whenever the streaming connection closes.
331 | @objc func getLastSuccessfulConnection(_ environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
332 | resolve(getLDClient(environment: environment)?.getConnectionInformation().lastKnownFlagValidity ?? 0)
333 | }
334 |
335 | @objc func getLastFailedConnection(_ environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
336 | resolve(getLDClient(environment: environment)?.getConnectionInformation().lastFailedConnection ?? 0)
337 | }
338 |
339 | @objc func getLastFailure(_ environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
340 | if let connectionInformation = getLDClient(environment: environment)?.getConnectionInformation() {
341 | var failureReason: String
342 | switch connectionInformation.lastConnectionFailureReason {
343 | case .unauthorized:
344 | failureReason = "UNAUTHORIZED"
345 | case .none:
346 | failureReason = "NONE"
347 | case .httpError:
348 | failureReason = "HTTP_ERROR"
349 | case .unknownError:
350 | failureReason = "UNKNOWN_ERROR"
351 | }
352 | resolve(failureReason)
353 | } else {
354 | resolve(nil)
355 | }
356 | }
357 |
358 | private func envConcat(environment: String, identifier: String) -> String {
359 | return environment + ";" + identifier
360 | }
361 |
362 | @objc func registerFeatureFlagListener(_ flagKey: String, environment: String) {
363 | let multiListenerId = envConcat(environment: environment, identifier: flagKey)
364 | let owner = ObserverOwner()
365 | flagListenerOwners[multiListenerId] = owner
366 | getLDClient(environment: environment)?.observe(key: flagKey, owner: owner) { changedFlag in
367 | if self.bridge != nil {
368 | self.sendEvent(withName: self.FLAG_PREFIX, body: ["flagKey": changedFlag.key, "listenerId": multiListenerId])
369 | }
370 | }
371 | }
372 |
373 | @objc func unregisterFeatureFlagListener(_ flagKey: String, environment: String) {
374 | let multiListenerId = envConcat(environment: environment, identifier: flagKey)
375 | if let owner = flagListenerOwners.removeValue(forKey: multiListenerId) {
376 | getLDClient(environment: environment)?.stopObserving(owner: owner)
377 | }
378 | }
379 |
380 | @objc func registerCurrentConnectionModeListener(_ listenerId: String, environment: String) {
381 | let multiListenerId = envConcat(environment: environment, identifier: listenerId)
382 | let owner = ObserverOwner()
383 | connectionModeListenerOwners[multiListenerId] = owner
384 | getLDClient(environment: environment)?.observeCurrentConnectionMode(owner: owner) { connectionMode in
385 | if self.bridge != nil {
386 | self.sendEvent(withName: self.CONNECTION_MODE_PREFIX, body: ["connectionMode": connectionMode, "listenerId": multiListenerId])
387 | }
388 | }
389 | }
390 |
391 | @objc func unregisterCurrentConnectionModeListener(_ listenerId: String, environment: String) {
392 | let multiListenerId = envConcat(environment: environment, identifier: listenerId)
393 | if let owner = connectionModeListenerOwners.removeValue(forKey: multiListenerId) {
394 | getLDClient(environment: environment)?.stopObserving(owner: owner)
395 | }
396 | }
397 |
398 | @objc func registerAllFlagsListener(_ listenerId: String, environment: String) {
399 | let multiListenerId = envConcat(environment: environment, identifier: listenerId)
400 | let owner = ObserverOwner()
401 | allFlagsListenerOwners[multiListenerId] = owner
402 | getLDClient(environment: environment)?.observeAll(owner: owner) { changedFlags in
403 | if self.bridge != nil {
404 | self.sendEvent(withName: self.ALL_FLAGS_PREFIX, body: ["flagKeys": Array(changedFlags.keys), "listenerId": multiListenerId])
405 | }
406 | }
407 | }
408 |
409 | @objc func unregisterAllFlagsListener(_ listenerId: String, environment: String) {
410 | let multiListenerId = envConcat(environment: environment, identifier: listenerId)
411 | if let owner = allFlagsListenerOwners.removeValue(forKey: multiListenerId) {
412 | getLDClient(environment: environment)?.stopObserving(owner: owner)
413 | }
414 | }
415 |
416 | @objc func isInitialized(_ environment: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
417 | if LDClient.get() == nil {
418 | reject(ERROR_UNKNOWN, "SDK has not been configured", nil)
419 | } else if let client = LDClient.get(environment: environment) {
420 | resolve(client.isInitialized)
421 | } else {
422 | reject(ERROR_UNKNOWN, "SDK not configured with requested environment", nil)
423 | }
424 | }
425 | }
426 |
427 | class ObserverOwner{}
428 |
429 | extension LDValue {
430 | static func fromBridge(_ value: Any) -> LDValue {
431 | guard !(value is NSNull)
432 | else { return .null }
433 | if let nsNumValue = value as? NSNumber {
434 | // Because we accept `LDValue` in contexts that can receive anything, the value is a
435 | // reference type in Objective-C. Because of that, RN bridges the type as a `NSNumber`,
436 | // so we must determine whether that `NSNumber` was originally created from a `BOOL`.
437 | // Adapted from https://stackoverflow.com/a/30223989
438 | let boolTypeId = CFBooleanGetTypeID()
439 | if CFGetTypeID(nsNumValue) == boolTypeId {
440 | return .bool(nsNumValue.boolValue)
441 | } else {
442 | return .number(Double(truncating: nsNumValue))
443 | }
444 | }
445 | if let stringValue = value as? String { return .string(stringValue) }
446 | if let arrayValue = value as? [Any] { return .array(arrayValue.map { fromBridge($0) }) }
447 | if let dictValue = value as? [String: Any] { return .object(dictValue.mapValues { fromBridge($0) }) }
448 | return .null
449 | }
450 |
451 | func toBridge() -> Any {
452 | switch self {
453 | case .null: return NSNull()
454 | case .bool(let boolValue): return boolValue
455 | case .number(let numValue): return numValue
456 | case .string(let stringValue): return stringValue
457 | case .array(let arrayValue): return arrayValue.map { $0.toBridge() }
458 | case .object(let objectValue): return objectValue.mapValues { $0.toBridge() }
459 | }
460 | }
461 | }
462 |
--------------------------------------------------------------------------------