├── .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 | [](https://www.npmjs.com/package/ld-react) [](https://www.npmjs.com/package/ld-react) [](https://www.npmjs.com/package/ld-react) [](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 |
11 |
12 | Home
13 | Contact
14 |
15 |
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 |
21 | In app.js, set clientSideId to your own Client-side ID. You can find this in the dashboard underAccount settings / Projects.
22 | Create a flag called 'dev-test-flag' in your project and turn it on and off.
23 |
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 |
Generate random number
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 |
--------------------------------------------------------------------------------