├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── example ├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── src │ ├── client │ │ └── index.js │ ├── server │ │ ├── index.js │ │ └── server.js │ └── universal │ │ ├── app.js │ │ ├── contact.js │ │ └── home.js ├── webpack.config.client.js ├── webpack.config.server.js └── yarn.lock ├── jest.config.js ├── lib ├── context.js ├── index.js ├── initLDClient.js ├── initUser.js ├── withFlagProvider.js └── withFlags.js ├── package.json ├── src ├── context.js ├── index.js ├── initLDClient.js ├── initUser.js ├── withFlagProvider.js └── withFlags.js ├── test └── setup.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties", 5 | "@babel/plugin-transform-async-to-generator", 6 | "@babel/plugin-transform-runtime" 7 | ] 8 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "allowImportExportEverywhere": true 5 | }, 6 | "extends": [ 7 | "airbnb", 8 | "eslint:recommended" 9 | ], 10 | "plugins": [ 11 | "babel" 12 | ], 13 | "rules": { 14 | "arrow-parens": 0, 15 | "eol-last": 0, 16 | "global-require": 0, 17 | "arrow-body-style": 0, 18 | "consistent-return": 0, 19 | "no-unneeded-ternary": 0, 20 | "max-len": 0, 21 | "no-param-reassign": 2, 22 | "new-cap": 0, 23 | "no-console": 0, 24 | "object-curly-spacing": 0, 25 | "spaced-comment": 0, 26 | "import/first": 0, 27 | "import/no-extraneous-dependencies": 0, 28 | "import/prefer-default-export": 0, 29 | "import/no-mutable-exports": 0, 30 | "import/no-named-as-default": 0, 31 | "no-trailing-spaces": 0, 32 | "no-underscore-dangle": 0, 33 | "no-use-before-define": 0, 34 | "no-duplicate-imports": 0, 35 | "import/no-duplicates": 1, 36 | "no-useless-escape": 0, 37 | "no-unused-expressions": [1 , {"allowTernary": true}], 38 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 39 | "react/sort-comp": 0 40 | }, 41 | "env": { 42 | "jest": true, 43 | "node": true 44 | }, 45 | "globals": { 46 | "jest": true, 47 | "td": true 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .eslintcache -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yusinto Ngadiman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ld-react 2 | 3 | [![npm version](https://img.shields.io/npm/v/ld-react.svg?style=flat-square)](https://www.npmjs.com/package/ld-react) [![npm downloads](https://img.shields.io/npm/dm/ld-react.svg?style=flat-square)](https://www.npmjs.com/package/ld-react) [![npm](https://img.shields.io/npm/dt/ld-react.svg?style=flat-square)](https://www.npmjs.com/package/ld-react) [![npm](https://img.shields.io/npm/l/ld-react.svg?style=flat-square)](https://www.npmjs.com/package/ld-react) 4 | 5 | This package has been superseded by the official [LaunchDarkly React SDK](https://github.com/launchdarkly/react-client-sdk). Please use that instead. 6 | 7 | > **The quickest and easiest way to integrate launch darkly with react** :tada: 8 | 9 | Why this package? 10 | * Easy and fast to use. Two steps to get feature flags into your react app. 11 | * Supports subscription out of the box. You get live changes on the client as you toggle features. 12 | * You automatically get camelCased keys as opposed to the default kebab-cased. 13 | * No need for redux! This package uses the new context api which is available from react ^16.3.0. 14 | 15 | ## Dependency 16 | 17 | This needs react ^16.4.0! It won't work otherwise. 18 | 19 | ## Installation 20 | 21 | yarn add ld-react 22 | 23 | ## Quickstart 24 | 25 | 1. Wrap your root app `withFlagProvider`: 26 | 27 | ```js 28 | import {withFlagProvider} from 'ld-react'; 29 | 30 | const App = () => 31 |
32 | 33 |
; 34 | 35 | export default withFlagProvider(App, {clientSideId: 'your-client-side-id'}); 36 | ``` 37 | 38 | 2. Wrap your component `withFlags` to get them via props: 39 | 40 | ```js 41 | import {withFlags} from 'ld-react'; 42 | 43 | const Home = props => { 44 | // flags are available via props.flags 45 | return props.flags.devTestFlag ?
Flag on
:
Flag off
; 46 | }; 47 | 48 | export default withFlags(Home); 49 | ``` 50 | 51 | That's it! 52 | 53 | ## API 54 | ### withFlagProvider(Component, {clientSideId, user, options}) 55 | This is a hoc which accepts a component and a config object with the above properties. 56 | `Component` and `clientSideId` are mandatory. 57 | 58 | For example: 59 | 60 | ```javascript 61 | import {withFlagProvider} from 'ld-react'; 62 | 63 | const App = () => 64 |
65 | 66 |
; 67 | 68 | export default withFlagProvider(App, {clientSideId: 'your-client-side-id'}); 69 | ``` 70 | 71 | The `user` property is optional. You can initialise the sdk with a custom user by specifying one. This must be an object containing 72 | at least a "key" property. If you don't specify a user object, ld-react will create a default one that looks like this: 73 | 74 | ```javascript 75 | const defaultUser = { 76 | key: uuid.v4(), // random guid 77 | ip: ip.address(), 78 | custom: { 79 | browser: userAgentParser.getResult().browser.name, 80 | device 81 | } 82 | }; 83 | ``` 84 | 85 | For more info on the user object, see [here](http://docs.launchdarkly.com/docs/js-sdk-reference#section-users). 86 | 87 | The `options` property is optional. It can be used to pass in extra options such as [Bootstrapping](https://github.com/launchdarkly/js-client#bootstrapping). 88 | For example: 89 | 90 | ```javascript 91 | withFlagProvider(Component, { 92 | clientSideId, 93 | options: { 94 | bootstrap: 'localStorage', 95 | }, 96 | }); 97 | ``` 98 | 99 | ### withFlags(Component) 100 | This is a hoc which passes all your flags to the specified component via props. Your flags will be available 101 | as camelCased properties under `this.props.flags`. For example: 102 | 103 | ```js 104 | import {withFlags} from 'ld-react'; 105 | 106 | class Home extends Component { 107 | render() { 108 | return ( 109 |
110 | { 111 | this.props.flags.devTestFlag ? // Look ma, feature flag! 112 |
Flag on
113 | : 114 |
Flag off
115 | } 116 |
117 | ); 118 | } 119 | } 120 | 121 | export default withFlags(Home); 122 | ``` 123 | 124 | ### ldClient 125 | Internally the ld-react initialises the ldclient-js sdk and stores a reference to the resultant ldClient object in memory. 126 | You can use this object to access the [official sdk methods](https://github.com/launchdarkly/js-client) directly. 127 | For example, you can do things like: 128 | 129 | ```js 130 | import {ldClient} from 'ld-react'; 131 | 132 | class Home extends Component { 133 | 134 | // track goals 135 | onAddToCard = () => ldClient.track('add to cart'); 136 | 137 | // change user context 138 | onLoginSuccessful = () => ldClient.identify({key: 'someUserId'}); 139 | 140 | // ... other implementation 141 | } 142 | ``` 143 | 144 | For more info on changing user context, see the [official documentation](http://docs.launchdarkly.com/docs/js-sdk-reference#section-changing-the-user-context). 145 | 146 | ## Example 147 | Check the [example](https://github.com/yusinto/ld-react/tree/master/example) for a fully working spa with 148 | react and react-router. Remember to enter your client side sdk in the client [root app file](https://github.com/yusinto/ld-react/blob/master/example/src/universal/app.js) 149 | and create a test flag called `dev-test-flag` before running the example! 150 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-transform-async-to-generator" 9 | ] 10 | } -------------------------------------------------------------------------------- /example/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "allowImportExportEverywhere": true 5 | }, 6 | "extends": [ 7 | "airbnb", 8 | "eslint:recommended", 9 | "plugin:react/recommended" 10 | ], 11 | "plugins": [ 12 | "babel" 13 | ], 14 | "rules": { 15 | "arrow-parens": 0, 16 | "eol-last": 0, 17 | "global-require": 0, 18 | "arrow-body-style": 0, 19 | "consistent-return": 0, 20 | "no-unneeded-ternary": 0, 21 | "max-len": 0, 22 | "no-param-reassign": 2, 23 | "new-cap": 0, 24 | "no-console": 0, 25 | "object-curly-spacing": 0, 26 | "spaced-comment": 0, 27 | "import/no-extraneous-dependencies": 0, 28 | "import/first": 0, 29 | "import/prefer-default-export": 0, 30 | "import/no-mutable-exports": 0, 31 | "import/no-named-as-default": 0, 32 | "react/jsx-filename-extension": 0, 33 | "react/jsx-indent": 0, 34 | "react/jsx-indent-props": 0, 35 | "react/jsx-space-before-closing": 0, 36 | "react/jsx-first-prop-new-line": 0, 37 | "react/prefer-stateless-function": 0, 38 | "react/jsx-closing-bracket-location": 0, 39 | "react/require-extension": 0, 40 | "react/sort-comp": 0, 41 | "react/jsx-wrap-multilines": 0, 42 | "react/jsx-no-bind": 0, 43 | "react/jsx-users-react": 0, 44 | "react/jsx-tag-spacing": 0, 45 | "jsx-a11y/anchor-is-valid": 0, 46 | "jsx-a11y/img-has-alt": 0, 47 | "no-trailing-spaces": 0, 48 | "no-underscore-dangle": 0, 49 | "no-use-before-define": 0, 50 | "no-duplicate-imports": 0, 51 | "import/no-duplicates": 1, 52 | "no-useless-escape": 0, 53 | "no-unused-expressions": [1 , {"allowTernary": true}] 54 | }, 55 | "env": { 56 | "browser": true, 57 | "jest": true, 58 | "node": true 59 | }, 60 | "globals": { 61 | "React": true, 62 | "fetch": true, 63 | "jest": true 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | dist 5 | .eslintcache -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # ld-react 2 | 3 | A simple spa demonstrating ld-react. 4 | 5 | yarn && yarn start 6 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ld-react-example", 3 | "version": "1.1.0", 4 | "description": "Example usage of ld-react", 5 | "main": "src/server/index.js", 6 | "scripts": { 7 | "start": "node src/server/index.js", 8 | "lint": "eslint ./src", 9 | "serve": "webpack-serve webpack.config.server" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/yusinto/ld-react.git" 14 | }, 15 | "keywords": [ 16 | "universal", 17 | "hot", 18 | "reload", 19 | "client", 20 | "server", 21 | "webpack", 22 | "bundle" 23 | ], 24 | "author": "Yus Ng", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/yusinto/ld-react" 28 | }, 29 | "homepage": "https://github.com/yusinto/ld-react", 30 | "dependencies": { 31 | "@babel/plugin-transform-async-to-generator": "^7.2.0", 32 | "@babel/polyfill": "^7.0.0", 33 | "express": "^4.16.4", 34 | "ld-react": "file:../lib", 35 | "lodash": "^4.17.11", 36 | "prop-types": "^15.6.2", 37 | "react": "^16.6.3", 38 | "react-dom": "^16.6.3", 39 | "react-router-dom": "^4.3.1" 40 | }, 41 | "devDependencies": { 42 | "@babel/cli": "^7.2.0", 43 | "@babel/core": "^7.2.0", 44 | "@babel/plugin-proposal-class-properties": "^7.2.1", 45 | "@babel/preset-env": "^7.2.0", 46 | "@babel/preset-react": "^7.0.0", 47 | "babel-eslint": "^10.0.1", 48 | "babel-loader": "^8.0.4", 49 | "eslint": "^5.10.0", 50 | "eslint-config-airbnb": "^17.1.0", 51 | "eslint-plugin-import": "^2.14.0", 52 | "eslint-plugin-jsx-a11y": "^6.1.2", 53 | "eslint-plugin-react": "^7.11.1", 54 | "universal-hot-reload": "^2.0.1", 55 | "webpack": "^4.27.1", 56 | "webpack-cli": "^3.1.2", 57 | "webpack-node-externals": "^1.7.2", 58 | "webpack-serve": "^2.0.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /example/src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {hydrate} from 'react-dom'; 3 | import {BrowserRouter} from 'react-router-dom'; 4 | import App from '../universal/app'; 5 | 6 | hydrate( 7 | 8 | 9 | , 10 | document.getElementById('reactDiv'), 11 | ); -------------------------------------------------------------------------------- /example/src/server/index.js: -------------------------------------------------------------------------------- 1 | const UniversalHotReload = require('universal-hot-reload').default; 2 | UniversalHotReload(require('../../webpack.config.server.js'), require('../../webpack.config.client.js')); -------------------------------------------------------------------------------- /example/src/server/server.js: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import React from 'react'; 3 | import {renderToString} from 'react-dom/server'; 4 | import {StaticRouter} from 'react-router-dom'; 5 | import App from '../universal/app'; 6 | 7 | const PORT = 3000; 8 | const app = Express(); 9 | 10 | app.use('/dist', Express.static('dist', {maxAge: '1d'})); 11 | 12 | app.use((req, res) => { 13 | const html = ` 14 | 15 | 16 | 17 | 18 | ld-react example 19 | 20 | 21 |
${renderToString( 22 | 25 | 26 | 27 | )}
28 | 29 | 30 | `; 31 | 32 | res.end(html); 33 | }); 34 | 35 | export default app.listen(PORT, () => { 36 | console.log(`Example app listening at ${PORT}...`); 37 | }); 38 | -------------------------------------------------------------------------------- /example/src/universal/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Switch, Link, Route, Redirect} from 'react-router-dom'; 3 | import Home from './home'; 4 | import Contact from './contact'; 5 | import {withFlagProvider} from 'ld-react'; 6 | 7 | const App = () => 8 |
9 |
10 | 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
; 27 | 28 | // Set clientSideId to your own Client-side ID. You can find this in 29 | // the dashboard under Account settings / Projects 30 | export default withFlagProvider(App, {clientSideId: '59b2b2596d1a250b1c78baa3'}); -------------------------------------------------------------------------------- /example/src/universal/contact.js: -------------------------------------------------------------------------------- 1 | import React, {Component, Timeout} from 'react'; 2 | 3 | export default props => 4 |
5 |

This is the contact page

6 |

7 | Check out my blog at reactjunkie.com 9 |

10 |
; 11 | -------------------------------------------------------------------------------- /example/src/universal/home.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {withFlags} from 'ld-react'; 3 | 4 | class Home extends Component { 5 | state = {randomNumber: 0}; 6 | 7 | onClickGenerateRandom = () => { 8 | const min = 1; 9 | const max = 100; 10 | const randomNumber = Math.floor(Math.random() * (max - min)) + min; 11 | this.setState({randomNumber}); 12 | }; 13 | 14 | render() { 15 | return ( 16 |
17 |

Welcome to ld-react example

18 |
19 | To run this example: 20 | 24 |
25 | { 26 | this.props.flags.devTestFlag ? 27 |
28 |

29 | SSE works! If you turn off your flag in launch darkly, your app will respond without a browser refresh. 30 | Try it! 31 |

32 | 33 |

{this.state.randomNumber}

34 |
35 | : 36 |
37 | The random number generator is turned off. Go to your launch darkly dashboard to turn it on. 38 |
39 | } 40 |
41 | ); 42 | } 43 | } 44 | 45 | export default withFlags(Home); -------------------------------------------------------------------------------- /example/webpack.config.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const WebpackServeUrl = 'http://localhost:3002'; 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'source-map', 8 | entry: ['@babel/polyfill', './src/client/index'], 9 | output: { 10 | path: path.resolve('dist'), 11 | publicPath: `${WebpackServeUrl}/dist/`, // MUST BE FULL PATH! 12 | filename: 'bundle.js', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.jsx?$/, 18 | include: path.resolve('src'), 19 | exclude: /node_modules/, 20 | loader: 'babel-loader', 21 | options: { 22 | cacheDirectory: true, 23 | }, 24 | }], 25 | }, 26 | }; -------------------------------------------------------------------------------- /example/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | devtool: 'source-map', 7 | entry: ['@babel/polyfill', './src/server/server.js'], // set this to your server entry point. This should be where you start your express server with .listen() 8 | target: 'node', // tell webpack this bundle will be used in nodejs environment. 9 | externals: [nodeExternals()], // Omit node_modules code from the bundle. You don't want and don't need them in the bundle. 10 | output: { 11 | path: path.resolve('dist'), 12 | filename: 'serverBundle.js', 13 | libraryTarget: 'commonjs2', // IMPORTANT! Add module.exports to the beginning of the bundle, so universal-hot-reload can access your app. 14 | }, 15 | // The rest of the config is pretty standard and can contain other webpack stuff you need. 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.jsx?$/, 20 | include: path.resolve('src'), 21 | exclude: /node_modules/, 22 | loader: 'babel-loader', 23 | options: { 24 | cacheDirectory: true, 25 | }, 26 | }], 27 | }, 28 | }; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['./test/setup.js'], 3 | }; -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.Consumer = exports.Provider = void 0; 7 | 8 | var _react = require("react"); 9 | 10 | var _createContext = (0, _react.createContext)(), 11 | Provider = _createContext.Provider, 12 | Consumer = _createContext.Consumer; // TODO: workout the default values for the context 13 | 14 | 15 | exports.Consumer = Consumer; 16 | exports.Provider = Provider; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | Object.defineProperty(exports, "withFlagProvider", { 9 | enumerable: true, 10 | get: function get() { 11 | return _withFlagProvider.default; 12 | } 13 | }); 14 | Object.defineProperty(exports, "withFlags", { 15 | enumerable: true, 16 | get: function get() { 17 | return _withFlags.default; 18 | } 19 | }); 20 | Object.defineProperty(exports, "ldClient", { 21 | enumerable: true, 22 | get: function get() { 23 | return _initLDClient.ldClient; 24 | } 25 | }); 26 | 27 | require("@babel/polyfill"); 28 | 29 | var _withFlagProvider = _interopRequireDefault(require("./withFlagProvider")); 30 | 31 | var _withFlags = _interopRequireDefault(require("./withFlags")); 32 | 33 | var _initLDClient = require("./initLDClient"); -------------------------------------------------------------------------------- /lib/initLDClient.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.default = exports.initLDClient = exports.ldClient = void 0; 9 | 10 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 11 | 12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 13 | 14 | var _lodash = _interopRequireDefault(require("lodash.camelcase")); 15 | 16 | var _ldclientJs = require("ldclient-js"); 17 | 18 | var _initUser = _interopRequireDefault(require("./initUser")); 19 | 20 | var ldClient; 21 | exports.ldClient = ldClient; 22 | 23 | var initLDClient = 24 | /*#__PURE__*/ 25 | function () { 26 | var _ref = (0, _asyncToGenerator2.default)( 27 | /*#__PURE__*/ 28 | _regenerator.default.mark(function _callee(clientSideId) { 29 | var user, 30 | options, 31 | _args = arguments; 32 | return _regenerator.default.wrap(function _callee$(_context) { 33 | while (1) { 34 | switch (_context.prev = _context.next) { 35 | case 0: 36 | user = _args.length > 1 && _args[1] !== undefined ? _args[1] : (0, _initUser.default)(); 37 | options = _args.length > 2 ? _args[2] : undefined; 38 | exports.ldClient = ldClient = (0, _ldclientJs.initialize)(clientSideId, user, options); 39 | _context.next = 5; 40 | return new Promise(function (resolve) { 41 | return ldClient.on('ready', function () { 42 | var rawFlags = ldClient.allFlags(); 43 | var flags = {}; 44 | 45 | for (var rawFlag in rawFlags) { 46 | var camelCasedKey = (0, _lodash.default)(rawFlag); 47 | flags[camelCasedKey] = rawFlags[rawFlag]; 48 | } 49 | 50 | resolve(flags); 51 | }); 52 | }); 53 | 54 | case 5: 55 | return _context.abrupt("return", _context.sent); 56 | 57 | case 6: 58 | case "end": 59 | return _context.stop(); 60 | } 61 | } 62 | }, _callee, this); 63 | })); 64 | 65 | return function initLDClient(_x) { 66 | return _ref.apply(this, arguments); 67 | }; 68 | }(); 69 | 70 | exports.initLDClient = initLDClient; 71 | var _default = initLDClient; 72 | exports.default = _default; -------------------------------------------------------------------------------- /lib/initUser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.default = void 0; 9 | 10 | var _uuid = _interopRequireDefault(require("uuid")); 11 | 12 | var _ip = _interopRequireDefault(require("ip")); 13 | 14 | var _uaParserJs = _interopRequireDefault(require("ua-parser-js")); 15 | 16 | var userAgentParser = new _uaParserJs.default(); 17 | var isMobileDevice = typeof window !== 'undefined' && userAgentParser.getDevice().type === 'mobile'; 18 | var isTabletDevice = typeof window !== 'undefined' && userAgentParser.getDevice().type === 'tablet'; 19 | 20 | var _default = function _default() { 21 | var device; 22 | 23 | if (isMobileDevice) { 24 | device = 'mobile'; 25 | } else if (isTabletDevice) { 26 | device = 'tablet'; 27 | } else { 28 | device = 'desktop'; 29 | } 30 | 31 | return { 32 | key: _uuid.default.v4(), 33 | ip: _ip.default.address(), 34 | custom: { 35 | browser: userAgentParser.getResult().browser.name, 36 | device: device 37 | } 38 | }; 39 | }; 40 | 41 | exports.default = _default; -------------------------------------------------------------------------------- /lib/withFlagProvider.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); 4 | 5 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 6 | 7 | Object.defineProperty(exports, "__esModule", { 8 | value: true 9 | }); 10 | exports.default = void 0; 11 | 12 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 13 | 14 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 15 | 16 | var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread")); 17 | 18 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); 19 | 20 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); 21 | 22 | var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn")); 23 | 24 | var _getPrototypeOf3 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf")); 25 | 26 | var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits")); 27 | 28 | var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized")); 29 | 30 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); 31 | 32 | var _react = _interopRequireWildcard(require("react")); 33 | 34 | var _context2 = require("./context"); 35 | 36 | var _lodash = _interopRequireDefault(require("lodash.camelcase")); 37 | 38 | var _initLDClient = require("./initLDClient"); 39 | 40 | var _default = function _default(WrappedComponent, _ref) { 41 | var clientSideId = _ref.clientSideId, 42 | user = _ref.user, 43 | options = _ref.options; 44 | return ( 45 | /*#__PURE__*/ 46 | function (_Component) { 47 | (0, _inherits2.default)(_class2, _Component); 48 | 49 | function _class2() { 50 | var _getPrototypeOf2; 51 | 52 | var _this; 53 | 54 | (0, _classCallCheck2.default)(this, _class2); 55 | 56 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 57 | args[_key] = arguments[_key]; 58 | } 59 | 60 | _this = (0, _possibleConstructorReturn2.default)(this, (_getPrototypeOf2 = (0, _getPrototypeOf3.default)(_class2)).call.apply(_getPrototypeOf2, [this].concat(args))); 61 | (0, _defineProperty2.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), "state", { 62 | flags: {} 63 | }); 64 | (0, _defineProperty2.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), "subscribeToChanges", function () { 65 | _initLDClient.ldClient.on('change', function (changes) { 66 | // changes look like: {'dev-test-flag': {current: true, previous: false}, ...} 67 | var flattened = {}; 68 | 69 | for (var key in changes) { 70 | flattened[(0, _lodash.default)(key)] = changes[key].current; 71 | } 72 | 73 | var flags = (0, _objectSpread2.default)({}, _this.state.flags, flattened); 74 | 75 | _this.setState({ 76 | flags: flags 77 | }); 78 | }); 79 | }); 80 | return _this; 81 | } 82 | 83 | (0, _createClass2.default)(_class2, [{ 84 | key: "componentDidMount", 85 | value: function () { 86 | var _componentDidMount = (0, _asyncToGenerator2.default)( 87 | /*#__PURE__*/ 88 | _regenerator.default.mark(function _callee() { 89 | var flags; 90 | return _regenerator.default.wrap(function _callee$(_context) { 91 | while (1) { 92 | switch (_context.prev = _context.next) { 93 | case 0: 94 | _context.next = 2; 95 | return (0, _initLDClient.initLDClient)(clientSideId, user, options); 96 | 97 | case 2: 98 | flags = _context.sent; 99 | this.setState({ 100 | flags: flags 101 | }); //eslint-disable-line 102 | 103 | this.subscribeToChanges(); 104 | 105 | case 5: 106 | case "end": 107 | return _context.stop(); 108 | } 109 | } 110 | }, _callee, this); 111 | })); 112 | 113 | return function componentDidMount() { 114 | return _componentDidMount.apply(this, arguments); 115 | }; 116 | }() 117 | }, { 118 | key: "render", 119 | value: function render() { 120 | return _react.default.createElement(_context2.Provider, { 121 | value: this.state.flags 122 | }, _react.default.createElement(WrappedComponent, this.props)); 123 | } 124 | }]); 125 | return _class2; 126 | }(_react.Component) 127 | ); 128 | }; 129 | 130 | exports.default = _default; -------------------------------------------------------------------------------- /lib/withFlags.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.default = void 0; 9 | 10 | var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); 11 | 12 | var _react = _interopRequireDefault(require("react")); 13 | 14 | var _context = require("./context"); 15 | 16 | var withFlags = function withFlags(Component) { 17 | return function (props) { 18 | return _react.default.createElement(_context.Consumer, null, function (flags) { 19 | return _react.default.createElement(Component, (0, _extends2.default)({ 20 | flags: flags 21 | }, props)); 22 | }); 23 | }; 24 | }; 25 | 26 | var _default = withFlags; 27 | exports.default = _default; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ld-react", 3 | "version": "1.2.0", 4 | "description": "Launch Darkly React integration library using context api", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "rimraf lib/* && babel src -d lib --ignore *.test.js", 9 | "lint": "eslint --cache --format 'node_modules/eslint-friendly-formatter' ./src", 10 | "build-publish": "npm run build && npm version patch -m 'Upgrade to %s' && npm publish && git push" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/yusinto/ld-react.git" 15 | }, 16 | "keywords": [ 17 | "launch", 18 | "darkly", 19 | "react", 20 | "context", 21 | "suspense", 22 | "feature", 23 | "flag", 24 | "toggle" 25 | ], 26 | "author": "Yusinto Ngadiman", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/yusinto/ld-react/issues" 30 | }, 31 | "homepage": "https://github.com/yusinto/ld-react#readme", 32 | "devDependencies": { 33 | "@babel/cli": "^7.1.0", 34 | "@babel/core": "^7.1.0", 35 | "@babel/node": "^7.0.0", 36 | "@babel/plugin-proposal-class-properties": "^7.1.0", 37 | "@babel/plugin-transform-async-to-generator": "^7.1.0", 38 | "@babel/plugin-transform-runtime": "^7.1.0", 39 | "@babel/preset-env": "^7.1.0", 40 | "@babel/preset-react": "^7.0.0", 41 | "babel-eslint": "^10.0.0", 42 | "babel-jest": "^23.6.0", 43 | "eslint": "^5.6.0", 44 | "eslint-config-airbnb": "^17.1.0", 45 | "eslint-friendly-formatter": "^4.0.0", 46 | "eslint-plugin-babel": "^5.2.0", 47 | "eslint-plugin-import": "^2.14.0", 48 | "eslint-plugin-jsx-a11y": "^6.1.1", 49 | "eslint-plugin-react": "^7.11.1", 50 | "jest": "^23.6.0", 51 | "rimraf": "^2.6.2", 52 | "testdouble": "^3.6.0" 53 | }, 54 | "dependencies": { 55 | "@babel/polyfill": "^7.0.0", 56 | "@babel/runtime": "^7.0.0", 57 | "ip": "^1.1.3", 58 | "ldclient-js": "^2.6.0", 59 | "lodash.camelcase": "^4.3.0", 60 | "react": "^16.5.2", 61 | "ua-parser-js": "^0.7.10", 62 | "uuid": "^3.3.2" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import {createContext} from 'react'; 2 | 3 | const {Provider, Consumer} = createContext(); // TODO: workout the default values for the context 4 | export {Provider, Consumer}; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill'; 2 | import withFlagProvider from './withFlagProvider'; 3 | import withFlags from './withFlags'; 4 | import {ldClient} from './initLDClient'; 5 | 6 | export { 7 | ldClient, 8 | withFlagProvider, 9 | withFlags, 10 | }; -------------------------------------------------------------------------------- /src/initLDClient.js: -------------------------------------------------------------------------------- 1 | import camelCase from 'lodash.camelcase'; 2 | import {initialize as ldClientInitialize} from 'ldclient-js'; 3 | import initUser from './initUser'; 4 | 5 | export let ldClient; 6 | 7 | export const initLDClient = async (clientSideId, user = initUser(), options) => { 8 | ldClient = ldClientInitialize(clientSideId, user, options); 9 | 10 | return await new Promise(resolve => ldClient.on('ready', () => { 11 | const rawFlags = ldClient.allFlags(); 12 | const flags = {}; 13 | for (const rawFlag in rawFlags) { 14 | const camelCasedKey = camelCase(rawFlag); 15 | flags[camelCasedKey] = rawFlags[rawFlag]; 16 | } 17 | resolve(flags); 18 | })); 19 | }; 20 | 21 | export default initLDClient; -------------------------------------------------------------------------------- /src/initUser.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid'; 2 | import ip from 'ip'; 3 | import UAParser from 'ua-parser-js'; 4 | 5 | const userAgentParser = new UAParser(); 6 | const isMobileDevice = typeof window !== 'undefined' && userAgentParser.getDevice().type === 'mobile'; 7 | const isTabletDevice = typeof window !== 'undefined' && userAgentParser.getDevice().type === 'tablet'; 8 | 9 | export default () => { 10 | let device; 11 | 12 | if (isMobileDevice) { 13 | device = 'mobile'; 14 | } else if (isTabletDevice) { 15 | device = 'tablet'; 16 | } else { 17 | device = 'desktop'; 18 | } 19 | 20 | return { 21 | key: uuid.v4(), 22 | ip: ip.address(), 23 | custom: { 24 | browser: userAgentParser.getResult().browser.name, 25 | device, 26 | }, 27 | }; 28 | }; -------------------------------------------------------------------------------- /src/withFlagProvider.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Provider} from './context'; 3 | import camelCase from 'lodash.camelcase'; 4 | import {initLDClient, ldClient} from './initLDClient'; 5 | 6 | export default (WrappedComponent, {clientSideId, user, options}) => { 7 | return class extends Component { 8 | state = {flags: {}}; 9 | 10 | subscribeToChanges = () => { 11 | ldClient.on('change', changes => { // changes look like: {'dev-test-flag': {current: true, previous: false}, ...} 12 | const flattened = {}; 13 | for (const key in changes) { 14 | flattened[camelCase(key)] = changes[key].current; 15 | } 16 | const flags = {...this.state.flags, ...flattened}; 17 | this.setState({flags}); 18 | }); 19 | }; 20 | 21 | async componentDidMount() { 22 | const flags = await initLDClient(clientSideId, user, options); 23 | this.setState({flags}); //eslint-disable-line 24 | this.subscribeToChanges(); 25 | } 26 | 27 | render() { 28 | return ( 29 | 30 | 31 | 32 | ); 33 | } 34 | } 35 | }; -------------------------------------------------------------------------------- /src/withFlags.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Consumer} from './context'; 3 | 4 | const withFlags = (Component) => props => ( 5 | 6 | { 7 | flags => 8 | } 9 | 10 | ); 11 | 12 | export default withFlags; -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import td from 'testdouble'; 2 | 3 | global.td = td; 4 | --------------------------------------------------------------------------------