├── .nvmrc ├── packages ├── test-utils │ ├── CHANGELOG.md │ ├── CHANGELOG.md │ └── package.json ├── react │ ├── modules │ │ ├── helpers │ │ │ ├── is-nil │ │ │ │ ├── index.ts │ │ │ │ ├── is-nil.ts │ │ │ │ └── is-nil.spec.js │ │ │ ├── get-flag-variation │ │ │ │ ├── index.ts │ │ │ │ ├── get-flag-variation.ts │ │ │ │ └── get-flag-variation.spec.js │ │ │ ├── get-is-feature-enabled │ │ │ │ ├── index.ts │ │ │ │ ├── get-is-feature-enabled.ts │ │ │ │ └── get-is-feature-enabled.spec.js │ │ │ ├── get-normalized-flag-name │ │ │ │ ├── index.ts │ │ │ │ ├── get-normalized-flag-name.ts │ │ │ │ └── get-normalized-flag-name.spec.js │ │ │ └── index.ts │ │ ├── components │ │ │ ├── reconfigure-adapter │ │ │ │ ├── index.ts │ │ │ │ ├── reconfigure-adapter.ts │ │ │ │ └── reconfigure-adapter.spec.js │ │ │ ├── configure-adapter │ │ │ │ ├── index.ts │ │ │ │ ├── helpers.ts │ │ │ │ └── helpers.spec.js │ │ │ ├── toggle-feature │ │ │ │ ├── index.ts │ │ │ │ └── toggle-feature.ts │ │ │ ├── adapter-context │ │ │ │ ├── index.ts │ │ │ │ ├── adapter-context.ts │ │ │ │ └── adapter-context.spec.js │ │ │ └── index.ts │ │ ├── hocs │ │ │ ├── set-display-name │ │ │ │ ├── index.ts │ │ │ │ ├── set-display-name.ts │ │ │ │ └── set-display-name.spec.js │ │ │ ├── wrap-display-name │ │ │ │ ├── index.ts │ │ │ │ ├── wrap-display-name.ts │ │ │ │ └── wrap-display-name.spec.js │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── use-adapter-subscription │ │ │ │ ├── index.ts │ │ │ │ ├── use-adapter-subscription.ts │ │ │ │ └── use-adapter-subscription.spec.js │ │ │ ├── use-adapter-reconfiguration │ │ │ │ ├── index.ts │ │ │ │ ├── use-adapter-reconfiguration.spec.js │ │ │ │ └── use-adapter-reconfiguration.ts │ │ │ └── index.ts │ │ ├── constants.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── index.ts │ ├── CHANGELOG.md │ ├── readme.md │ └── package.json ├── react-redux │ ├── modules │ │ ├── store │ │ │ ├── constants.ts │ │ │ └── enhancer │ │ │ │ ├── index.ts │ │ │ │ ├── enhancer.ts │ │ │ │ └── enhancer.spec.js │ │ ├── components │ │ │ ├── configure │ │ │ │ ├── index.ts │ │ │ │ ├── configure.tsx │ │ │ │ └── configure.spec.js │ │ │ ├── toggle-feature │ │ │ │ ├── index.ts │ │ │ │ ├── toggle-feature.tsx │ │ │ │ └── toggle-feature.spec.js │ │ │ ├── inject-feature-toggle │ │ │ │ ├── index.ts │ │ │ │ ├── inject-feature-toggle.tsx │ │ │ │ └── inject-feature-toggle.spec.js │ │ │ ├── inject-feature-toggles │ │ │ │ ├── index.ts │ │ │ │ ├── inject-feature-toggles.tsx │ │ │ │ └── inject-feature-toggles.spec.js │ │ │ ├── branch-on-feature-toggle │ │ │ │ ├── index.ts │ │ │ │ └── branch-on-feature-toggle.tsx │ │ │ ├── reconfigure │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── use-adapter-status │ │ │ │ ├── index.ts │ │ │ │ ├── use-adapter-status.ts │ │ │ │ └── use-adapter-status.spec.js │ │ │ ├── use-feature-toggle │ │ │ │ ├── index.ts │ │ │ │ ├── use-feature-toggle.ts │ │ │ │ └── use-feature-toggle.spec.js │ │ │ ├── use-feature-toggles │ │ │ │ ├── index.ts │ │ │ │ ├── use-feature-toggles.ts │ │ │ │ └── use-feature-toggles.spec.js │ │ │ ├── use-flag-variations │ │ │ │ ├── index.ts │ │ │ │ ├── use-flag-variations.ts │ │ │ │ └── use-flag-variations.spec.js │ │ │ ├── use-update-flags │ │ │ │ ├── index.ts │ │ │ │ └── use-update-flags.ts │ │ │ ├── use-update-status │ │ │ │ ├── index.ts │ │ │ │ └── use-update-status.ts │ │ │ ├── use-adapter-reconfiguration │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── ducks │ │ │ ├── status │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── status.ts │ │ │ │ └── status.spec.js │ │ │ ├── flags │ │ │ │ ├── types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── flags.ts │ │ │ │ └── flags.spec.js │ │ │ └── index.ts │ │ ├── types.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── index.ts │ ├── test-utils │ │ └── index.js │ ├── readme.md │ ├── CHANGELOG.md │ └── package.json ├── splitio-adapter │ ├── modules │ │ ├── adapter │ │ │ └── index.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── index.ts │ ├── CHANGELOG.md │ ├── readme.md │ └── package.json ├── launchdarkly-adapter │ ├── modules │ │ ├── adapter │ │ │ └── index.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── @types │ │ └── tiny-invariant │ │ │ └── index.d.ts │ ├── index.ts │ ├── CHANGELOG.md │ ├── readme.md │ └── package.json ├── react-broadcast │ ├── modules │ │ ├── components │ │ │ ├── configure │ │ │ │ ├── index.ts │ │ │ │ ├── configure.spec.js │ │ │ │ └── configure.tsx │ │ │ ├── toggle-feature │ │ │ │ ├── index.ts │ │ │ │ ├── toggle-feature.tsx │ │ │ │ └── toggle-feature.spec.js │ │ │ ├── flags-context │ │ │ │ ├── index.ts │ │ │ │ └── flags-context.ts │ │ │ ├── inject-feature-toggle │ │ │ │ ├── index.ts │ │ │ │ ├── inject-feature-toggle.tsx │ │ │ │ └── inject-feature-toggle.spec.js │ │ │ ├── inject-feature-toggles │ │ │ │ ├── index.ts │ │ │ │ ├── inject-feature-toggles.tsx │ │ │ │ └── inject-feature-toggles.spec.js │ │ │ ├── reconfigure │ │ │ │ └── index.ts │ │ │ ├── branch-on-feature-toggle │ │ │ │ ├── index.ts │ │ │ │ ├── branch-on-feature-toggle.tsx │ │ │ │ └── branch-on-feature-toggle.spec.js │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── use-adapter-status │ │ │ │ ├── index.ts │ │ │ │ ├── use-adapter-status.ts │ │ │ │ └── use-adapter-status.spec.js │ │ │ ├── use-feature-toggle │ │ │ │ ├── index.ts │ │ │ │ ├── use-feature-toggle.ts │ │ │ │ └── use-feature-toggle.spec.js │ │ │ ├── use-feature-toggles │ │ │ │ ├── index.ts │ │ │ │ ├── use-feature-toggles.ts │ │ │ │ └── use-feature-toggles.spec.js │ │ │ ├── use-flag-variations │ │ │ │ ├── index.ts │ │ │ │ ├── use-flag-variations.ts │ │ │ │ └── use-flag-variations.spec.js │ │ │ ├── use-adapter-reconfiguration │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── index.ts │ ├── readme.md │ └── package.json ├── types │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── readme.md │ └── package.json ├── memory-adapter │ ├── tsconfig.json │ ├── modules │ │ ├── adapter │ │ │ └── index.ts │ │ └── index.ts │ ├── index.ts │ ├── CHANGELOG.md │ ├── readme.md │ └── package.json └── localstorage-adapter │ ├── tsconfig.json │ ├── modules │ ├── adapter │ │ └── index.ts │ └── index.ts │ ├── index.ts │ ├── CHANGELOG.md │ ├── readme.md │ └── package.json ├── .gitattributes ├── .yarnrc ├── demo ├── .env ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── src │ ├── index.css │ ├── index.js │ ├── modules │ │ ├── index.js │ │ └── counter.js │ ├── flags.js │ ├── App.css │ ├── store.js │ ├── logo.svg │ └── App.js ├── .gitignore └── package.json ├── .eslintignore ├── .prettierignore ├── demo.gif ├── logo.png ├── jest-runner-test.config.js ├── resources ├── logo-2x3.pdf ├── logo-1,5x2.pdf └── logo-circle.pdf ├── .prettierrc ├── jest-runner-eslint.config.js ├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── auto-merge.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── settings.yml └── workflows │ ├── test.yml │ └── release.yml ├── husky.config.js ├── jest.lint.config.js ├── .editorconfig ├── .renovaterc.json ├── .changeset ├── config.json └── README.md ├── codecov.yml ├── lint-staged.config.js ├── commitlint.config.js ├── .gitignore ├── LICENSE ├── tsconfig.json ├── jest.test.config.js ├── .eslintrc.yaml ├── bin └── version.js ├── throwing-console-patch.js ├── babel.config.js └── rollup.config.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /packages/test-utils/CHANGELOG.md : -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | workspaces-experimental true 2 | -------------------------------------------------------------------------------- /demo/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | **/dist/** 3 | .changesets 4 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/flopflip/master/demo.gif -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/flopflip/master/logo.png -------------------------------------------------------------------------------- /jest-runner-test.config.js: -------------------------------------------------------------------------------- 1 | require('@testing-library/jest-dom/extend-expect'); 2 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/is-nil/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './is-nil'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/store/constants.ts: -------------------------------------------------------------------------------- 1 | export const STATE_SLICE = '@flopflip'; 2 | -------------------------------------------------------------------------------- /packages/splitio-adapter/modules/adapter/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './adapter'; 2 | -------------------------------------------------------------------------------- /packages/launchdarkly-adapter/modules/adapter/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './adapter'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/store/enhancer/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './enhancer'; 2 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/flopflip/master/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/configure/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './configure'; 2 | -------------------------------------------------------------------------------- /resources/logo-2x3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/flopflip/master/resources/logo-2x3.pdf -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/configure/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './configure'; 2 | -------------------------------------------------------------------------------- /resources/logo-1,5x2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/flopflip/master/resources/logo-1,5x2.pdf -------------------------------------------------------------------------------- /resources/logo-circle.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/flopflip/master/resources/logo-circle.pdf -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "parser": "typescript" 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/toggle-feature/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './toggle-feature'; 2 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/get-flag-variation/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './get-flag-variation'; 2 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/toggle-feature/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './toggle-feature'; 2 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-adapter-status/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './use-adapter-status'; 2 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-feature-toggle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './use-feature-toggle'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-adapter-status/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './use-adapter-status'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-feature-toggle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './use-feature-toggle'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-feature-toggles/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './use-feature-toggles'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-flag-variations/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './use-flag-variations'; 2 | -------------------------------------------------------------------------------- /packages/react/modules/components/reconfigure-adapter/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './reconfigure-adapter'; 2 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/get-is-feature-enabled/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './get-is-feature-enabled'; 2 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-feature-toggles/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './use-feature-toggles'; 2 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-flag-variations/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './use-flag-variations'; 2 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/get-normalized-flag-name/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './get-normalized-flag-name'; 2 | -------------------------------------------------------------------------------- /packages/react/modules/hocs/set-display-name/index.ts: -------------------------------------------------------------------------------- 1 | export { default as setDisplayName } from './set-display-name'; 2 | -------------------------------------------------------------------------------- /packages/react/modules/hooks/use-adapter-subscription/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './use-adapter-subscription'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/inject-feature-toggle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './inject-feature-toggle'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/inject-feature-toggles/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './inject-feature-toggles'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-update-flags/index.ts: -------------------------------------------------------------------------------- 1 | export { useUpdateFlags as default } from './use-update-flags'; 2 | -------------------------------------------------------------------------------- /packages/react/modules/components/configure-adapter/index.ts: -------------------------------------------------------------------------------- 1 | export { default, AdapterStates } from './configure-adapter'; 2 | -------------------------------------------------------------------------------- /packages/react/modules/hocs/wrap-display-name/index.ts: -------------------------------------------------------------------------------- 1 | export { default as wrapDisplayName } from './wrap-display-name'; 2 | -------------------------------------------------------------------------------- /packages/react/modules/hooks/use-adapter-reconfiguration/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './use-adapter-reconfiguration'; 2 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/flags-context/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FlagsContext } from './flags-context'; 2 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/inject-feature-toggle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './inject-feature-toggle'; 2 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/inject-feature-toggles/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './inject-feature-toggles'; 2 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/reconfigure/index.ts: -------------------------------------------------------------------------------- 1 | export { ReconfigureAdapter as default } from '@flopflip/react'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/branch-on-feature-toggle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './branch-on-feature-toggle'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/reconfigure/index.ts: -------------------------------------------------------------------------------- 1 | export { ReconfigureAdapter as default } from '@flopflip/react'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-update-status/index.ts: -------------------------------------------------------------------------------- 1 | export { useUpdateStatus as default } from './use-update-status'; 2 | -------------------------------------------------------------------------------- /packages/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": ["./index.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/branch-on-feature-toggle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './branch-on-feature-toggle'; 2 | -------------------------------------------------------------------------------- /jest-runner-eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cliOptions: { 3 | format: 'node_modules/eslint-formatter-pretty', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": ["./modules/index.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /packages/memory-adapter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": ["./modules/index.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-adapter-reconfiguration/index.ts: -------------------------------------------------------------------------------- 1 | export { useAdapterReconfiguration as default } from '@flopflip/react'; 2 | -------------------------------------------------------------------------------- /packages/react-redux/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": ["./modules/index.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-adapter-reconfiguration/index.ts: -------------------------------------------------------------------------------- 1 | export { useAdapterReconfiguration as default } from '@flopflip/react'; 2 | -------------------------------------------------------------------------------- /packages/react-broadcast/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": ["./modules/index.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/is-nil/is-nil.ts: -------------------------------------------------------------------------------- 1 | const isNil = (value: any) => value === undefined || value === null; 2 | 3 | export default isNil; 4 | -------------------------------------------------------------------------------- /packages/react/modules/hocs/index.ts: -------------------------------------------------------------------------------- 1 | export { setDisplayName } from './set-display-name'; 2 | export { wrapDisplayName } from './wrap-display-name'; 3 | -------------------------------------------------------------------------------- /packages/splitio-adapter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": ["./modules/index.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: tdeekens 4 | ko_fi: tdeekens 5 | custom: https://paypal.me/tdeekens 6 | -------------------------------------------------------------------------------- /packages/launchdarkly-adapter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": ["./modules/index.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /packages/localstorage-adapter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": ["./modules/index.ts"], 4 | "include": [] 5 | } 6 | -------------------------------------------------------------------------------- /husky.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | 'commit-msg': 'commitlint -e $HUSKY_GIT_PARAMS', 4 | 'pre-commit': 'lint-staged', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/splitio-adapter/modules/index.ts: -------------------------------------------------------------------------------- 1 | const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; 2 | 3 | export { version }; 4 | export { default } from './adapter'; 5 | -------------------------------------------------------------------------------- /packages/launchdarkly-adapter/modules/index.ts: -------------------------------------------------------------------------------- 1 | const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; 2 | 3 | export { version }; 4 | export { default } from './adapter'; 5 | -------------------------------------------------------------------------------- /packages/react-redux/modules/ducks/status/index.ts: -------------------------------------------------------------------------------- 1 | export { default as statusReducer } from './status'; 2 | export { UPDATE_STATUS, updateStatus, selectStatus } from './status'; 3 | -------------------------------------------------------------------------------- /packages/memory-adapter/modules/adapter/index.ts: -------------------------------------------------------------------------------- 1 | const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; 2 | 3 | export { version }; 4 | export { default, updateFlags } from './adapter'; 5 | -------------------------------------------------------------------------------- /jest.lint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runner: 'jest-runner-eslint', 3 | displayName: 'lint', 4 | testMatch: ['/packages/**/*.js', '/packages/**/*.ts'], 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react/modules/components/toggle-feature/index.ts: -------------------------------------------------------------------------------- 1 | import { Props } from './toggle-feature'; 2 | 3 | export type TProps = Props; 4 | 5 | export { default } from './toggle-feature'; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/localstorage-adapter/modules/adapter/index.ts: -------------------------------------------------------------------------------- 1 | const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; 2 | 3 | export { version }; 4 | export { default, updateFlags, STORAGE_SLICE } from './adapter'; 5 | -------------------------------------------------------------------------------- /packages/react-redux/modules/ducks/flags/types.ts: -------------------------------------------------------------------------------- 1 | import type { TFlags } from '@flopflip/types'; 2 | 3 | export type TUpdateFlagsAction = { 4 | type: string; 5 | payload: { flags: TFlags }; 6 | }; 7 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /packages/memory-adapter/modules/index.ts: -------------------------------------------------------------------------------- 1 | const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; 2 | 3 | export { default } from './adapter'; 4 | export { updateFlags } from './adapter'; 5 | export { version }; 6 | -------------------------------------------------------------------------------- /packages/react/modules/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FLAG_PROP_KEY = 'isFeatureEnabled'; 2 | export const DEFAULT_FLAGS_PROP_KEY = 'featureToggles'; 3 | export const ALL_FLAGS_PROP_KEY = '@flopflip/flags'; 4 | -------------------------------------------------------------------------------- /packages/react/modules/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useAdapterReconfiguration } from './use-adapter-reconfiguration'; 2 | export { default as useAdapterSubscription } from './use-adapter-subscription'; 3 | -------------------------------------------------------------------------------- /packages/localstorage-adapter/modules/index.ts: -------------------------------------------------------------------------------- 1 | const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; 2 | 3 | export { default } from './adapter'; 4 | export { updateFlags, STORAGE_SLICE } from './adapter'; 5 | export { version }; 6 | -------------------------------------------------------------------------------- /packages/react-redux/modules/ducks/status/types.ts: -------------------------------------------------------------------------------- 1 | import type { TAdapterStatusChange } from '@flopflip/types'; 2 | 3 | export type TUpdateStatusAction = { 4 | type: string; 5 | payload: { status: TAdapterStatusChange }; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/react-redux/modules/ducks/flags/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as flagsReducer, 3 | createReducer as createFlagsReducer, 4 | } from './flags'; 5 | export { UPDATE_FLAGS, updateFlags, selectFlag, selectFlags } from './flags'; 6 | -------------------------------------------------------------------------------- /packages/react/modules/components/adapter-context/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './adapter-context'; 2 | export { 3 | createAdapterContext, 4 | useAdapterContext, 5 | selectAdapterConfigurationStatus, 6 | } from './adapter-context'; 7 | -------------------------------------------------------------------------------- /packages/launchdarkly-adapter/@types/tiny-invariant/index.d.ts: -------------------------------------------------------------------------------- 1 | declare function invariant( 2 | testValue: any, 3 | format?: string, 4 | ...extra: readonly any[] 5 | ): void; 6 | 7 | declare module 'tiny-invariant' { 8 | export = invariant; 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/flags-context/flags-context.ts: -------------------------------------------------------------------------------- 1 | import type { TFlags } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | 5 | const intialFlagsContext = {}; 6 | const FlagsContext = React.createContext(intialFlagsContext); 7 | 8 | export default FlagsContext; 9 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as getIsFeatureEnabled } from './get-is-feature-enabled'; 2 | export { default as getFlagVariation } from './get-flag-variation'; 3 | export { default as getNormalizedFlagName } from './get-normalized-flag-name'; 4 | export { default as isNil } from './is-nil'; 5 | -------------------------------------------------------------------------------- /.renovaterc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "group:monorepos", 5 | "schedule:weekly" 6 | ], 7 | "lockFileMaintenance": { 8 | "enabled": true 9 | }, 10 | "circleci": { "enabled": false }, 11 | "automerge": true, 12 | "major": { 13 | "automerge": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/react/modules/hocs/set-display-name/set-display-name.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default >( 4 | nextDisplayName: string 5 | ) => (BaseComponent: T): T => { 6 | BaseComponent.displayName = nextDisplayName; 7 | 8 | return BaseComponent; 9 | }; 10 | -------------------------------------------------------------------------------- /demo/src/modules/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { 3 | createFlopflipReducer, 4 | FLOPFLIP_STATE_SLICE, 5 | } from '@flopflip/react-redux'; 6 | import counter from './counter'; 7 | 8 | export default combineReducers({ 9 | [FLOPFLIP_STATE_SLICE]: createFlopflipReducer(), 10 | counter, 11 | }); 12 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.0.3/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "tdeekens/flopflip" 7 | } 8 | ], 9 | "commit": false, 10 | "linked": [], 11 | "access": "restricted", 12 | "baseBranch": "master" 13 | } 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: 'reach, diff, flags, files' 3 | behavior: default 4 | require_changes: false 5 | require_base: no 6 | require_head: yes 7 | branches: null 8 | coverage: 9 | status: 10 | project: 11 | default: 12 | target: 80% 13 | patch: 14 | default: 15 | target: 80% 16 | -------------------------------------------------------------------------------- /packages/types/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flopflip/types 2 | 3 | ## 2.5.3 4 | ### Patch Changes 5 | 6 | 7 | 8 | - [`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e) [#1102](https://github.com/tdeekens/flopflip/pull/1102) Thanks [@tdeekens](https://github.com/tdeekens)! - Update dependencies (TypeScript 3.9) 9 | -------------------------------------------------------------------------------- /demo/src/flags.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT_ASYNC_BUTTON = 'incrementAsync'; 2 | export const DECREMENT_ASYNC_BUTTON = 'decrementAsync'; 3 | export const INCREMENT_SYNC_BUTTON = 'incrementSync'; 4 | 5 | export default { 6 | [INCREMENT_ASYNC_BUTTON]: false, 7 | [DECREMENT_ASYNC_BUTTON]: true, 8 | [INCREMENT_SYNC_BUTTON]: 'yellow', 9 | }; 10 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/get-normalized-flag-name/get-normalized-flag-name.ts: -------------------------------------------------------------------------------- 1 | import type { TFlagName } from '@flopflip/types'; 2 | 3 | import camelCase from 'lodash/camelCase'; 4 | 5 | const getNormalizedFlagName = (flagName: TFlagName): TFlagName => { 6 | return camelCase(flagName); 7 | }; 8 | 9 | export default getNormalizedFlagName; 10 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'demo/**/*.js': ['npm run fix:eslint', 'npm run format:ts', 'git add -u'], 3 | 'packages/**/*.{js,ts}': [ 4 | 'npm run fix:eslint', 5 | 'npm run format:ts', 6 | 'git add -u', 7 | ], 8 | '*.md': ['npm run format:md', 'git add -u'], 9 | '*.yaml': ['npm run format:yaml', 'git add -u'], 10 | }; 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Summary 2 | 3 | 4 | 5 | #### Description 6 | 7 | 8 | 9 | #### Technical debt & future 10 | 11 | 16 | -------------------------------------------------------------------------------- /packages/react/modules/hocs/wrap-display-name/wrap-display-name.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function wrapDisplayName( 4 | BaseComponent: React.ComponentType, 5 | hocName: string 6 | ) { 7 | const previousDisplayName = BaseComponent.displayName ?? BaseComponent.name; 8 | 9 | return `${hocName}(${previousDisplayName ?? 'Component'})`; 10 | } 11 | -------------------------------------------------------------------------------- /demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-adapter-status/use-adapter-status.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { selectStatus } from '../../ducks/status'; 4 | 5 | export default function useAdapterStatus() { 6 | const adapterStatus = useSelector(selectStatus); 7 | 8 | React.useDebugValue({ adapterStatus }); 9 | 10 | return adapterStatus; 11 | } 12 | -------------------------------------------------------------------------------- /packages/react/index.ts: -------------------------------------------------------------------------------- 1 | // This file exists because we want jest to use our non-compiled code to run tests 2 | // if this file is missing, and you have a `module` or `main` that points to a non-existing file 3 | // (ie, a bundle that hasn't been built yet) then jest will fail if the bundle is not yet built. 4 | // all apps should export all their named exports from their root index.js 5 | export * from './modules'; 6 | -------------------------------------------------------------------------------- /packages/react/modules/hooks/use-adapter-reconfiguration/use-adapter-reconfiguration.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useAdapterReconfiguration from './use-adapter-reconfiguration'; 3 | 4 | const reconfigure = jest.fn(); 5 | 6 | it('should return a function', () => { 7 | React.useContext = jest.fn(() => ({ reconfigure })); 8 | expect(useAdapterReconfiguration()).toBe(reconfigure); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/memory-adapter/index.ts: -------------------------------------------------------------------------------- 1 | // This file exists because we want jest to use our non-compiled code to run tests 2 | // if this file is missing, and you have a `module` or `main` that points to a non-existing file 3 | // (ie, a bundle that hasn't been built yet) then jest will fail if the bundle is not yet built. 4 | // all apps should export all their named exports from their root index.js 5 | export * from './modules'; 6 | -------------------------------------------------------------------------------- /packages/react-broadcast/index.ts: -------------------------------------------------------------------------------- 1 | // This file exists because we want jest to use our non-compiled code to run tests 2 | // if this file is missing, and you have a `module` or `main` that points to a non-existing file 3 | // (ie, a bundle that hasn't been built yet) then jest will fail if the bundle is not yet built. 4 | // all apps should export all their named exports from their root index.js 5 | export * from './modules'; 6 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useFeatureToggle } from './use-feature-toggle'; 2 | export { default as useFeatureToggles } from './use-feature-toggles'; 3 | export { default as useFlagVariations } from './use-flag-variations'; 4 | export { default as useAdapterStatus } from './use-adapter-status'; 5 | export { default as useAdapterReconfiguration } from './use-adapter-reconfiguration'; 6 | -------------------------------------------------------------------------------- /packages/react-redux/index.ts: -------------------------------------------------------------------------------- 1 | // This file exists because we want jest to use our non-compiled code to run tests 2 | // if this file is missing, and you have a `module` or `main` that points to a non-existing file 3 | // (ie, a bundle that hasn't been built yet) then jest will fail if the bundle is not yet built. 4 | // all apps should export all their named exports from their root index.js 5 | export * from './modules'; 6 | -------------------------------------------------------------------------------- /packages/splitio-adapter/index.ts: -------------------------------------------------------------------------------- 1 | // This file exists because we want jest to use our non-compiled code to run tests 2 | // if this file is missing, and you have a `module` or `main` that points to a non-existing file 3 | // (ie, a bundle that hasn't been built yet) then jest will fail if the bundle is not yet built. 4 | // all apps should export all their named exports from their root index.js 5 | export * from './modules'; 6 | -------------------------------------------------------------------------------- /packages/launchdarkly-adapter/index.ts: -------------------------------------------------------------------------------- 1 | // This file exists because we want jest to use our non-compiled code to run tests 2 | // if this file is missing, and you have a `module` or `main` that points to a non-existing file 3 | // (ie, a bundle that hasn't been built yet) then jest will fail if the bundle is not yet built. 4 | // all apps should export all their named exports from their root index.js 5 | export * from './modules'; 6 | -------------------------------------------------------------------------------- /packages/localstorage-adapter/index.ts: -------------------------------------------------------------------------------- 1 | // This file exists because we want jest to use our non-compiled code to run tests 2 | // if this file is missing, and you have a `module` or `main` that points to a non-existing file 3 | // (ie, a bundle that hasn't been built yet) then jest will fail if the bundle is not yet built. 4 | // all apps should export all their named exports from their root index.js 5 | export * from './modules'; 6 | -------------------------------------------------------------------------------- /packages/react/modules/components/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as AdapterContext, 3 | createAdapterContext, 4 | useAdapterContext, 5 | selectAdapterConfigurationStatus, 6 | } from './adapter-context'; 7 | export { default as ToggleFeature } from './toggle-feature'; 8 | export { default as ConfigureAdapter } from './configure-adapter'; 9 | export { default as ReconfigureAdapter } from './reconfigure-adapter'; 10 | -------------------------------------------------------------------------------- /packages/react/modules/hooks/use-adapter-reconfiguration/use-adapter-reconfiguration.ts: -------------------------------------------------------------------------------- 1 | import type { TAdapterContext } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { AdapterContext } from '@flopflip/react'; 5 | 6 | export default function useAdapterReconfiguration() { 7 | const adapterContext: TAdapterContext = React.useContext(AdapterContext); 8 | 9 | return adapterContext.reconfigure; 10 | } 11 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | extends: ['@commitlint/config-conventional'], 4 | parserPreset: { 5 | parserOpts: { 6 | // Allow to write a "scope" with slashes 7 | // E.g. `refactor(app/my-component): something` 8 | headerPattern: /^(\w*)(?:\(([\w\$\.\/\-\* ]*)\))?\: (.*)$/, 9 | }, 10 | }, 11 | rules: { 12 | 'header-max-length': [0, 'always', 100], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-auto-merge - https://github.com/bobvanderlinden/probot-auto-merge 2 | 3 | deleteBranchAfterMerge: true 4 | updateBranch: true 5 | mergeMethod: rebase 6 | 7 | minApprovals: 8 | MEMBER: 2 9 | maxRequestedChanges: 10 | COLLABORATOR: 0 11 | blockingLabels: 12 | - WIP 13 | - Blocked 14 | 15 | rules: 16 | - minApprovals: 17 | OWNER: 1 18 | - requiredLabels: 19 | - Automerge 20 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/index.ts: -------------------------------------------------------------------------------- 1 | const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; 2 | 3 | export { 4 | ToggleFeature, 5 | injectFeatureToggle, 6 | injectFeatureToggles, 7 | branchOnFeatureToggle, 8 | ConfigureFlopFlip, 9 | ReconfigureFlopFlip, 10 | } from './components'; 11 | export { 12 | useFeatureToggle, 13 | useFeatureToggles, 14 | useAdapterStatus, 15 | useAdapterReconfiguration, 16 | } from './hooks'; 17 | export { version }; 18 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ToggleFeature } from './toggle-feature'; 2 | export { default as injectFeatureToggle } from './inject-feature-toggle'; 3 | export { default as injectFeatureToggles } from './inject-feature-toggles'; 4 | export { default as branchOnFeatureToggle } from './branch-on-feature-toggle'; 5 | export { default as ConfigureFlopFlip } from './configure'; 6 | export { default as ReconfigureFlopFlip } from './reconfigure'; 7 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ToggleFeature } from './toggle-feature'; 2 | export { default as injectFeatureToggles } from './inject-feature-toggles'; 3 | export { default as injectFeatureToggle } from './inject-feature-toggle'; 4 | export { default as branchOnFeatureToggle } from './branch-on-feature-toggle'; 5 | export { default as ConfigureFlopFlip } from './configure'; 6 | export { default as ReconfigureFlopFlip } from './reconfigure'; 7 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-adapter-status/use-adapter-status.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | useAdapterContext, 4 | selectAdapterConfigurationStatus, 5 | } from '@flopflip/react'; 6 | 7 | export default function useAdapterStatus() { 8 | const { status } = useAdapterContext(); 9 | 10 | const adapterStatus = selectAdapterConfigurationStatus( 11 | status.configurationStatus 12 | ); 13 | 14 | React.useDebugValue({ adapterStatus }); 15 | 16 | return adapterStatus; 17 | } 18 | -------------------------------------------------------------------------------- /packages/memory-adapter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flopflip/memory-adapter 2 | 3 | ## 1.7.3 4 | ### Patch Changes 5 | 6 | 7 | 8 | - [`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e) [#1102](https://github.com/tdeekens/flopflip/pull/1102) Thanks [@tdeekens](https://github.com/tdeekens)! - Update dependencies (TypeScript 3.9) 9 | 10 | - Updated dependencies [[`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e)]: 11 | - @flopflip/types@2.5.3 12 | -------------------------------------------------------------------------------- /packages/splitio-adapter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flopflip/splitio-adapter 2 | 3 | ## 1.8.3 4 | ### Patch Changes 5 | 6 | 7 | 8 | - [`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e) [#1102](https://github.com/tdeekens/flopflip/pull/1102) Thanks [@tdeekens](https://github.com/tdeekens)! - Update dependencies (TypeScript 3.9) 9 | 10 | - Updated dependencies [[`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e)]: 11 | - @flopflip/types@2.5.3 12 | -------------------------------------------------------------------------------- /packages/launchdarkly-adapter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flopflip/launchdarkly-adapter 2 | 3 | ## 2.13.3 4 | ### Patch Changes 5 | 6 | 7 | 8 | - [`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e) [#1102](https://github.com/tdeekens/flopflip/pull/1102) Thanks [@tdeekens](https://github.com/tdeekens)! - Update dependencies (TypeScript 3.9) 9 | 10 | - Updated dependencies [[`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e)]: 11 | - @flopflip/types@2.5.3 12 | -------------------------------------------------------------------------------- /packages/localstorage-adapter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flopflip/localstorage-adapter 2 | 3 | ## 1.7.3 4 | ### Patch Changes 5 | 6 | 7 | 8 | - [`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e) [#1102](https://github.com/tdeekens/flopflip/pull/1102) Thanks [@tdeekens](https://github.com/tdeekens)! - Update dependencies (TypeScript 3.9) 9 | 10 | - Updated dependencies [[`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e)]: 11 | - @flopflip/types@2.5.3 12 | -------------------------------------------------------------------------------- /packages/react-redux/modules/types.ts: -------------------------------------------------------------------------------- 1 | import type { TFlags, TAdapterStatus } from '@flopflip/types'; 2 | import type { TUpdateStatusAction } from './ducks/status/types'; 3 | import type { TUpdateFlagsAction } from './ducks/flags/types'; 4 | 5 | import { STATE_SLICE } from './store/constants'; 6 | 7 | export type TState = { 8 | [STATE_SLICE]: { 9 | flags?: TFlags; 10 | status?: TAdapterStatus; 11 | }; 12 | }; 13 | 14 | export type UpdateFlagsAction = TUpdateFlagsAction; 15 | export type UpdateStatusAction = TUpdateStatusAction; 16 | -------------------------------------------------------------------------------- /packages/react-redux/test-utils/index.js: -------------------------------------------------------------------------------- 1 | import { createStore as createReduxStore } from 'redux'; 2 | import { combineReducers } from 'redux'; 3 | import { 4 | createFlopflipReducer, 5 | FLOPFLIP_STATE_SLICE, 6 | } from '@flopflip/react-redux'; 7 | 8 | const defaultInitialState = {}; 9 | 10 | const reducer = combineReducers({ 11 | [FLOPFLIP_STATE_SLICE]: createFlopflipReducer(), 12 | }); 13 | const createStore = (initialState = defaultInitialState) => 14 | createReduxStore(reducer, initialState); 15 | 16 | export { createStore }; 17 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useUpdateFlags } from './use-update-flags'; 2 | export { default as useUpdateStatus } from './use-update-status'; 3 | 4 | export { default as useAdapterReconfiguration } from './use-adapter-reconfiguration'; 5 | export { default as useAdapterStatus } from './use-adapter-status'; 6 | export { default as useFeatureToggle } from './use-feature-toggle'; 7 | export { default as useFeatureToggles } from './use-feature-toggles'; 8 | export { default as useFlagVariations } from './use-flag-variations'; 9 | -------------------------------------------------------------------------------- /packages/react/modules/hocs/wrap-display-name/wrap-display-name.spec.js: -------------------------------------------------------------------------------- 1 | import wrapDisplayName from './wrap-display-name'; 2 | 3 | function BaseComponent() { 4 | return 'BaseComponent'; 5 | } 6 | 7 | BaseComponent.displayName = 'BaseComponent'; 8 | 9 | describe('rendering', () => { 10 | const hocName = 'testHoc'; 11 | 12 | it('should include `hocName` in wrapped display name', () => { 13 | const wrappedDisplayName = wrapDisplayName(BaseComponent, hocName); 14 | 15 | expect(wrappedDisplayName).toContain(hocName); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/is-nil/is-nil.spec.js: -------------------------------------------------------------------------------- 1 | import isNil from './is-nil'; 2 | 3 | describe('when null', () => { 4 | it('should indicate that the value is nil', () => { 5 | expect(isNil(null)).toBe(true); 6 | }); 7 | }); 8 | 9 | describe('when undefined', () => { 10 | it('should indicate that the value is nil', () => { 11 | expect(isNil(undefined)).toBe(true); 12 | }); 13 | }); 14 | 15 | describe('when anything else', () => { 16 | it('should indicate that the value is not nil', () => { 17 | expect(isNil('foo')).toBe(false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/get-is-feature-enabled/get-is-feature-enabled.ts: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlagVariation, TFlags } from '@flopflip/types'; 2 | 3 | import { DEFAULT_FLAG_PROP_KEY } from '../../constants'; 4 | import getFlagVariation from '../get-flag-variation'; 5 | 6 | const getIsFeatureEnabled = ( 7 | flagName: TFlagName = DEFAULT_FLAG_PROP_KEY, 8 | flagVariation: TFlagVariation = true 9 | ): ((flags: Readonly) => boolean) => { 10 | return (flags) => getFlagVariation(flagName)(flags) === flagVariation; 11 | }; 12 | 13 | export default getIsFeatureEnabled; 14 | -------------------------------------------------------------------------------- /packages/react/modules/hocs/set-display-name/set-display-name.spec.js: -------------------------------------------------------------------------------- 1 | import setDisplayName from './set-display-name'; 2 | 3 | function BaseComponent() { 4 | return 'BaseComponent'; 5 | } 6 | 7 | BaseComponent.displayName = 'BaseComponent'; 8 | 9 | describe('rendering', () => { 10 | const nextDisplayName = 'RenamedBaseComponent'; 11 | 12 | it('should overwrite the previous display name', () => { 13 | const EnhancedComponent = setDisplayName(nextDisplayName)(BaseComponent); 14 | 15 | expect(EnhancedComponent.displayName).toEqual(nextDisplayName); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-update-flags/use-update-flags.ts: -------------------------------------------------------------------------------- 1 | import type { TFlagsChange } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { Dispatch } from 'redux'; 5 | import { useDispatch } from 'react-redux'; 6 | import { updateFlags } from '../../ducks'; 7 | 8 | const useUpdateFlags = () => { 9 | const dispatch = useDispatch>>(); 10 | return React.useCallback( 11 | (flags: Readonly) => dispatch(updateFlags(flags)), 12 | [dispatch] 13 | ); 14 | }; 15 | 16 | export { useUpdateFlags }; 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Describe the bug** 7 | A clear and concise description of what the bug is. 8 | 9 | **To Reproduce** 10 | Steps to reproduce the behavior: 11 | 12 | 1. Given '...' 13 | 2. Then '....' 14 | 3. That '....' 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-update-status/use-update-status.ts: -------------------------------------------------------------------------------- 1 | import type { TAdapterStatusChange } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { Dispatch } from 'redux'; 5 | import { useDispatch } from 'react-redux'; 6 | import { updateStatus } from '../../ducks'; 7 | 8 | const useUpdateStatus = () => { 9 | const dispatch = useDispatch>>(); 10 | return React.useCallback( 11 | (status: Readonly) => dispatch(updateStatus(status)), 12 | [dispatch] 13 | ); 14 | }; 15 | 16 | export { useUpdateStatus }; 17 | -------------------------------------------------------------------------------- /packages/react/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flopflip/react 2 | 3 | ## 9.1.14 4 | 5 | ### Patch Changes 6 | 7 | - [`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e) [#1102](https://github.com/tdeekens/flopflip/pull/1102) Thanks [@tdeekens](https://github.com/tdeekens)! - Update dependencies (TypeScript 3.9) 8 | 9 | ## 9.1.13 10 | 11 | ### Patch Changes 12 | 13 | - [`ee96512`](https://github.com/tdeekens/flopflip/commit/ee96512dd32ab75e6f9790df9322d6bc27642eac) [#1089](https://github.com/tdeekens/flopflip/pull/1089) Thanks [@tdeekens](https://github.com/tdeekens)! - Updating dependencies. 14 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-flag-variations/use-flag-variations.ts: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlagVariation } from '@flopflip/types'; 2 | 3 | import { getFlagVariation } from '@flopflip/react'; 4 | import { useSelector } from 'react-redux'; 5 | import { selectFlags } from '../../ducks/flags'; 6 | 7 | export default function useFlagVariations(flagNames: Readonly) { 8 | const allFlags = useSelector(selectFlags); 9 | 10 | const flagVariations: TFlagVariation[] = flagNames.map((requestedVariation) => 11 | getFlagVariation(requestedVariation)(allFlags) 12 | ); 13 | 14 | return flagVariations; 15 | } 16 | -------------------------------------------------------------------------------- /packages/test-utils/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flopflip/test-utils 2 | 3 | ## 1.1.15 4 | 5 | ### Patch Changes 6 | 7 | - [`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e) [#1102](https://github.com/tdeekens/flopflip/pull/1102) Thanks [@tdeekens](https://github.com/tdeekens)! - Update dependencies (TypeScript 3.9) 8 | 9 | ## 1.1.14 10 | 11 | ### Patch Changes 12 | 13 | - [`ee96512`](https://github.com/tdeekens/flopflip/commit/ee96512dd32ab75e6f9790df9322d6bc27642eac) [#1089](https://github.com/tdeekens/flopflip/pull/1089) Thanks [@tdeekens](https://github.com/tdeekens)! - Updating dependencies. 14 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-flag-variations/use-flag-variations.ts: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlags, TFlagVariation } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { getFlagVariation } from '@flopflip/react'; 5 | import { FlagsContext } from '../../components/flags-context'; 6 | 7 | export default function useFlagVariations(flagNames: Readonly) { 8 | const allFlags: TFlags = React.useContext(FlagsContext); 9 | 10 | const flagVariations: TFlagVariation[] = flagNames.map((requestedVariation) => 11 | getFlagVariation(requestedVariation)(allFlags) 12 | ); 13 | 14 | return flagVariations; 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/get-normalized-flag-name/get-normalized-flag-name.spec.js: -------------------------------------------------------------------------------- 1 | import getNormalizedFlagName from './get-normalized-flag-name'; 2 | 3 | describe('when not camel caased', () => { 4 | it('should normalized the flag name', () => { 5 | expect(getNormalizedFlagName('foo-flag')).toEqual('fooFlag'); 6 | expect(getNormalizedFlagName('foo_flag')).toEqual('fooFlag'); 7 | expect(getNormalizedFlagName('foo flag')).toEqual('fooFlag'); 8 | }); 9 | }); 10 | 11 | describe('when camel caased', () => { 12 | it('should normalized the flag name', () => { 13 | expect(getNormalizedFlagName('fooFlag')).toEqual('fooFlag'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/types/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 🎛 flopflip - Feature Toggling 🎚
3 | flip or flop a feature via adapters (e.g. memory or LaunchDarkly) with real-time updates through a Redux store or React's context. 4 |

5 | 6 |

7 | Logo

8 | Toggle (flip or flop) features being stored in Redux or in a broadcasting system (through the context) via a set of React components or HoCs. 9 |

10 | 11 | ### `@flopflip/types` 12 | 13 | This repository is part of the `flopflip` mono repository. Please head [here](https://github.com/tdeekens/flopflip) for more information. 14 | -------------------------------------------------------------------------------- /packages/react-redux/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 🎛 flopflip - Feature Toggling 🎚
3 | flip or flop a feature via adapters (e.g. memory or LaunchDarkly) with real-time updates through a Redux store or React's context. 4 |

5 | 6 |

7 | Logo

8 | Toggle (flip or flop) features being stored in Redux or in a broadcasting system (through the context) via a set of React components or HoCs. 9 |

10 | 11 | ### `@flopflip/react-redux` 12 | 13 | This repository is part of the `flopflip` mono repository. Please head [here](https://github.com/tdeekens/flopflip) for more information. 14 | -------------------------------------------------------------------------------- /packages/react/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 🎛 flopflip - Feature Toggling 🎚
3 | flip or flop a feature via adapters (e.g. memory or LaunchDarkly) with real-time updates through a Redux store or React's context. 4 |

5 | 6 |

7 | Logo

8 | Toggle (flip or flop) features being stored in Redux or in a broadcasting system (through the context) via a set of React components or HoCs. 9 |

10 | 11 | ### `@flopflip/launchdarkly-react` 12 | 13 | This repository is part of the `flopflip` mono repository. Please head [here](https://github.com/tdeekens/flopflip) for more information. 14 | -------------------------------------------------------------------------------- /packages/memory-adapter/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 🎛 flopflip - Feature Toggling 🎚
3 | flip or flop a feature via adapters (e.g. memory or LaunchDarkly) with real-time updates through a Redux store or React's context. 4 |

5 | 6 |

7 | Logo

8 | Toggle (flip or flop) features being stored in Redux or in a broadcasting system (through the context) via a set of React components or HoCs. 9 |

10 | 11 | ### `@flopflip/memory-adapter` 12 | 13 | This repository is part of the `flopflip` mono repository. Please head [here](https://github.com/tdeekens/flopflip) for more information. 14 | -------------------------------------------------------------------------------- /packages/react-broadcast/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 🎛 flopflip - Feature Toggling 🎚
3 | flip or flop a feature via adapters (e.g. memory or LaunchDarkly) with real-time updates through a Redux store or React's context. 4 |

5 | 6 |

7 | Logo

8 | Toggle (flip or flop) features being stored in Redux or in a broadcasting system (through the context) via a set of React components or HoCs. 9 |

10 | 11 | ### `@flopflip/react-broadcast` 12 | 13 | This repository is part of the `flopflip` mono repository. Please head [here](https://github.com/tdeekens/flopflip) for more information. 14 | -------------------------------------------------------------------------------- /packages/splitio-adapter/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 🎛 flopflip - Feature Toggling 🎚
3 | flip or flop a feature via adapters (e.g. memory or LaunchDarkly) with real-time updates through a Redux store or React's context. 4 |

5 | 6 |

7 | Logo

8 | Toggle (flip or flop) features being stored in Redux or in a broadcasting system (through the context) via a set of React components or HoCs. 9 |

10 | 11 | ### `@flopflip/splitio-adapter` 12 | 13 | This repository is part of the `flopflip` mono repository. Please head [here](https://github.com/tdeekens/flopflip) for more information. 14 | -------------------------------------------------------------------------------- /packages/launchdarkly-adapter/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 🎛 flopflip - Feature Toggling 🎚
3 | flip or flop a feature via adapters (e.g. memory or LaunchDarkly) with real-time updates through a Redux store or React's context. 4 |

5 | 6 |

7 | Logo

8 | Toggle (flip or flop) features being stored in Redux or in a broadcasting system (through the context) via a set of React components or HoCs. 9 |

10 | 11 | ### `@flopflip/launchdarkly-adapter` 12 | 13 | This repository is part of the `flopflip` mono repository. Please head [here](https://github.com/tdeekens/flopflip) for more information. 14 | -------------------------------------------------------------------------------- /packages/localstorage-adapter/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 🎛 flopflip - Feature Toggling 🎚
3 | flip or flop a feature via adapters (e.g. memory or LaunchDarkly) with real-time updates through a Redux store or React's context. 4 |

5 | 6 |

7 | Logo

8 | Toggle (flip or flop) features being stored in Redux or in a broadcasting system (through the context) via a set of React components or HoCs. 9 |

10 | 11 | ### `@flopflip/localstorage-adapter` 12 | 13 | This repository is part of the `flopflip` mono repository. Please head [here](https://github.com/tdeekens/flopflip) for more information. 14 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-feature-toggle/use-feature-toggle.ts: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlags, TFlagVariation } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { getIsFeatureEnabled } from '@flopflip/react'; 5 | import { FlagsContext } from '../../components/flags-context'; 6 | 7 | export default function useFeatureToggle( 8 | flagName: TFlagName, 9 | flagVariation: TFlagVariation = true 10 | ) { 11 | const flags: TFlags = React.useContext(FlagsContext); 12 | const isFeatureEnabled: boolean = getIsFeatureEnabled( 13 | flagName, 14 | flagVariation 15 | )(flags); 16 | 17 | React.useDebugValue({ 18 | flagName, 19 | flagVariation, 20 | isEnabled: isFeatureEnabled, 21 | }); 22 | 23 | return isFeatureEnabled; 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-feature-toggle/use-feature-toggle.ts: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlagVariation } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { useSelector } from 'react-redux'; 5 | import { getIsFeatureEnabled } from '@flopflip/react'; 6 | import { selectFlags } from '../../ducks/flags'; 7 | 8 | export default function useFeatureToggle( 9 | flagName: TFlagName, 10 | flagVariation: TFlagVariation = true 11 | ) { 12 | const flags = useSelector(selectFlags); 13 | const isFeatureEnabled: boolean = getIsFeatureEnabled( 14 | flagName, 15 | flagVariation 16 | )(flags); 17 | 18 | React.useDebugValue({ 19 | flagName, 20 | flagVariation, 21 | isEnabled: isFeatureEnabled, 22 | }); 23 | 24 | return isFeatureEnabled; 25 | } 26 | -------------------------------------------------------------------------------- /.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 | .npmrc 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | dist 40 | 41 | coverage 42 | 43 | # VSCode 44 | .vscode 45 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: flopflip 3 | description: 🎚Flip or flop features in your React application in real-time backed by flag provider of your choice 🚦 4 | homepage: https://techblog.commercetools.com/embracing-real-time-feature-toggling-in-your-react-application-a5e6052716a9 5 | private: false 6 | has_issues: true 7 | has_wiki: false 8 | has_downloads: true 9 | default_branch: master 10 | allow_squash_merge: true 11 | allow_merge_commit: true 12 | allow_rebase_merge: true 13 | labels: 14 | - name: Automerge 15 | color: cfdb62 16 | - name: Bug 17 | color: ee0701 18 | - name: Documentation 19 | color: d4c5f9 20 | - name: ✨ Feature 21 | color: 84b6eb 22 | - name: ❓ Question 23 | color: cc317c 24 | - name: ❤️ Refactoring 25 | color: fbca04 26 | - name: Wontfix 27 | color: ffffff 28 | -------------------------------------------------------------------------------- /packages/react/modules/index.ts: -------------------------------------------------------------------------------- 1 | import { TProps as ToggleFeatureProps } from './components/toggle-feature'; 2 | 3 | export type TToggleFeatureProps = ToggleFeatureProps; 4 | 5 | const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; 6 | 7 | export { 8 | AdapterContext, 9 | createAdapterContext, 10 | useAdapterContext, 11 | selectAdapterConfigurationStatus, 12 | ToggleFeature, 13 | ConfigureAdapter, 14 | ReconfigureAdapter, 15 | } from './components'; 16 | 17 | export { getIsFeatureEnabled, getFlagVariation, isNil } from './helpers'; 18 | 19 | export { 20 | DEFAULT_FLAG_PROP_KEY, 21 | DEFAULT_FLAGS_PROP_KEY, 22 | ALL_FLAGS_PROP_KEY, 23 | } from './constants'; 24 | 25 | export { setDisplayName, wrapDisplayName } from './hocs'; 26 | 27 | export { useAdapterReconfiguration, useAdapterSubscription } from './hooks'; 28 | 29 | export { version }; 30 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/toggle-feature/toggle-feature.tsx: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlagVariation } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { 5 | ToggleFeature as SharedToggleFeature, 6 | TToggleFeatureProps, 7 | } from '@flopflip/react'; 8 | import { useFeatureToggle } from '../../hooks/'; 9 | 10 | type Props = { 11 | flag: TFlagName; 12 | variation?: TFlagVariation; 13 | // eslint-disable-next-line @typescript-eslint/ban-types 14 | } & Omit; 15 | 16 | const ToggleFeature = (props: OwnProps) => { 17 | const isFeatureEnabled = useFeatureToggle(props.flag, props.variation); 18 | return ; 19 | }; 20 | 21 | ToggleFeature.displayName = 'ToggleFeature'; 22 | 23 | export default ToggleFeature; 24 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@flopflip/launchdarkly-adapter": "2.13.2", 7 | "@flopflip/localstorage-adapter": "1.7.2", 8 | "@flopflip/memory-adapter": "1.7.2", 9 | "@flopflip/react-broadcast": "10.1.15", 10 | "@flopflip/react-redux": "10.1.15", 11 | "classnames": "2.2.6", 12 | "lodash.flowright": "3.5.0", 13 | "react": "16.13.1", 14 | "react-dom": "16.13.1", 15 | "react-redux": "7.2.0", 16 | "react-scripts": "3.4.1", 17 | "redux": "4.0.5", 18 | "redux-logger": "3.0.6", 19 | "redux-thunk": "2.3.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "eject": "react-scripts eject" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/react-redux/modules/index.ts: -------------------------------------------------------------------------------- 1 | const version = '__@FLOPFLIP/VERSION_OF_RELEASE__'; 2 | 3 | export { default as createFlopFlipEnhancer } from './store/enhancer'; 4 | // Import this separately to avoid a circular dependency 5 | export { STATE_SLICE as FLOPFLIP_STATE_SLICE } from './store/constants'; 6 | 7 | export { 8 | createFlopflipReducer, 9 | flopflipReducer, 10 | selectFlags as selectFeatureFlags, 11 | selectFlag as selectFeatureFlag, 12 | UPDATE_STATUS, 13 | UPDATE_FLAGS, 14 | } from './ducks'; 15 | 16 | export { 17 | ToggleFeature, 18 | injectFeatureToggle, 19 | injectFeatureToggles, 20 | branchOnFeatureToggle, 21 | ConfigureFlopFlip, 22 | ReconfigureFlopFlip, 23 | } from './components'; 24 | 25 | export { 26 | useAdapterReconfiguration, 27 | useAdapterStatus, 28 | useFeatureToggle, 29 | useFeatureToggles, 30 | } from './hooks'; 31 | 32 | export { version }; 33 | -------------------------------------------------------------------------------- /packages/react-redux/modules/ducks/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { TFlags } from '@flopflip/types'; 3 | import { TUpdateFlagsAction } from './flags/types'; 4 | import { TUpdateStatusAction } from './status/types'; 5 | import { flagsReducer, createFlagsReducer } from './flags'; 6 | import { statusReducer } from './status'; 7 | 8 | type Actions = TUpdateFlagsAction & TUpdateStatusAction; 9 | 10 | export { updateStatus, UPDATE_STATUS } from './status'; 11 | export { updateFlags, UPDATE_FLAGS, selectFlag, selectFlags } from './flags'; 12 | 13 | export const flopflipReducer = combineReducers({ 14 | flags: flagsReducer, 15 | status: statusReducer, 16 | }); 17 | export const createFlopflipReducer = (preloadedState: Readonly = {}) => 18 | combineReducers({ 19 | flags: createFlagsReducer(preloadedState), 20 | status: statusReducer, 21 | }); 22 | -------------------------------------------------------------------------------- /demo/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | button { 31 | border-radius: 10px; 32 | border: 1; 33 | padding: 5px 10px; 34 | margin: 10px 0; 35 | font-size: 14px; 36 | } 37 | 38 | .incrementSyncButton--disabled { 39 | display: none; 40 | } 41 | 42 | .incrementSyncButton--yellow { 43 | background: #cce310; 44 | } 45 | 46 | .incrementSyncButton--blue { 47 | background: #134074; 48 | } 49 | 50 | .incrementSyncButton--purple { 51 | background: #8923af; 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-feature-toggles/use-feature-toggles.ts: -------------------------------------------------------------------------------- 1 | import type { TFlags, TFlagName, TFlagVariation } from '@flopflip/types'; 2 | 3 | import { useSelector } from 'react-redux'; 4 | import { getIsFeatureEnabled } from '@flopflip/react'; 5 | import { selectFlags } from '../../ducks/flags'; 6 | 7 | export default function useFeatureToggles(flags: Readonly) { 8 | const allFlags = useSelector(selectFlags); 9 | 10 | const requestedFlags: boolean[] = Object.entries(flags).reduce( 11 | // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types 12 | (previousFlags, [flagName, flagVariation]: [TFlagName, TFlagVariation]) => { 13 | const isFeatureEnabled: boolean = getIsFeatureEnabled( 14 | flagName, 15 | flagVariation 16 | )(allFlags); 17 | 18 | return [...previousFlags, isFeatureEnabled]; 19 | }, 20 | [] 21 | ); 22 | 23 | return requestedFlags; 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/toggle-feature/toggle-feature.tsx: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly } from 'ts-essentials'; 2 | import type { TFlagName, TFlagVariation } from '@flopflip/types'; 3 | 4 | import React from 'react'; 5 | import { 6 | ToggleFeature as SharedToggleFeature, 7 | TToggleFeatureProps, 8 | } from '@flopflip/react'; 9 | import { useFeatureToggle } from '../../hooks/'; 10 | 11 | type Props = DeepReadonly< 12 | { 13 | flag: TFlagName; 14 | variation?: TFlagVariation; 15 | // eslint-disable-next-line @typescript-eslint/ban-types 16 | } & Omit 17 | >; 18 | 19 | const ToggleFeature = (props: OwnProps) => { 20 | const isFeatureEnabled = useFeatureToggle(props.flag, props.variation); 21 | return ; 22 | }; 23 | 24 | ToggleFeature.displayName = 'ToggleFeature'; 25 | 26 | export default ToggleFeature; 27 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-feature-toggles/use-feature-toggles.ts: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlags, TFlagVariation } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { getIsFeatureEnabled } from '@flopflip/react'; 5 | import { FlagsContext } from '../../components/flags-context'; 6 | 7 | export default function useFeatureToggles(flags: Readonly) { 8 | const allFlags: TFlags = React.useContext(FlagsContext); 9 | 10 | const requestedFlags: boolean[] = Object.entries(flags).reduce( 11 | // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types 12 | (previousFlags, [flagName, flagVariation]: [TFlagName, TFlagVariation]) => { 13 | const isFeatureEnabled: boolean = getIsFeatureEnabled( 14 | flagName, 15 | flagVariation 16 | )(allFlags); 17 | 18 | return [...previousFlags, isFeatureEnabled]; 19 | }, 20 | [] 21 | ); 22 | 23 | return requestedFlags; 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-redux/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flopflip/react-redux 2 | 3 | ## 10.1.16 4 | 5 | ### Patch Changes 6 | 7 | - [`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e) [#1102](https://github.com/tdeekens/flopflip/pull/1102) Thanks [@tdeekens](https://github.com/tdeekens)! - Update dependencies (TypeScript 3.9) 8 | 9 | - Updated dependencies [[`32cc6a8`](https://github.com/tdeekens/flopflip/commit/32cc6a823ff9812ab2f256b69dd3f46e273feb5e)]: 10 | - @flopflip/react@9.1.14 11 | - @flopflip/types@2.5.3 12 | 13 | ## 10.1.15 14 | 15 | ### Patch Changes 16 | 17 | - [`ee96512`](https://github.com/tdeekens/flopflip/commit/ee96512dd32ab75e6f9790df9322d6bc27642eac) [#1089](https://github.com/tdeekens/flopflip/pull/1089) Thanks [@tdeekens](https://github.com/tdeekens)! - Updating dependencies. 18 | 19 | - Updated dependencies [[`ee96512`](https://github.com/tdeekens/flopflip/commit/ee96512dd32ab75e6f9790df9322d6bc27642eac)]: 20 | - @flopflip/react@9.1.13 21 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/branch-on-feature-toggle/branch-on-feature-toggle.tsx: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlagVariation } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { useFeatureToggle } from '../../hooks'; 5 | 6 | type TBranchOnFeatureToggleOptions = Readonly<{ 7 | flag: TFlagName; 8 | variation?: TFlagVariation; 9 | }>; 10 | export default function branchOnFeatureToggle( 11 | { flag: flagName, variation: flagVariation }: TBranchOnFeatureToggleOptions, 12 | UntoggledComponent?: React.ComponentType 13 | ) { 14 | return (ToggledComponent: React.ComponentType) => { 15 | const WrappedToggledComponent = (ownProps: OwnProps) => { 16 | const isFeatureEnabled = useFeatureToggle(flagName, flagVariation); 17 | 18 | if (isFeatureEnabled) return ; 19 | if (UntoggledComponent) return ; 20 | return null; 21 | }; 22 | 23 | return WrappedToggledComponent; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/branch-on-feature-toggle/branch-on-feature-toggle.tsx: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlagVariation } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { useFeatureToggle } from '../../hooks'; 5 | 6 | type TBranchOnFeatureToggleOptions = Readonly<{ 7 | flag: TFlagName; 8 | variation?: TFlagVariation; 9 | }>; 10 | export default function branchOnFeatureToggle( 11 | { flag: flagName, variation: flagVariation }: TBranchOnFeatureToggleOptions, 12 | UntoggledComponent?: React.ComponentType 13 | ) { 14 | return (ToggledComponent: React.ComponentType) => { 15 | const WrappedToggledComponent = (ownProps: OwnProps) => { 16 | const isFeatureEnabled = useFeatureToggle(flagName, flagVariation); 17 | 18 | if (isFeatureEnabled) return ; 19 | if (UntoggledComponent) return ; 20 | return null; 21 | }; 22 | 23 | return WrappedToggledComponent; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/get-flag-variation/get-flag-variation.ts: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlagVariation, TFlags } from '@flopflip/types'; 2 | 3 | import warning from 'tiny-warning'; 4 | import { DEFAULT_FLAG_PROP_KEY } from '../../constants'; 5 | import getNormalizedFlagName from '../get-normalized-flag-name'; 6 | import isNil from '../is-nil'; 7 | 8 | const getFlagVariation = ( 9 | flagName: TFlagName = DEFAULT_FLAG_PROP_KEY 10 | ): ((flags: Readonly) => TFlagVariation) => { 11 | const normalizedFlagName = getNormalizedFlagName(flagName); 12 | 13 | warning( 14 | normalizedFlagName === flagName, 15 | '@flopflip/react: passed flag name does not seem to be normalized which may result in unexpected toggling. Please refer to our readme for more information: https://github.com/tdeekens/flopflip#flag-normalization' 16 | ); 17 | 18 | return (flags) => { 19 | const flagVariation = flags[normalizedFlagName]; 20 | 21 | return isNil(flagVariation) ? false : flagVariation; 22 | }; 23 | }; 24 | 25 | export default getFlagVariation; 26 | -------------------------------------------------------------------------------- /packages/react/modules/components/configure-adapter/helpers.ts: -------------------------------------------------------------------------------- 1 | import { DeepReadonly } from 'ts-essentials'; 2 | import React from 'react'; 3 | import merge from 'deepmerge'; 4 | import { 5 | TAdapterArgs, 6 | TAdapterReconfiguration, 7 | TConfigureAdapterChildren, 8 | TConfigureAdapterChildrenAsFunction, 9 | } from '@flopflip/types'; 10 | 11 | const isFunctionChildren = ( 12 | children: TConfigureAdapterChildren 13 | ): children is TConfigureAdapterChildrenAsFunction => 14 | typeof children === 'function'; 15 | 16 | const isEmptyChildren = (children: TConfigureAdapterChildren) => 17 | !isFunctionChildren(children) && React.Children.count(children) === 0; 18 | 19 | const mergeAdapterArgs = ( 20 | previousAdapterArgs: TAdapterArgs, 21 | { 22 | adapterArgs: nextAdapterArgs, 23 | options = {}, 24 | }: DeepReadonly 25 | ): TAdapterArgs => 26 | options.shouldOverwrite 27 | ? nextAdapterArgs 28 | : merge(previousAdapterArgs, nextAdapterArgs); 29 | 30 | export { isFunctionChildren, isEmptyChildren, mergeAdapterArgs }; 31 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/inject-feature-toggle/inject-feature-toggle.tsx: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlagVariation } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { 5 | wrapDisplayName, 6 | setDisplayName, 7 | DEFAULT_FLAG_PROP_KEY, 8 | } from '@flopflip/react'; 9 | import { useFlagVariations } from '../../hooks'; 10 | 11 | type InjectedProps = { 12 | [propKey: string]: TFlagVariation; 13 | }; 14 | 15 | export default ( 16 | flagName: TFlagName, 17 | propKey: string = DEFAULT_FLAG_PROP_KEY 18 | ) => ( 19 | Component: React.ComponentType 20 | ): React.ComponentType => { 21 | const WrappedComponent = (ownProps: OwnProps) => { 22 | const [flagVariation] = useFlagVariations([flagName]); 23 | const props = { 24 | ...ownProps, 25 | [propKey]: flagVariation, 26 | }; 27 | 28 | return ; 29 | }; 30 | 31 | setDisplayName(wrapDisplayName(WrappedComponent, 'injectFeatureToggle')); 32 | 33 | return WrappedComponent; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/react/modules/components/reconfigure-adapter/reconfigure-adapter.ts: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly } from 'ts-essentials'; 2 | import type { TUser } from '@flopflip/types'; 3 | 4 | import React from 'react'; 5 | import AdapterContext from '../adapter-context'; 6 | 7 | type Props = DeepReadonly<{ 8 | shouldOverwrite?: boolean; 9 | user: TUser; 10 | children?: React.ReactNode; 11 | }>; 12 | 13 | const ReconfigureAdapter = (props: Props) => { 14 | const adapterContext = React.useContext(AdapterContext); 15 | 16 | React.useEffect(() => { 17 | adapterContext.reconfigure( 18 | { 19 | user: props.user, 20 | }, 21 | { 22 | shouldOverwrite: props.shouldOverwrite, 23 | } 24 | ); 25 | }, [props.user, props.shouldOverwrite, adapterContext]); 26 | 27 | return props.children ? React.Children.only(props.children) : null; 28 | }; 29 | 30 | ReconfigureAdapter.displayName = 'ReconfigureAdapter'; 31 | ReconfigureAdapter.defaultProps = { 32 | shouldOverwrite: false, 33 | children: null, 34 | }; 35 | 36 | export default ReconfigureAdapter; 37 | -------------------------------------------------------------------------------- /packages/test-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@flopflip/test-utils", 4 | "version": "1.1.15", 5 | "description": "Test utils for flipflop", 6 | "scripts": { 7 | "build": "exit 0" 8 | }, 9 | "main": "index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/tdeekens/flopflip.git" 13 | }, 14 | "author": "Tobias Deekens ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/tdeekens/flopflip/issues" 18 | }, 19 | "homepage": "https://github.com/tdeekens/flopflip#readme", 20 | "keywords": [ 21 | "feature-flags", 22 | "feature-toggles", 23 | "types" 24 | ], 25 | "dependencies": { 26 | "@testing-library/jest-dom": "5.8.0", 27 | "@testing-library/react": "10.0.4", 28 | "codecov": "3.7.0", 29 | "jest": "26.0.1", 30 | "jest-localstorage-mock": "2.4.2", 31 | "jest-plugin-filename": "0.0.1", 32 | "jest-runner-eslint": "0.8.0", 33 | "jest-watch-yarn-workspaces": "1.1.0" 34 | }, 35 | "devDependencies": { 36 | "read-pkg-up": "7.0.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 suǝʞǝǝpʇ 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 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/inject-feature-toggle/inject-feature-toggle.tsx: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlagVariation } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { 5 | wrapDisplayName, 6 | setDisplayName, 7 | DEFAULT_FLAG_PROP_KEY, 8 | } from '@flopflip/react'; 9 | import { useFlagVariations } from '../../hooks'; 10 | 11 | type InjectedProps = { 12 | [propKey: string]: TFlagVariation; 13 | }; 14 | 15 | export default function injectFeatureToggle( 16 | flagName: TFlagName, 17 | propKey: string = DEFAULT_FLAG_PROP_KEY 18 | ) { 19 | return ( 20 | Component: React.ComponentType 21 | ): React.ComponentType => { 22 | const WrappedComponent = (ownProps: OwnProps) => { 23 | const [flagVariation] = useFlagVariations([flagName]); 24 | const props = { 25 | ...ownProps, 26 | [propKey]: flagVariation, 27 | }; 28 | 29 | return ; 30 | }; 31 | 32 | setDisplayName(wrapDisplayName(WrappedComponent, 'injectFeatureToggle')); 33 | 34 | return WrappedComponent; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /demo/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import logger from 'redux-logger'; 4 | // Import adapter from '@flopflip/localstorage-adapter'; 5 | // import { createFlopFlipEnhancer } from '@flopflip/react-redux'; 6 | import rootReducer from './modules'; 7 | 8 | /* Const defaultFlags = { 'aDefault-Flag': true }; 9 | const adapterArgs = { 10 | clientSideId: '596788417a20200c2b70c89e', 11 | user: { key: 'ld-2@tdeekens.name' }, 12 | }; */ 13 | 14 | const initialState = {}; 15 | const enhancers = [ 16 | // NOTE: Comment in the line below to add the store enhancer. 17 | // createFlopFlipEnhancer(adapter, adapterArgs) 18 | ]; 19 | const middleware = [thunk, logger]; 20 | 21 | if (process.env.NODE_ENV === 'development') { 22 | const devToolsExtension = window.devToolsExtension; 23 | 24 | if (typeof devToolsExtension === 'function') { 25 | enhancers.push(devToolsExtension()); 26 | } 27 | } 28 | 29 | const composedEnhancers = compose(applyMiddleware(...middleware), ...enhancers); 30 | 31 | const store = createStore(rootReducer, initialState, composedEnhancers); 32 | 33 | export default store; 34 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/get-flag-variation/get-flag-variation.spec.js: -------------------------------------------------------------------------------- 1 | import warning from 'tiny-warning'; 2 | import getFlagVariation from './get-flag-variation'; 3 | 4 | jest.mock('tiny-warning'); 5 | 6 | describe('with existing flag variation', () => { 7 | describe('with flag variation', () => { 8 | const args = { fooFlag: 'foo-variation' }; 9 | 10 | it('should return the flag variation', () => { 11 | expect(getFlagVariation('fooFlag')(args)).toBe('foo-variation'); 12 | }); 13 | }); 14 | }); 15 | 16 | describe('with non normalized flag variation', () => { 17 | it('should return the flag variation', () => { 18 | const args = { fooFlag: true }; 19 | expect(getFlagVariation('foo-flag')(args)).toBe(true); 20 | }); 21 | 22 | it('should invoke `warning`', () => { 23 | const args = { fooFlag: false }; 24 | getFlagVariation('fooFlag')(args); 25 | 26 | expect(warning).toHaveBeenCalled(); 27 | }); 28 | }); 29 | 30 | describe('with non existing flag variation', () => { 31 | it('should indicate flag variation not existing', () => { 32 | const args = { fooFlag: true }; 33 | expect(getFlagVariation('fooFlag2')(args)).toBe(false); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "emitDecoratorMetadata": false, 6 | "experimentalDecorators": false, 7 | "esModuleInterop": true, 8 | "importHelpers": true, 9 | "jsx": "react", 10 | "lib": ["esnext", "dom"], 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": false, 15 | "noImplicitReturns": true, 16 | "resolveJsonModule": true, 17 | "noImplicitThis": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "pretty": true, 21 | "removeComments": true, 22 | "sourceMap": true, 23 | "strict": true, 24 | "strictNullChecks": true, 25 | "stripInternal": true, 26 | "target": "es5", 27 | "allowJs": false 28 | }, 29 | "include": [ 30 | "packages/**/*.ts", 31 | "packages/**/modules/**/*.ts", 32 | "packages/**/modules/**/*.tsx", 33 | "packages/**/modules/**/*.js", 34 | "packages/**/@types/**/*.d.ts", 35 | "packages/test-utils/*.js" 36 | ], 37 | "exclude": [ 38 | "packages/**/node_modules/*.d.ts", 39 | "node_modules/*.d.ts", 40 | "**/dist/**/*" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/inject-feature-toggles/inject-feature-toggles.tsx: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlags } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { 5 | wrapDisplayName, 6 | setDisplayName, 7 | DEFAULT_FLAGS_PROP_KEY, 8 | } from '@flopflip/react'; 9 | import { useFlagVariations } from '../../hooks'; 10 | 11 | type InjectedProps = { 12 | [propKey: string]: TFlags; 13 | }; 14 | 15 | export default ( 16 | flagNames: Readonly, 17 | propKey: string = DEFAULT_FLAGS_PROP_KEY 18 | ) => ( 19 | Component: React.ComponentType 20 | ): React.ComponentType => { 21 | const WrappedComponent = (ownProps: OwnProps) => { 22 | const flagVariations = useFlagVariations(flagNames); 23 | const flags = Object.fromEntries( 24 | flagNames.map((flagName, indexOfFlagName) => [ 25 | flagName, 26 | flagVariations[indexOfFlagName], 27 | ]) 28 | ); 29 | const props = { 30 | ...ownProps, 31 | [propKey]: flags, 32 | }; 33 | 34 | return ; 35 | }; 36 | 37 | setDisplayName(wrapDisplayName(WrappedComponent, 'injectFeatureToggles')); 38 | 39 | return WrappedComponent; 40 | }; 41 | -------------------------------------------------------------------------------- /jest.test.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'test', 3 | preset: 'ts-jest/presets/js-with-babel', 4 | // Without this option, somehow CI fails to run the tests with the following error: 5 | // TypeError: Unable to require `.d.ts` file. 6 | // This is usually the result of a faulty configuration or import. Make sure there is a `.js`, `.json` or another executable extension available alongside `core.ts`. 7 | // Fix is based on this comment: 8 | // - https://github.com/kulshekhar/ts-jest/issues/805#issuecomment-456055213 9 | // - https://github.com/kulshekhar/ts-jest/blob/master/docs/user/config/isolatedModules.md 10 | globals: { 11 | 'ts-jest': { 12 | isolatedModules: true, 13 | }, 14 | }, 15 | setupFiles: [ 16 | 'raf/polyfill', 17 | 'jest-localstorage-mock', 18 | './throwing-console-patch.js', 19 | ], 20 | setupFilesAfterEnv: ['./jest-runner-test.config.js'], 21 | moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], 22 | testEnvironment: 'jest-environment-jsdom-sixteen', 23 | testURL: 'http://localhost', 24 | watchPlugins: ['jest-plugin-filename', 'jest-watch-yarn-workspaces'], 25 | testPathIgnorePatterns: [ 26 | '/node_modules/', 27 | '/packages/.*/node_modules', 28 | '/packages/.*/dist', 29 | ], 30 | coveragePathIgnorePatterns: ['/node_modules/'], 31 | }; 32 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/inject-feature-toggles/inject-feature-toggles.tsx: -------------------------------------------------------------------------------- 1 | import type { TFlagName, TFlags } from '@flopflip/types'; 2 | 3 | import React from 'react'; 4 | import { 5 | wrapDisplayName, 6 | setDisplayName, 7 | DEFAULT_FLAGS_PROP_KEY, 8 | } from '@flopflip/react'; 9 | import { useFlagVariations } from '../../hooks'; 10 | 11 | type InjectedProps = { 12 | [propKey: string]: TFlags; 13 | }; 14 | 15 | export default function injectFeatureToggles( 16 | flagNames: Readonly, 17 | propKey: string = DEFAULT_FLAGS_PROP_KEY 18 | ) { 19 | return ( 20 | Component: React.ComponentType 21 | ): React.ComponentType => { 22 | const WrappedComponent = (ownProps: OwnProps) => { 23 | const flagVariations = useFlagVariations(flagNames); 24 | const flags = Object.fromEntries( 25 | flagNames.map((flagName, indexOfFlagName) => [ 26 | flagName, 27 | flagVariations[indexOfFlagName], 28 | ]) 29 | ); 30 | const props = { 31 | ...ownProps, 32 | [propKey]: flags, 33 | }; 34 | 35 | return ; 36 | }; 37 | 38 | setDisplayName(wrapDisplayName(WrappedComponent, 'injectFeatureToggles')); 39 | 40 | return WrappedComponent; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | sourceType: module 3 | ecmaVersion: 7 4 | ecmaFeatures: 5 | jsx: true 6 | modules: true 7 | extends: 8 | - xo 9 | - xo-typescript 10 | - xo-react 11 | - prettier 12 | - prettier/@typescript-eslint 13 | - prettier/react 14 | - plugin:jest/recommended 15 | - plugin:testing-library/react 16 | env: 17 | es6: true 18 | jest: true 19 | browser: true 20 | plugins: 21 | - prettier 22 | - jest 23 | rules: 24 | jest/no-disabled-tests: warn 25 | jest/no-focused-tests: error 26 | jest/no-identical-title: error 27 | jest/valid-expect: error 28 | max-nested-callbacks: 29 | - error 30 | - 20 31 | default-param-last: 0 32 | testing-library/prefer-presence-queries: 'error' 33 | testing-library/await-async-query: 'error' 34 | globals: 35 | VERSION: true 36 | overrides: 37 | - files: 38 | - '*.spec.js' 39 | - 'packages/test-utils/*.js' 40 | rules: 41 | react/prop-types: 0 42 | - files: 43 | - '*.js' 44 | rules: 45 | '@typescript-eslint/promise-function-async': 0 46 | '@typescript-eslint/no-unsafe-member-access': 0 47 | '@typescript-eslint/no-unsafe-call': 0 48 | '@typescript-eslint/no-unsafe-return': 0 49 | '@typescript-eslint/prefer-readonly-parameter-types': 0 50 | settings: 51 | react: 52 | version: 'detect' 53 | -------------------------------------------------------------------------------- /packages/react/modules/helpers/get-is-feature-enabled/get-is-feature-enabled.spec.js: -------------------------------------------------------------------------------- 1 | import getIsFeatureEnabled from './get-is-feature-enabled'; 2 | 3 | jest.mock('tiny-warning'); 4 | 5 | describe('with existing flag', () => { 6 | describe('with flag variation', () => { 7 | const args = { fooFlag: 'foo-variation' }; 8 | 9 | it('should indicate feature being enabled', () => { 10 | expect(getIsFeatureEnabled('fooFlag', 'foo-variation')(args)).toBe(true); 11 | }); 12 | 13 | it('should indicate feature being disabled', () => { 14 | expect(getIsFeatureEnabled('fooFlag', 'foo-variation-1')(args)).toBe( 15 | false 16 | ); 17 | }); 18 | }); 19 | 20 | describe('without flag variation', () => { 21 | it('should indicate feature being enabled', () => { 22 | const args = { fooFlag: true }; 23 | expect(getIsFeatureEnabled('fooFlag')(args)).toBe(true); 24 | }); 25 | 26 | it('should indicate feature being disabled', () => { 27 | const args = { fooFlag: false }; 28 | expect(getIsFeatureEnabled('fooFlag')(args)).toBe(false); 29 | }); 30 | }); 31 | }); 32 | 33 | describe('with non existing flag', () => { 34 | it('should indicate feature being disabled', () => { 35 | const args = { fooFlag: true }; 36 | expect(getIsFeatureEnabled('fooFlag2')(args)).toBe(false); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-feature-toggles/use-feature-toggles.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useFeatureToggles from './use-feature-toggles'; 3 | import { renderWithAdapter } from '@flopflip/test-utils'; 4 | import Configure from '../../components/configure'; 5 | 6 | const render = (TestComponent) => 7 | renderWithAdapter(TestComponent, { 8 | components: { ConfigureFlopFlip: Configure }, 9 | }); 10 | 11 | const TestComponent = () => { 12 | const [ 13 | isEnabledFeatureEnabled, 14 | isDisabledFeatureDisabled, 15 | ] = useFeatureToggles({ 16 | enabledFeature: true, 17 | disabledFeature: true, 18 | }); 19 | 20 | return ( 21 |
    22 |
  • Is enabled: {isEnabledFeatureEnabled ? 'Yes' : 'No'}
  • 23 |
  • Is disabled: {isDisabledFeatureDisabled ? 'No' : 'Yes'}
  • 24 |
25 | ); 26 | }; 27 | 28 | it('should indicate a feature being disabled', async () => { 29 | const rendered = render(); 30 | 31 | await rendered.waitUntilConfigured(); 32 | 33 | expect(rendered.getByText('Is disabled: Yes')).toBeInTheDocument(); 34 | }); 35 | 36 | it('should indicate a feature being enabled', async () => { 37 | const rendered = render(); 38 | 39 | await rendered.waitUntilConfigured(); 40 | 41 | expect(rendered.getByText('Is enabled: Yes')).toBeInTheDocument(); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-adapter-status/use-adapter-status.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderWithAdapter } from '@flopflip/test-utils'; 3 | import useAdapterStatus from './'; 4 | import Configure from '../../components/configure'; 5 | 6 | const render = (TestComponent) => 7 | renderWithAdapter(TestComponent, { 8 | components: { ConfigureFlopFlip: Configure }, 9 | }); 10 | 11 | const TestComponent = () => { 12 | const { isConfiguring, isConfigured } = useAdapterStatus(); 13 | 14 | return ( 15 |
    16 |
  • Is configuring: {isConfiguring ? 'Yes' : 'No'}
  • 17 |
  • Is configured: {isConfigured ? 'Yes' : 'No'}
  • 18 |
19 | ); 20 | }; 21 | 22 | it('should indicate the adapter not configured yet', async () => { 23 | const rendered = render(); 24 | 25 | expect(rendered.getByText(/Is configuring: Yes/i)).toBeInTheDocument(); 26 | expect(rendered.getByText(/Is configured: No/i)).toBeInTheDocument(); 27 | 28 | await rendered.waitUntilConfigured(); 29 | }); 30 | 31 | it('should indicate the adapter is configured', async () => { 32 | const rendered = render(); 33 | 34 | await rendered.waitUntilConfigured(); 35 | 36 | expect(rendered.getByText(/Is configuring: No/i)).toBeInTheDocument(); 37 | expect(rendered.getByText(/Is configured: Yes/i)).toBeInTheDocument(); 38 | }); 39 | -------------------------------------------------------------------------------- /bin/version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable */ 3 | 4 | const mri = require('mri'); 5 | const path = require('path'); 6 | const replace = require('replace'); 7 | 8 | const version = process.env.npm_package_version; 9 | const package = process.env.npm_package_name; 10 | const pwd = process.env.PWD; 11 | 12 | const flags = mri(process.argv.slice(2), { alias: { help: ['h'] } }); 13 | const commands = flags._; 14 | 15 | if (commands.length === 0 || (flags.help && commands.length === 0)) { 16 | console.log(` 17 | Usage: version [command] [options] 18 | Commands: 19 | print Print the version 20 | amend Amends the version to the built files 21 | `); 22 | process.exit(0); 23 | } 24 | 25 | const command = commands[0]; 26 | 27 | switch (command) { 28 | case 'print': { 29 | console.log(`Version for ${package} of release will be ${version}`); 30 | break; 31 | } 32 | case 'amend': { 33 | const distFolder = path.join(pwd, 'dist'); 34 | const paths = [distFolder]; 35 | 36 | replace({ 37 | regex: '__@FLOPFLIP/VERSION_OF_RELEASE__', 38 | replacement: version, 39 | paths, 40 | recursive: true, 41 | silent: true, 42 | }); 43 | 44 | console.log(`Amended for ${package} for release ${version}`); 45 | break; 46 | } 47 | default: 48 | console.log(`Unknown script "${command}".`); 49 | break; 50 | } 51 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-feature-toggle/use-feature-toggle.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useFeatureToggle from './use-feature-toggle'; 3 | import { renderWithAdapter } from '@flopflip/test-utils'; 4 | import Configure from '../../components/configure'; 5 | 6 | const render = (TestComponent) => 7 | renderWithAdapter(TestComponent, { 8 | components: { ConfigureFlopFlip: Configure }, 9 | }); 10 | 11 | const TestComponent = () => { 12 | const isEnabledFeatureEnabled = useFeatureToggle('enabledFeature'); 13 | const isDisabledFeatureDisabled = useFeatureToggle('disabledFeature'); 14 | const flagVariation = useFeatureToggle('variation', null); 15 | 16 | return ( 17 |
    18 |
  • Is enabled: {isEnabledFeatureEnabled ? 'Yes' : 'No'}
  • 19 |
  • Is disabled: {isDisabledFeatureDisabled ? 'No' : 'Yes'}
  • 20 |
  • Variation: {flagVariation}
  • 21 |
22 | ); 23 | }; 24 | 25 | it('should indicate a feature being disabled', async () => { 26 | const rendered = render(); 27 | 28 | await rendered.waitUntilConfigured(); 29 | 30 | expect(rendered.getByText('Is disabled: Yes')).toBeInTheDocument(); 31 | }); 32 | 33 | it('should indicate a feature being enabled', async () => { 34 | const rendered = render(); 35 | 36 | await rendered.waitUntilConfigured(); 37 | 38 | expect(rendered.getByText('Is enabled: Yes')).toBeInTheDocument(); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/react/modules/hooks/use-adapter-subscription/use-adapter-subscription.ts: -------------------------------------------------------------------------------- 1 | import type { TAdapter } from '@flopflip/types'; 2 | import { TAdapterSubscriptionStatus } from '@flopflip/types'; 3 | 4 | import React from 'react'; 5 | 6 | function useAdapterSubscription(adapter: TAdapter) { 7 | /** 8 | * NOTE: 9 | * This state needs to be duplicated in a React.ref 10 | * as under test multiple instances of flopflip might 11 | * be rendered. This yields in them competing in adapter 12 | * subscription state (e.g. A unsubscribing and B subscribing 13 | * which yields A and B being subscribed as the adapter 14 | * is a singleton). 15 | */ 16 | const useAdapterSubscriptionStatusRef = React.useRef( 17 | TAdapterSubscriptionStatus.Subscribed 18 | ); 19 | 20 | const { subscribe, unsubscribe } = adapter; 21 | 22 | React.useEffect(() => { 23 | if (subscribe) { 24 | subscribe(); 25 | } 26 | 27 | useAdapterSubscriptionStatusRef.current = 28 | TAdapterSubscriptionStatus.Subscribed; 29 | 30 | return () => { 31 | if (unsubscribe) { 32 | unsubscribe(); 33 | } 34 | 35 | useAdapterSubscriptionStatusRef.current = 36 | TAdapterSubscriptionStatus.Unsubscribed; 37 | }; 38 | }, [subscribe, unsubscribe]); 39 | 40 | return (demandedAdapterSubscriptionStatus: TAdapterSubscriptionStatus) => 41 | useAdapterSubscriptionStatusRef.current === 42 | demandedAdapterSubscriptionStatus; 43 | } 44 | 45 | export default useAdapterSubscription; 46 | -------------------------------------------------------------------------------- /packages/react-redux/modules/store/enhancer/enhancer.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Store, 3 | StoreEnhancerStoreCreator, 4 | Reducer, 5 | PreloadedState, 6 | } from 'redux'; 7 | import type { 8 | TAdapter, 9 | TAdapterArgs, 10 | TAdapterStatusChange, 11 | TFlagsChange, 12 | TAdapterInterface, 13 | } from '@flopflip/types'; 14 | import type { TState } from '../../types'; 15 | 16 | import { updateFlags, updateStatus } from '../../ducks'; 17 | 18 | export default function createFlopFlipEnhancer( 19 | adapter: TAdapter, 20 | adapterArgs: TAdapterArgs 21 | ): ( 22 | next: StoreEnhancerStoreCreator 23 | ) => ( 24 | reducer: Reducer, 25 | preloadedState?: PreloadedState 26 | ) => Store { 27 | // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types 28 | return (next) => (...args) => { 29 | const store: Store = next(...args); 30 | 31 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 32 | (adapter as TAdapterInterface).configure(adapterArgs, { 33 | // NOTE: This is like `bindActionCreators` but the bound action 34 | // creators are renamed to fit the adapter API and conventions. 35 | onFlagsStateChange: (flags: Readonly) => { 36 | store.dispatch(updateFlags(flags)); 37 | }, 38 | onStatusStateChange: (status: Readonly) => { 39 | store.dispatch(updateStatus(status)); 40 | }, 41 | }); 42 | 43 | return store; 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flopflip/types", 3 | "version": "2.5.3", 4 | "description": "Type definitions for flipflop", 5 | "main": "dist/@flopflip-types.cjs.js", 6 | "module": "dist/@flopflip-types.es.js", 7 | "typings": "dist/typings/index.d.ts", 8 | "scripts": { 9 | "prebuild": "rimraf dist/**", 10 | "build": "cross-env npm run build:es && npm run build:cjs && npm run build:typings", 11 | "build:typings": "cross-env tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/typings", 12 | "build:watch": "cross-env npm run build:es -- -w", 13 | "build:es": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f es -i index.ts -o dist/@flopflip-types.es.js", 14 | "build:cjs": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f cjs -i index.ts -o dist/@flopflip-types.cjs.js" 15 | }, 16 | "files": [ 17 | "readme.md", 18 | "dist/**" 19 | ], 20 | "types": "dist/typings/index.d.ts", 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/tdeekens/flopflip.git" 27 | }, 28 | "author": "Tobias Deekens ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/tdeekens/flopflip/issues" 32 | }, 33 | "homepage": "https://github.com/tdeekens/flopflip#readme", 34 | "keywords": [ 35 | "feature-flags", 36 | "feature-toggles", 37 | "types" 38 | ], 39 | "dependencies": { 40 | "ts-essentials": "6.0.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /throwing-console-patch.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors/safe'); 2 | 3 | const shouldSilenceWarnings = (...messages) => 4 | [].some((msgRegex) => messages.some((msg) => msgRegex.test(msg))); 5 | 6 | const shouldNotThrowWarnings = (...messages) => 7 | [].some((msgRegex) => messages.some((msg) => msgRegex.test(msg))); 8 | 9 | const logOrThrow = (log, method, messages) => { 10 | const warning = `console.${method} calls not allowed in tests`; 11 | if (process.env.CI) { 12 | if (shouldSilenceWarnings(messages)) return; 13 | 14 | log(warning, '\n', ...messages); 15 | 16 | // NOTE: That some warnings should be logged allowing us to refactor graceully 17 | // without having to introduce a breaking change. 18 | if (shouldNotThrowWarnings(messages)) return; 19 | 20 | throw new Error(...messages); 21 | } else { 22 | log(colors.bgYellow.black(' WARN '), warning, '\n', ...messages); 23 | } 24 | }; 25 | 26 | // eslint-disable-next-line no-console 27 | const logMessage = console.log; 28 | global.console.log = (...messages) => { 29 | logOrThrow(logMessage, 'log', messages); 30 | }; 31 | 32 | // eslint-disable-next-line no-console 33 | const logInfo = console.info; 34 | global.console.info = (...messages) => { 35 | logOrThrow(logInfo, 'info', messages); 36 | }; 37 | 38 | // eslint-disable-next-line no-console 39 | const logWarning = console.warn; 40 | global.console.warn = (...messages) => { 41 | logOrThrow(logWarning, 'warn', messages); 42 | }; 43 | 44 | // eslint-disable-next-line no-console 45 | const logError = console.error; 46 | global.console.error = (...messages) => { 47 | logOrThrow(logError, 'error', messages); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/hooks/use-flag-variations/use-flag-variations.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useFlagVariations from './use-flag-variations'; 3 | import { renderWithAdapter } from '@flopflip/test-utils'; 4 | import Configure from '../../components/configure'; 5 | 6 | const render = (TestComponent) => 7 | renderWithAdapter(TestComponent, { 8 | components: { ConfigureFlopFlip: Configure }, 9 | }); 10 | 11 | const TestComponent = () => { 12 | const [ 13 | isEnabledFeatureEnabled, 14 | isDisabledFeatureDisabled, 15 | variation, 16 | ] = useFlagVariations(['enabledFeature', 'disabledFeature', 'variation']); 17 | 18 | return ( 19 |
    20 |
  • Is enabled: {isEnabledFeatureEnabled ? 'Yes' : 'No'}
  • 21 |
  • Is disabled: {isDisabledFeatureDisabled ? 'No' : 'Yes'}
  • 22 |
  • Variation: {variation}
  • 23 |
24 | ); 25 | }; 26 | 27 | it('should indicate a feature being disabled', async () => { 28 | const rendered = render(); 29 | 30 | await rendered.waitUntilConfigured(); 31 | 32 | expect(rendered.getByText('Is disabled: Yes')).toBeInTheDocument(); 33 | }); 34 | 35 | it('should indicate a feature being enabled', async () => { 36 | const rendered = render(); 37 | 38 | await rendered.waitUntilConfigured(); 39 | 40 | expect(rendered.getByText('Is enabled: Yes')).toBeInTheDocument(); 41 | }); 42 | 43 | it('should indicate a flag variation', async () => { 44 | const rendered = render(); 45 | 46 | await rendered.waitUntilConfigured(); 47 | 48 | expect(rendered.getByText('Variation: A')).toBeInTheDocument(); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/react/modules/components/adapter-context/adapter-context.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | TReconfigureAdapter, 3 | TAdapterContext, 4 | TAdapterStatus, 5 | } from '@flopflip/types'; 6 | import { 7 | TAdapterSubscriptionStatus, 8 | TAdapterConfigurationStatus, 9 | } from '@flopflip/types'; 10 | 11 | import React from 'react'; 12 | 13 | const initialReconfigureAdapter: TReconfigureAdapter = () => undefined; 14 | const initialAdapterStatus: TAdapterStatus = { 15 | subscriptionStatus: TAdapterSubscriptionStatus.Subscribed, 16 | configurationStatus: TAdapterConfigurationStatus.Unconfigured, 17 | }; 18 | const createAdapterContext = ( 19 | reconfigure?: TReconfigureAdapter, 20 | status?: TAdapterStatus 21 | ): TAdapterContext => ({ 22 | reconfigure: reconfigure ?? initialReconfigureAdapter, 23 | status: status ?? initialAdapterStatus, 24 | }); 25 | 26 | const initialAdapterContext = createAdapterContext(); 27 | const AdapterContext = React.createContext(initialAdapterContext); 28 | 29 | const selectAdapterConfigurationStatus = ( 30 | configurationStatus?: TAdapterConfigurationStatus 31 | ) => ({ 32 | isReady: configurationStatus === TAdapterConfigurationStatus.Configured, 33 | isUnconfigured: 34 | configurationStatus === TAdapterConfigurationStatus.Unconfigured, 35 | isConfiguring: 36 | configurationStatus === TAdapterConfigurationStatus.Configuring, 37 | isConfigured: configurationStatus === TAdapterConfigurationStatus.Configured, 38 | }); 39 | 40 | const useAdapterContext = () => React.useContext(AdapterContext); 41 | 42 | export default AdapterContext; 43 | export { 44 | createAdapterContext, 45 | useAdapterContext, 46 | selectAdapterConfigurationStatus, 47 | }; 48 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test and Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | install: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [13.x, 14.x] 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: yarn cache directory 21 | id: yarn-cache 22 | run: echo "::set-output name=dir::$(yarn cache dir)" 23 | - name: Setup node_modules cache 24 | uses: actions/cache@v1 25 | with: 26 | path: ${{ steps.yarn-cache.outputs.dir }} 27 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-yarn- 30 | - name: Setup Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - name: Install 35 | run: yarn install --frozen-lockfile 36 | - name: Build 37 | run: yarn build 38 | - name: Lint 39 | run: yarn lint 40 | - name: Test 41 | if: startsWith(matrix.node-version, '13') 42 | run: yarn test:ci 43 | env: 44 | CI: true 45 | - name: Test (with coverage) 46 | if: startsWith(matrix.node-version, '14') 47 | run: yarn test:ci:coverage 48 | env: 49 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 50 | - name: Type check 51 | run: yarn type-check 52 | - name: Bundle size check 53 | run: yarn test:sizes 54 | env: 55 | BUNDLESIZE_GITHUB_TOKEN: ${{ secrets.BUNDLESIZE_GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /packages/react-redux/modules/ducks/status/status.ts: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly } from 'ts-essentials'; 2 | import type { TAdapterStatus, TAdapterStatusChange } from '@flopflip/types'; 3 | import { 4 | TAdapterSubscriptionStatus, 5 | TAdapterConfigurationStatus, 6 | } from '@flopflip/types'; 7 | 8 | import { selectAdapterConfigurationStatus } from '@flopflip/react'; 9 | import { TUpdateStatusAction } from './types'; 10 | import { TState } from '../../types'; 11 | import { STATE_SLICE } from '../../store/constants'; 12 | 13 | // Actions 14 | export const UPDATE_STATUS = '@flopflip/status/update'; 15 | 16 | const initialState: TAdapterStatus = { 17 | subscriptionStatus: TAdapterSubscriptionStatus.Subscribed, 18 | configurationStatus: TAdapterConfigurationStatus.Unconfigured, 19 | }; 20 | 21 | // Reducer 22 | const reducer = ( 23 | // eslint-disable-next-line @typescript-eslint/default-param-last 24 | state: Readonly = initialState, 25 | action: DeepReadonly 26 | ): TAdapterStatus => { 27 | switch (action.type) { 28 | case UPDATE_STATUS: 29 | return { 30 | ...state, 31 | ...action.payload.status, 32 | }; 33 | 34 | default: 35 | return state; 36 | } 37 | }; 38 | 39 | export default reducer; 40 | 41 | // Action Creators 42 | export const updateStatus = ( 43 | nextStatus: Readonly 44 | ): TUpdateStatusAction => ({ 45 | type: UPDATE_STATUS, 46 | payload: { status: nextStatus }, 47 | }); 48 | // Selectors 49 | export const selectStatus = (state: DeepReadonly) => { 50 | const { status } = state[STATE_SLICE]; 51 | 52 | return selectAdapterConfigurationStatus(status?.configurationStatus); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/configure/configure.tsx: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly } from 'ts-essentials'; 2 | import type { 3 | TFlags, 4 | TAdapter, 5 | TConfigureAdapterProps, 6 | TConfigureAdapterChildren, 7 | } from '@flopflip/types'; 8 | 9 | import React from 'react'; 10 | import { ConfigureAdapter, useAdapterSubscription } from '@flopflip/react'; 11 | import { useUpdateFlags, useUpdateStatus } from '../../hooks'; 12 | 13 | type BaseProps = { 14 | children?: TConfigureAdapterChildren; 15 | shouldDeferAdapterConfiguration?: boolean; 16 | defaultFlags?: TFlags; 17 | }; 18 | type Props = BaseProps & 19 | TConfigureAdapterProps; 20 | 21 | const defaultProps: Pick< 22 | BaseProps, 23 | 'defaultFlags' | 'shouldDeferAdapterConfiguration' 24 | > = { 25 | defaultFlags: {}, 26 | shouldDeferAdapterConfiguration: false, 27 | }; 28 | 29 | const Configure = ( 30 | props: DeepReadonly> 31 | ) => { 32 | const handleUpdateFlags = useUpdateFlags(); 33 | const handleUpdateStatus = useUpdateStatus(); 34 | 35 | useAdapterSubscription(props.adapter); 36 | 37 | return ( 38 | 46 | {props.children} 47 | 48 | ); 49 | }; 50 | 51 | Configure.displayName = 'ConfigureFlopflip'; 52 | Configure.defaultProps = defaultProps; 53 | 54 | export default Configure; 55 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /packages/memory-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flopflip/memory-adapter", 3 | "version": "1.7.3", 4 | "description": "An in memory adapter for flipflop", 5 | "main": "dist/@flopflip-memory-adapter.cjs.js", 6 | "module": "dist/@flopflip-memory-adapter.es.js", 7 | "typings": "dist/typings/index.d.ts", 8 | "scripts": { 9 | "prepare": "./../../bin/version.js amend", 10 | "prebuild": "rimraf dist/**", 11 | "build": "cross-env npm run build:es && npm run build:cjs && npm run build:typings", 12 | "build:typings": "cross-env tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/typings", 13 | "build:watch": "cross-env npm run build:es -- -w", 14 | "build:es": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f es -i modules/index.ts -o dist/@flopflip-memory-adapter.es.js", 15 | "build:cjs": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f cjs -i modules/index.ts -o dist/@flopflip-memory-adapter.cjs.js" 16 | }, 17 | "files": [ 18 | "readme.md", 19 | "dist/**" 20 | ], 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/tdeekens/flopflip.git" 27 | }, 28 | "author": "Tobias Deekens ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/tdeekens/flopflip/issues" 32 | }, 33 | "homepage": "https://github.com/tdeekens/flopflip#readme", 34 | "keywords": [ 35 | "feature-flags", 36 | "feature-toggles", 37 | "memory", 38 | "client" 39 | ], 40 | "dependencies": { 41 | "@babel/runtime": "7.9.6", 42 | "@flopflip/types": "^2.5.3", 43 | "lodash": "4.17.15", 44 | "mitt": "1.2.0", 45 | "tiny-warning": "1.0.3", 46 | "ts-essentials": "6.0.5" 47 | }, 48 | "devDependencies": { 49 | "read-pkg-up": "7.0.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/localstorage-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flopflip/localstorage-adapter", 3 | "version": "1.7.3", 4 | "description": "An localstorage adapter for flipflop", 5 | "main": "dist/@flopflip-localstorage-adapter.cjs.js", 6 | "module": "dist/@flopflip-localstorage-adapter.es.js", 7 | "typings": "dist/typings/index.d.ts", 8 | "scripts": { 9 | "prepare": "./../../bin/version.js amend", 10 | "prebuild": "rimraf dist/**", 11 | "build": "cross-env npm run build:es && npm run build:cjs && npm run build:typings", 12 | "build:typings": "cross-env tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/typings", 13 | "build:watch": "cross-env npm run build:es -- -w", 14 | "build:es": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f es -i modules/index.ts -o dist/@flopflip-localstorage-adapter.es.js", 15 | "build:cjs": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f cjs -i modules/index.ts -o dist/@flopflip-localstorage-adapter.cjs.js" 16 | }, 17 | "files": [ 18 | "readme.md", 19 | "dist/**" 20 | ], 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/tdeekens/flopflip.git" 27 | }, 28 | "author": "Tobias Deekens ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/tdeekens/flopflip/issues" 32 | }, 33 | "homepage": "https://github.com/tdeekens/flopflip#readme", 34 | "keywords": [ 35 | "feature-flags", 36 | "feature-toggles", 37 | "localstorage", 38 | "client" 39 | ], 40 | "dependencies": { 41 | "@babel/runtime": "7.9.6", 42 | "@flopflip/types": "^2.5.3", 43 | "mitt": "1.2.0", 44 | "tiny-warning": "1.0.3", 45 | "ts-essentials": "6.0.5" 46 | }, 47 | "devDependencies": { 48 | "read-pkg-up": "7.0.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-adapter-status/use-adapter-status.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderWithAdapter } from '@flopflip/test-utils'; 3 | import { Provider } from 'react-redux'; 4 | import { createStore } from '../../../test-utils'; 5 | import { STATE_SLICE } from '../../store/constants'; 6 | import useAdapterStatus from './use-adapter-status'; 7 | import Configure from '../../components/configure'; 8 | 9 | const render = (store, TestComponent) => 10 | renderWithAdapter(TestComponent, { 11 | components: { 12 | ConfigureFlopFlip: Configure, 13 | Wrapper: , 14 | }, 15 | }); 16 | 17 | const TestComponent = () => { 18 | const { isConfiguring, isConfigured } = useAdapterStatus(); 19 | 20 | return ( 21 |
    22 |
  • Is configuring: {isConfiguring ? 'Yes' : 'No'}
  • 23 |
  • Is configured: {isConfigured ? 'Yes' : 'No'}
  • 24 |
25 | ); 26 | }; 27 | 28 | it('should indicate the adapter not configured yet', async () => { 29 | const store = createStore({ 30 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 31 | }); 32 | 33 | const rendered = render(store, ); 34 | 35 | expect(rendered.getByText(/Is configured: No/i)).toBeInTheDocument(); 36 | expect(rendered.getByText(/Is configuring: Yes/i)).toBeInTheDocument(); 37 | 38 | await rendered.waitUntilConfigured(); 39 | }); 40 | 41 | it('should indicate the adapter is configured and not configuring any longer', async () => { 42 | const store = createStore({ 43 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 44 | }); 45 | 46 | const rendered = render(store, ); 47 | 48 | await rendered.waitUntilConfigured(); 49 | 50 | expect(rendered.getByText(/Is configured: Yes/i)).toBeInTheDocument(); 51 | expect(rendered.getByText(/Is configuring: No/i)).toBeInTheDocument(); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/react/modules/components/configure-adapter/helpers.spec.js: -------------------------------------------------------------------------------- 1 | import { mergeAdapterArgs } from './helpers'; 2 | 3 | describe('mergeAdapterArgs', () => { 4 | describe('when not `shouldOverwrite`', () => { 5 | const previousAdapterArgs = { 6 | 'some-prop': 'was-present', 7 | }; 8 | const nextAdapterArgs = { 9 | 'another-prop': 'is-added', 10 | }; 11 | 12 | it('should merge the next properties', () => { 13 | expect( 14 | mergeAdapterArgs(previousAdapterArgs, { 15 | adapterArgs: nextAdapterArgs, 16 | options: { shouldOverwrite: false }, 17 | }) 18 | ).toEqual(expect.objectContaining(nextAdapterArgs)); 19 | }); 20 | 21 | it('should keep the previous properties', () => { 22 | expect( 23 | mergeAdapterArgs(previousAdapterArgs, { 24 | adapterArgs: nextAdapterArgs, 25 | options: { shouldOverwrite: false }, 26 | }) 27 | ).toEqual(expect.objectContaining(previousAdapterArgs)); 28 | }); 29 | }); 30 | 31 | describe('when `shouldOverwrite`', () => { 32 | const previousAdapterArgs = { 33 | 'some-prop': 'was-present', 34 | }; 35 | const nextAdapterArgs = { 36 | 'another-prop': 'is-added', 37 | }; 38 | 39 | it('should merge the next properties', () => { 40 | expect( 41 | mergeAdapterArgs(previousAdapterArgs, { 42 | adapterArgs: nextAdapterArgs, 43 | options: { shouldOverwrite: true }, 44 | }) 45 | ).toEqual(expect.objectContaining(nextAdapterArgs)); 46 | }); 47 | 48 | it('should not keep the previous properties', () => { 49 | expect( 50 | mergeAdapterArgs(previousAdapterArgs, { 51 | adapterArgs: nextAdapterArgs, 52 | options: { shouldOverwrite: true }, 53 | }) 54 | ).not.toEqual(expect.objectContaining(previousAdapterArgs)); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-feature-toggle/use-feature-toggle.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import useFeatureToggle from './use-feature-toggle'; 4 | import { renderWithAdapter } from '@flopflip/test-utils'; 5 | import { createStore } from '../../../test-utils'; 6 | import { STATE_SLICE } from '../../store/constants'; 7 | import Configure from '../../components/configure'; 8 | 9 | jest.mock('tiny-warning'); 10 | 11 | const render = (store, TestComponent) => 12 | renderWithAdapter(TestComponent, { 13 | components: { 14 | ConfigureFlopFlip: Configure, 15 | Wrapper: , 16 | }, 17 | }); 18 | 19 | const TestComponent = () => { 20 | const isEnabledFeatureEnabled = useFeatureToggle('enabledFeature'); 21 | const isDisabledFeatureDisabled = useFeatureToggle('disabledFeature'); 22 | 23 | return ( 24 |
    25 |
  • Is enabled: {isEnabledFeatureEnabled ? 'Yes' : 'No'}
  • 26 |
  • Is disabled: {isDisabledFeatureDisabled ? 'No' : 'Yes'}
  • 27 |
28 | ); 29 | }; 30 | 31 | describe('when adapter is configured', () => { 32 | it('should indicate a feature being disabled', async () => { 33 | const store = createStore({ 34 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 35 | }); 36 | const rendered = render(store, ); 37 | 38 | await rendered.waitUntilConfigured(); 39 | 40 | expect(rendered.getByText('Is disabled: Yes')).toBeInTheDocument(); 41 | }); 42 | 43 | it('should indicate a feature being enabled', async () => { 44 | const store = createStore({ 45 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 46 | }); 47 | 48 | const rendered = render(store, ); 49 | 50 | await rendered.waitUntilConfigured(); 51 | 52 | expect(rendered.getByText('Is enabled: Yes')).toBeInTheDocument(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-feature-toggles/use-feature-toggles.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import useFeatureToggles from './use-feature-toggles'; 4 | import { renderWithAdapter } from '@flopflip/test-utils'; 5 | import { createStore } from '../../../test-utils'; 6 | import { STATE_SLICE } from '../../store/constants'; 7 | import Configure from '../../components/configure'; 8 | 9 | const render = (store, TestComponent) => 10 | renderWithAdapter(TestComponent, { 11 | components: { 12 | ConfigureFlopFlip: Configure, 13 | Wrapper: , 14 | }, 15 | }); 16 | 17 | const TestComponent = () => { 18 | const [ 19 | isEnabledFeatureEnabled, 20 | isDisabledFeatureDisabled, 21 | ] = useFeatureToggles({ 22 | enabledFeature: true, 23 | disabledFeature: true, 24 | }); 25 | 26 | return ( 27 |
    28 |
  • Is enabled: {isEnabledFeatureEnabled ? 'Yes' : 'No'}
  • 29 |
  • Is disabled: {isDisabledFeatureDisabled ? 'No' : 'Yes'}
  • 30 |
31 | ); 32 | }; 33 | 34 | describe('when adapter is configured', () => { 35 | it('should indicate a feature being disabled', async () => { 36 | const store = createStore({ 37 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 38 | }); 39 | 40 | const { getByText, waitUntilConfigured } = render(store, ); 41 | 42 | await waitUntilConfigured(); 43 | 44 | expect(getByText('Is disabled: Yes')).toBeInTheDocument(); 45 | }); 46 | 47 | it('should indicate a feature being enabled', async () => { 48 | const store = createStore({ 49 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 50 | }); 51 | 52 | const { getByText, waitUntilConfigured } = render(store, ); 53 | 54 | await waitUntilConfigured(); 55 | 56 | expect(getByText('Is enabled: Yes')).toBeInTheDocument(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: GitHub context 14 | run: echo "$GITHUB_CONTEXT" 15 | env: 16 | GITHUB_CONTEXT: ${{ toJson(github) }} 17 | - name: Checkout 18 | if: github.ref == 'refs/heads/master' 19 | uses: actions/checkout@v2 20 | with: 21 | ref: master 22 | fetch-depth: 50 23 | 24 | - name: Fetch all tags (for releases) 25 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 26 | 27 | - name: Read .nvmrc 28 | run: echo ::set-output name=NVMRC::$(cat .nvmrc) 29 | id: nvm 30 | 31 | - name: Setup Node (uses version in .nvmrc) 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: "${{ steps.nvm.outputs.NVMRC }}" 35 | 36 | - name: Get yarn cache 37 | id: yarn-cache 38 | run: echo "::set-output name=dir::$(yarn cache dir)" 39 | 40 | - uses: actions/cache@v1 41 | with: 42 | path: ${{ steps.yarn-cache.outputs.dir }} 43 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 44 | restore-keys: | 45 | ${{ runner.os }}-yarn- 46 | - name: Install dependencies 47 | run: yarn install --frozen-lockfile 48 | 49 | - name: Creating .npmrc 50 | run: | 51 | cat << EOF > "$HOME/.npmrc" 52 | email=nerd@tdeekens.name 53 | //registry.npmjs.org/:_authToken=$NPM_TOKEN 54 | EOF 55 | env: 56 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | 58 | - name: Create Release Pull Request or Publish to npm 59 | id: changesets 60 | uses: changesets/action@master 61 | with: 62 | publish: yarn release 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 65 | -------------------------------------------------------------------------------- /packages/react-redux/modules/ducks/flags/flags.ts: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly } from 'ts-essentials'; 2 | import type { 3 | TFlagName, 4 | TFlagVariation, 5 | TFlags, 6 | TFlagsChange, 7 | } from '@flopflip/types'; 8 | 9 | import { isNil } from '@flopflip/react'; 10 | import { TUpdateFlagsAction } from './types.js'; 11 | import { TState } from '../../types'; 12 | import { STATE_SLICE } from '../../store/constants'; 13 | import { Reducer } from 'redux'; 14 | 15 | // Actions 16 | export const UPDATE_FLAGS = '@flopflip/flags/update'; 17 | 18 | const initialState: TFlags = {}; 19 | 20 | // Reducer 21 | const reducer = ( 22 | // eslint-disable-next-line @typescript-eslint/default-param-last 23 | state: Readonly = initialState, 24 | action: DeepReadonly 25 | ): TFlags => { 26 | switch (action.type) { 27 | case UPDATE_FLAGS: 28 | return { 29 | ...state, 30 | ...action.payload.flags, 31 | }; 32 | 33 | default: 34 | return state; 35 | } 36 | }; 37 | 38 | export default reducer; 39 | 40 | export const createReducer = ( 41 | preloadedState: Readonly = initialState 42 | ): Reducer> => ( 43 | // eslint-disable-next-line @typescript-eslint/default-param-last 44 | state = preloadedState, 45 | action 46 | ) => reducer(state, action); 47 | 48 | // Action Creators 49 | export const updateFlags = ( 50 | flags: Readonly 51 | ): TUpdateFlagsAction => ({ 52 | type: UPDATE_FLAGS, 53 | payload: { flags }, 54 | }); 55 | 56 | // Selectors 57 | export const selectFlags = (state: DeepReadonly) => 58 | state[STATE_SLICE].flags ?? {}; 59 | export const selectFlag = ( 60 | flagName: TFlagName 61 | ): ((state: DeepReadonly) => TFlagVariation) => (state) => { 62 | const allFlags: TFlags = selectFlags(state); 63 | const flagValue: TFlagVariation = allFlags[flagName]; 64 | 65 | return isNil(flagValue) ? false : flagValue; 66 | }; 67 | -------------------------------------------------------------------------------- /packages/react/modules/components/adapter-context/adapter-context.spec.js: -------------------------------------------------------------------------------- 1 | import { TAdapterConfigurationStatus } from '@flopflip/types'; 2 | import { selectAdapterConfigurationStatus } from './adapter-context'; 3 | 4 | describe('selectAdapterConfigurationStatus', () => { 5 | describe('when configured', () => { 6 | it('should indicate ready state', () => { 7 | expect( 8 | selectAdapterConfigurationStatus(TAdapterConfigurationStatus.Configured) 9 | ).toEqual( 10 | expect.objectContaining({ 11 | isReady: true, 12 | }) 13 | ); 14 | }); 15 | it('should indicate configured state', () => { 16 | expect( 17 | selectAdapterConfigurationStatus(TAdapterConfigurationStatus.Configured) 18 | ).toEqual( 19 | expect.objectContaining({ 20 | isConfigured: true, 21 | }) 22 | ); 23 | }); 24 | }); 25 | describe('when configuring', () => { 26 | it('should indicate configuring state', () => { 27 | expect( 28 | selectAdapterConfigurationStatus( 29 | TAdapterConfigurationStatus.Configuring 30 | ) 31 | ).toEqual( 32 | expect.objectContaining({ 33 | isConfiguring: true, 34 | }) 35 | ); 36 | }); 37 | it('should not indicate configured state', () => { 38 | expect( 39 | selectAdapterConfigurationStatus( 40 | TAdapterConfigurationStatus.Configuring 41 | ) 42 | ).toEqual( 43 | expect.objectContaining({ 44 | isConfigured: false, 45 | }) 46 | ); 47 | }); 48 | }); 49 | describe('when unconfigured', () => { 50 | it('should indicate unconfigured', () => { 51 | expect( 52 | selectAdapterConfigurationStatus( 53 | TAdapterConfigurationStatus.Unconfigured 54 | ) 55 | ).toEqual( 56 | expect.objectContaining({ 57 | isUnconfigured: true, 58 | }) 59 | ); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/toggle-feature/toggle-feature.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderWithAdapter, components } from '@flopflip/test-utils'; 3 | import ToggleFeature from './toggle-feature'; 4 | import Configure from '../configure'; 5 | 6 | const render = (TestComponent) => 7 | renderWithAdapter(TestComponent, { 8 | components: { ConfigureFlopFlip: Configure }, 9 | }); 10 | const TestEnabledComponent = () => ( 11 | 12 | 13 | 14 | ); 15 | const TestDisabledComponent = () => ( 16 | 17 | 18 | 19 | ); 20 | 21 | describe('when feature is disabled', () => { 22 | it('should not render the component representing a enabled feature', async () => { 23 | const rendered = render(); 24 | 25 | expect(rendered.queryByFlagName('disabledFeature')).not.toBeInTheDocument(); 26 | 27 | await rendered.waitUntilConfigured(); 28 | }); 29 | 30 | describe('when enabling feature', () => { 31 | it('should render the component representing a enabled feature', async () => { 32 | const rendered = render(); 33 | 34 | await rendered.waitUntilConfigured(); 35 | 36 | rendered.changeFlagVariation('disabledFeature', true); 37 | 38 | expect(rendered.queryByFlagName('disabledFeature')).toBeInTheDocument(); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('when feature is enabled', () => { 44 | it('should render the component representing a enabled feature', async () => { 45 | const rendered = render(); 46 | 47 | await rendered.waitUntilConfigured(); 48 | 49 | expect(rendered.queryByFlagName('enabledFeature')).toHaveAttribute( 50 | 'data-flag-status', 51 | 'enabled' 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/splitio-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flopflip/splitio-adapter", 3 | "version": "1.8.3", 4 | "description": "A adapter around the split.io client for flipflop", 5 | "main": "dist/@flopflip-splitio-adapter.cjs.js", 6 | "module": "dist/@flopflip-splitio-adapter.es.js", 7 | "typings": "dist/typings/index.d.ts", 8 | "scripts": { 9 | "prepare": "./../../bin/version.js amend", 10 | "prebuild": "rimraf dist/**", 11 | "build": "cross-env npm run build:es && npm run build:cjs && npm run build:typings", 12 | "build:typings": "cross-env tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/typings", 13 | "build:watch": "cross-env npm run build:es -- -w", 14 | "build:es": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f es -i modules/index.ts -o dist/@flopflip-splitio-adapter.es.js", 15 | "build:cjs": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f cjs -i modules/index.ts -o dist/@flopflip-splitio-adapter.cjs.js" 16 | }, 17 | "files": [ 18 | "readme.md", 19 | "dist/**" 20 | ], 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/tdeekens/flopflip.git" 27 | }, 28 | "author": "Tobias Deekens ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/tdeekens/flopflip/issues" 32 | }, 33 | "homepage": "https://github.com/tdeekens/flopflip#readme", 34 | "devDependencies": { 35 | "@flopflip/types": "^2.5.3", 36 | "read-pkg-up": "7.0.1" 37 | }, 38 | "dependencies": { 39 | "@babel/runtime": "7.9.6", 40 | "@flopflip/types": "^2.5.3", 41 | "@splitsoftware/splitio": "10.12.1", 42 | "deepmerge": "4.2.2", 43 | "lodash": "4.17.15", 44 | "tiny-warning": "1.0.3", 45 | "ts-essentials": "6.0.5" 46 | }, 47 | "keywords": [ 48 | "feature-flags", 49 | "feature-toggles", 50 | "split.io", 51 | "client" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const isEnv = (env) => process.env.NODE_ENV === env; 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | '@babel/env', 7 | { 8 | useBuiltIns: 'entry', 9 | corejs: 3, 10 | ...(isEnv('test') 11 | ? { 12 | targets: { 13 | browsers: ['last 1 versions'], 14 | node: '8', 15 | }, 16 | modules: 'commonjs', 17 | } 18 | : { 19 | targets: { 20 | browsers: ['last 2 versions', 'ie >= 11'], 21 | }, 22 | modules: false, 23 | useBuiltIns: 'entry', 24 | include: ['transform-classes'], 25 | }), 26 | }, 27 | ], 28 | [ 29 | '@babel/preset-react', 30 | { 31 | development: isEnv('test'), 32 | useBuiltIns: true, 33 | }, 34 | ], 35 | '@babel/preset-typescript', 36 | ], 37 | plugins: [ 38 | '@babel/plugin-external-helpers', 39 | [ 40 | '@babel/plugin-proposal-class-properties', 41 | { 42 | loose: true, 43 | }, 44 | ], 45 | '@babel/plugin-proposal-export-default-from', 46 | '@babel/plugin-proposal-export-namespace-from', 47 | [ 48 | '@babel/plugin-proposal-object-rest-spread', 49 | { 50 | useBuiltIns: true, 51 | }, 52 | ], 53 | '@babel/plugin-syntax-dynamic-import', 54 | '@babel/plugin-transform-destructuring', 55 | '@babel/plugin-transform-react-constant-elements', 56 | '@babel/plugin-transform-runtime', 57 | '@babel/plugin-proposal-optional-chaining', 58 | '@babel/plugin-proposal-nullish-coalescing-operator', 59 | isEnv('test') && [ 60 | '@babel/plugin-transform-regenerator', 61 | { 62 | async: false, 63 | }, 64 | ], 65 | isEnv('test') && 'babel-plugin-transform-dynamic-import', 66 | isEnv('test') && '@babel/plugin-transform-modules-commonjs', 67 | ].filter(Boolean), 68 | }; 69 | -------------------------------------------------------------------------------- /packages/launchdarkly-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flopflip/launchdarkly-adapter", 3 | "version": "2.13.3", 4 | "description": "A adapter around the LaunchDarkly client for flipflop", 5 | "main": "dist/@flopflip-launchdarkly-adapter.cjs.js", 6 | "module": "dist/@flopflip-launchdarkly-adapter.es.js", 7 | "typings": "dist/typings/index.d.ts", 8 | "scripts": { 9 | "prepare": "./../../bin/version.js amend", 10 | "prebuild": "rimraf dist/**", 11 | "build": "cross-env npm run build:es && npm run build:cjs && npm run build:typings", 12 | "build:typings": "cross-env tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/typings", 13 | "build:watch": "cross-env npm run build:es -- -w", 14 | "build:es": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f es -i modules/index.ts -o dist/@flopflip-launchdarkly-adapter.es.js", 15 | "build:cjs": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f cjs -i modules/index.ts -o dist/@flopflip-launchdarkly-adapter.cjs.js" 16 | }, 17 | "files": [ 18 | "readme.md", 19 | "dist/**" 20 | ], 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/tdeekens/flopflip.git" 27 | }, 28 | "author": "Tobias Deekens ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/tdeekens/flopflip/issues" 32 | }, 33 | "homepage": "https://github.com/tdeekens/flopflip#readme", 34 | "devDependencies": { 35 | "@flopflip/types": "^2.5.3", 36 | "read-pkg-up": "7.0.1" 37 | }, 38 | "dependencies": { 39 | "@babel/runtime": "7.9.6", 40 | "@flopflip/types": "^2.5.3", 41 | "debounce-fn": "4.0.0", 42 | "deepmerge": "4.2.2", 43 | "launchdarkly-js-client-sdk": "2.17.5", 44 | "lodash": "4.17.15", 45 | "tiny-warning": "1.0.3", 46 | "ts-essentials": "6.0.5" 47 | }, 48 | "keywords": [ 49 | "feature-flags", 50 | "feature-toggles", 51 | "LaunchDarkly", 52 | "client" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /demo/src/modules/counter.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT_REQUESTED = 'counter/INCREMENT_REQUESTED'; 2 | export const INCREMENT = 'counter/INCREMENT'; 3 | export const DECREMENT_REQUESTED = 'counter/DECREMENT_REQUESTED'; 4 | export const DECREMENT = 'counter/DECREMENT'; 5 | 6 | const initialState = { 7 | count: 0, 8 | isIncrementing: false, 9 | isDecrementing: false, 10 | }; 11 | 12 | export default (state = initialState, action) => { 13 | switch (action.type) { 14 | case INCREMENT_REQUESTED: 15 | return { 16 | ...state, 17 | isIncrementing: true, 18 | }; 19 | 20 | case INCREMENT: 21 | return { 22 | ...state, 23 | count: state.count + 1, 24 | isIncrementing: !state.isIncrementing, 25 | }; 26 | 27 | case DECREMENT_REQUESTED: 28 | return { 29 | ...state, 30 | isDecrementing: true, 31 | }; 32 | 33 | case DECREMENT: 34 | return { 35 | ...state, 36 | count: state.count - 1, 37 | isDecrementing: !state.isDecrementing, 38 | }; 39 | 40 | default: 41 | return state; 42 | } 43 | }; 44 | 45 | export const increment = () => { 46 | return (dispatch) => { 47 | dispatch({ 48 | type: INCREMENT_REQUESTED, 49 | }); 50 | 51 | dispatch({ 52 | type: INCREMENT, 53 | }); 54 | }; 55 | }; 56 | 57 | export const incrementAsync = () => { 58 | return (dispatch) => { 59 | dispatch({ 60 | type: INCREMENT_REQUESTED, 61 | }); 62 | 63 | return setTimeout(() => { 64 | dispatch({ 65 | type: INCREMENT, 66 | }); 67 | }, 3000); 68 | }; 69 | }; 70 | 71 | export const decrement = () => { 72 | return (dispatch) => { 73 | dispatch({ 74 | type: DECREMENT_REQUESTED, 75 | }); 76 | 77 | dispatch({ 78 | type: DECREMENT, 79 | }); 80 | }; 81 | }; 82 | 83 | export const decrementAsync = () => { 84 | return (dispatch) => { 85 | dispatch({ 86 | type: DECREMENT_REQUESTED, 87 | }); 88 | 89 | return setTimeout(() => { 90 | dispatch({ 91 | type: DECREMENT, 92 | }); 93 | }, 3000); 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flopflip/react", 3 | "version": "9.1.14", 4 | "description": "A feature toggle wrapper to use LaunchDarkly with React", 5 | "main": "dist/@flopflip-react.cjs.js", 6 | "module": "dist/@flopflip-react.es.js", 7 | "browser": "dist/@flopflip-react.umd.js", 8 | "typings": "dist/typings/index.d.ts", 9 | "scripts": { 10 | "prepare": "./../../bin/version.js amend", 11 | "prebuild": "rimraf dist/**", 12 | "build": "cross-env npm run build:es && npm run build:cjs && npm run build:typings", 13 | "build:typings": "cross-env tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/typings", 14 | "build:watch": "cross-env npm run build:es -- -w", 15 | "build:es": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f es -i modules/index.ts -o dist/@flopflip-react.es.js", 16 | "build:cjs": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f cjs -i modules/index.ts -o dist/@flopflip-react.cjs.js" 17 | }, 18 | "files": [ 19 | "readme.md", 20 | "dist/**" 21 | ], 22 | "types": "dist/typings/index.d.ts", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/tdeekens/flopflip.git" 29 | }, 30 | "author": "Tobias Deekens ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/tdeekens/flopflip/issues" 34 | }, 35 | "homepage": "https://github.com/tdeekens/flopflip#readme", 36 | "devDependencies": { 37 | "@flopflip/types": "^2.5.3", 38 | "@types/react": "16.9.35", 39 | "@types/react-dom": "16.9.8", 40 | "react": "16.13.1", 41 | "react-dom": "16.13.1" 42 | }, 43 | "peerDependencies": { 44 | "react": "^16.8", 45 | "react-dom": "^16.8" 46 | }, 47 | "dependencies": { 48 | "@babel/runtime": "7.9.6", 49 | "@types/react-is": "16.7.1", 50 | "deepmerge": "4.2.2", 51 | "lodash": "4.17.15", 52 | "react-fast-compare": "3.1.1", 53 | "react-is": "16.13.1", 54 | "tiny-warning": "1.0.3", 55 | "ts-essentials": "6.0.5" 56 | }, 57 | "keywords": [ 58 | "react", 59 | "feature-flags", 60 | "feature-toggles", 61 | "LaunchDarkly", 62 | "client" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /packages/react/modules/components/toggle-feature/toggle-feature.ts: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly } from 'ts-essentials'; 2 | 3 | import React from 'react'; 4 | import warning from 'tiny-warning'; 5 | import { isValidElementType } from 'react-is'; 6 | 7 | type RenderFnArgs = Readonly<{ 8 | isFeatureEnabled: boolean; 9 | }>; 10 | type RenderFn = (args: RenderFnArgs) => React.ReactNode; 11 | export type Props = DeepReadonly<{ 12 | untoggledComponent?: React.ComponentType; 13 | toggledComponent?: React.ComponentType; 14 | render?: () => React.ReactNode; 15 | children?: RenderFn | React.ReactNode; 16 | isFeatureEnabled: boolean; 17 | }>; 18 | 19 | const ToggleFeature = (props: Props) => { 20 | if (props.untoggledComponent) 21 | warning( 22 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 23 | isValidElementType(props.untoggledComponent), 24 | `Invalid prop 'untoggledComponent' supplied to 'ToggleFeature': the prop is not a valid React component` 25 | ); 26 | 27 | if (props.toggledComponent) 28 | warning( 29 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 30 | isValidElementType(props.toggledComponent), 31 | `Invalid prop 'toggledComponent' supplied to 'ToggleFeature': the prop is not a valid React component` 32 | ); 33 | 34 | if (props.isFeatureEnabled) { 35 | if (props.toggledComponent) 36 | return React.createElement(props.toggledComponent); 37 | 38 | if (props.children) { 39 | if (typeof props.children === 'function') 40 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 41 | return props.children({ 42 | isFeatureEnabled: props.isFeatureEnabled, 43 | }); 44 | return React.Children.only(props.children); 45 | } 46 | 47 | if (typeof props.render === 'function') return props.render(); 48 | } 49 | 50 | if (typeof props.children === 'function') 51 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 52 | return props.children({ 53 | isFeatureEnabled: props.isFeatureEnabled, 54 | }); 55 | 56 | if (props.untoggledComponent) { 57 | return React.createElement(props.untoggledComponent); 58 | } 59 | 60 | return null; 61 | }; 62 | 63 | ToggleFeature.displayName = 'ToggleFeature'; 64 | 65 | export default ToggleFeature; 66 | -------------------------------------------------------------------------------- /packages/react-broadcast/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flopflip/react-broadcast", 3 | "version": "10.1.15", 4 | "description": "A feature toggle wrapper to use LaunchDarkly with React", 5 | "main": "dist/@flopflip-react-broadcast.cjs.js", 6 | "module": "dist/@flopflip-react-broadcast.es.js", 7 | "browser": "dist/@flopflip-react-broadcast.umd.js", 8 | "typings": "dist/typings/index.d.ts", 9 | "scripts": { 10 | "prebuild": "rimraf dist/**", 11 | "prepare": "./../../bin/version.js amend", 12 | "build": "cross-env npm run build:umd && npm run build:umd:min && npm run build:es && npm run build:cjs && npm run build:typings", 13 | "build:typings": "cross-env tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/typings", 14 | "build:watch": "cross-env npm run build:es -- -w", 15 | "build:umd": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f umd -i modules/index.ts -o dist/@flopflip-react-broadcast.umd.js", 16 | "build:umd:min": "cross-env NODE_ENV=production rollup -c ../../rollup.config.js -f umd -i modules/index.ts -o dist/@flopflip-react-broadcast.umd.min.js", 17 | "build:es": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f es -i modules/index.ts -o dist/@flopflip-react-broadcast.es.js", 18 | "build:cjs": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f cjs -i modules/index.ts -o dist/@flopflip-react-broadcast.cjs.js" 19 | }, 20 | "files": [ 21 | "readme.md", 22 | "dist/**" 23 | ], 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/tdeekens/flopflip.git" 30 | }, 31 | "author": "Tobias Deekens ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/tdeekens/flopflip/issues" 35 | }, 36 | "homepage": "https://github.com/tdeekens/flopflip#readme", 37 | "devDependencies": { 38 | "@types/react": "16.9.35", 39 | "@types/react-dom": "16.9.8", 40 | "react": "16.13.1", 41 | "react-dom": "16.13.1", 42 | "read-pkg-up": "7.0.1" 43 | }, 44 | "peerDependencies": { 45 | "react": "^16.8 || ^17.0", 46 | "react-dom": "^16.8 || ^17.0" 47 | }, 48 | "dependencies": { 49 | "@babel/runtime": "7.9.6", 50 | "@flopflip/react": "^9.1.12", 51 | "@flopflip/types": "^2.5.2", 52 | "lodash": "4.17.15" 53 | }, 54 | "keywords": [ 55 | "react", 56 | "feature-flags", 57 | "feature-toggles", 58 | "LaunchDarkly", 59 | "client" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /packages/react-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flopflip/react-redux", 3 | "version": "10.1.16", 4 | "description": "A feature toggle wrapper to use LaunchDarkly with React Redux", 5 | "main": "dist/@flopflip-react-redux.cjs.js", 6 | "module": "dist/@flopflip-react-redux.es.js", 7 | "browser": "dist/@flopflip-react-redux.umd.js", 8 | "typings": "dist/typings/index.d.ts", 9 | "scripts": { 10 | "prepare": "./../../bin/version.js amend", 11 | "prebuild": "rimraf dist/**", 12 | "build": "cross-env npm run build:umd && npm run build:umd:min && npm run build:es && npm run build:cjs && npm run build:typings", 13 | "build:typings": "cross-env tsc -p tsconfig.json --emitDeclarationOnly --declarationDir dist/typings", 14 | "build:watch": "cross-env npm run build:es -- -w", 15 | "build:umd": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f umd -i modules/index.ts -o dist/@flopflip-react-redux.umd.js", 16 | "build:umd:min": "cross-env NODE_ENV=production rollup -c ../../rollup.config.js -f umd -i modules/index.ts -o dist/@flopflip-react-redux.umd.min.js", 17 | "build:es": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f es -i modules/index.ts -o dist/@flopflip-react-redux.es.js", 18 | "build:cjs": "cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f cjs -i modules/index.ts -o dist/@flopflip-react-redux.cjs.js" 19 | }, 20 | "files": [ 21 | "readme.md", 22 | "dist/**" 23 | ], 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/tdeekens/flopflip.git" 30 | }, 31 | "author": "Tobias Deekens ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/tdeekens/flopflip/issues" 35 | }, 36 | "homepage": "https://github.com/tdeekens/flopflip#readme", 37 | "devDependencies": { 38 | "react": "16.13.1", 39 | "react-dom": "16.13.1", 40 | "react-redux": "7.2.0", 41 | "read-pkg-up": "7.0.1", 42 | "redux": "4.0.5" 43 | }, 44 | "dependencies": { 45 | "@babel/runtime": "7.9.6", 46 | "@flopflip/react": "^9.1.14", 47 | "@flopflip/types": "^2.5.3", 48 | "@types/react-redux": "7.1.9", 49 | "lodash": "4.17.15", 50 | "ts-essentials": "6.0.5" 51 | }, 52 | "peerDependencies": { 53 | "react": "^16.8 || ^17.0", 54 | "react-dom": "^16.8 || ^17.0", 55 | "react-redux": "^7.0.0", 56 | "redux": "^4.0" 57 | }, 58 | "keywords": [ 59 | "feature-flags", 60 | "feature-toggles", 61 | "LaunchDarkly", 62 | "client" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /demo/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/react-redux/modules/hooks/use-flag-variations/use-flag-variations.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import useFlagVariations from './use-flag-variations'; 4 | import { renderWithAdapter } from '@flopflip/test-utils'; 5 | import { createStore } from '../../../test-utils'; 6 | import { STATE_SLICE } from '../../store/constants'; 7 | import Configure from '../../components/configure'; 8 | 9 | const render = (store, TestComponent) => 10 | renderWithAdapter(TestComponent, { 11 | components: { 12 | ConfigureFlopFlip: Configure, 13 | Wrapper: , 14 | }, 15 | }); 16 | 17 | const TestComponent = () => { 18 | const [ 19 | isEnabledFeatureEnabled, 20 | isDisabledFeatureDisabled, 21 | variation, 22 | ] = useFlagVariations(['enabledFeature', 'disabledFeature', 'variation']); 23 | 24 | return ( 25 |
    26 |
  • Is enabled: {isEnabledFeatureEnabled ? 'Yes' : 'No'}
  • 27 |
  • Is disabled: {isDisabledFeatureDisabled ? 'No' : 'Yes'}
  • 28 |
  • Variation: {variation}
  • 29 |
30 | ); 31 | }; 32 | 33 | describe('when adaopter is configured', () => { 34 | it('should indicate a feature being disabled', async () => { 35 | const store = createStore({ 36 | [STATE_SLICE]: { 37 | flags: { 38 | enabledFeature: true, 39 | disabledFeature: false, 40 | variation: 'A', 41 | }, 42 | }, 43 | }); 44 | 45 | const rendered = render(store, ); 46 | 47 | await rendered.waitUntilConfigured(); 48 | 49 | expect(rendered.getByText('Is disabled: Yes')).toBeInTheDocument(); 50 | }); 51 | 52 | it('should indicate a feature being enabled', async () => { 53 | const store = createStore({ 54 | [STATE_SLICE]: { 55 | flags: { 56 | enabledFeature: true, 57 | disabledFeature: false, 58 | variation: 'A', 59 | }, 60 | }, 61 | }); 62 | 63 | const rendered = render(store, ); 64 | 65 | await rendered.waitUntilConfigured(); 66 | 67 | expect(rendered.getByText('Is enabled: Yes')).toBeInTheDocument(); 68 | }); 69 | 70 | it('should indicate a flag variation', async () => { 71 | const store = createStore({ 72 | [STATE_SLICE]: { 73 | flags: { 74 | enabledFeature: true, 75 | disabledFeature: false, 76 | variation: 'A', 77 | }, 78 | }, 79 | }); 80 | 81 | const rendered = render(store, ); 82 | 83 | await rendered.waitUntilConfigured(); 84 | 85 | expect(rendered.getByText('Variation: A')).toBeInTheDocument(); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/inject-feature-toggle/inject-feature-toggle.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderWithAdapter, components } from '@flopflip/test-utils'; 3 | import injectFeatureToggle from './inject-feature-toggle'; 4 | import Configure from '../configure'; 5 | 6 | const render = (TestComponent) => 7 | renderWithAdapter(TestComponent, { 8 | components: { ConfigureFlopFlip: Configure }, 9 | }); 10 | 11 | describe('without `propKey`', () => { 12 | describe('when feature is disabled', () => { 13 | it('should render receive the flag value as `false`', async () => { 14 | const TestComponent = injectFeatureToggle('disabledFeature')( 15 | components.FlagsToComponent 16 | ); 17 | 18 | const rendered = render(); 19 | 20 | expect(rendered.queryByFlagName('isFeatureEnabled')).toHaveTextContent( 21 | 'false' 22 | ); 23 | 24 | await rendered.waitUntilConfigured(); 25 | }); 26 | 27 | describe('when enabling feature', () => { 28 | it('should render the component representing a enabled feature', async () => { 29 | const TestComponent = injectFeatureToggle('disabledFeature')( 30 | components.FlagsToComponent 31 | ); 32 | 33 | const rendered = render(); 34 | 35 | await rendered.waitUntilConfigured(); 36 | 37 | rendered.changeFlagVariation('disabledFeature', true); 38 | 39 | expect(rendered.queryByFlagName('isFeatureEnabled')).toHaveTextContent( 40 | 'true' 41 | ); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('when feature is enabled', () => { 47 | it('should render receive the flag value as `true`', async () => { 48 | const TestComponent = injectFeatureToggle('enabledFeature')( 49 | components.FlagsToComponent 50 | ); 51 | 52 | const rendered = render(); 53 | 54 | await rendered.waitUntilConfigured(); 55 | 56 | expect(rendered.queryByFlagName('isFeatureEnabled')).toHaveTextContent( 57 | 'true' 58 | ); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('with `propKey`', () => { 64 | describe('when feature is disabled', () => { 65 | it('should render receive the flag value as `false`', async () => { 66 | const TestComponent = injectFeatureToggle( 67 | 'disabledFeature', 68 | 'customPropKey' 69 | )(components.FlagsToComponent); 70 | const rendered = render(); 71 | 72 | await rendered.waitUntilConfigured(); 73 | 74 | expect(rendered.queryByFlagName('customPropKey')).toHaveTextContent( 75 | 'false' 76 | ); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const readPkgUp = require('read-pkg-up'); 3 | const { nodeResolve } = require('@rollup/plugin-node-resolve'); 4 | const commonjs = require('@rollup/plugin-commonjs'); 5 | const { babel } = require('@rollup/plugin-babel'); 6 | const replace = require('@rollup/plugin-replace'); 7 | const json = require('@rollup/plugin-json'); 8 | const { terser } = require('rollup-plugin-terser'); 9 | const builtins = require('rollup-plugin-node-builtins'); 10 | const globals = require('rollup-plugin-node-globals'); 11 | const filesize = require('rollup-plugin-filesize'); 12 | const babelOptions = require('./babel.config'); 13 | const { packageJson: pkg } = readPkgUp.sync({ 14 | cwd: fs.realpathSync(process.cwd()), 15 | }); 16 | 17 | const env = process.env.NODE_ENV; 18 | const name = process.env.npm_package_name; 19 | const format = process.env.npm_lifecycle_event.split(':')[1]; 20 | const extensions = ['.js', '.ts', '.tsx', '.es', '.mjs']; 21 | 22 | const pkgDependencies = Object.keys(pkg.dependencies || {}); 23 | const pkgPeerDependencies = Object.keys(pkg.peerDependencies || {}); 24 | const pkgOptionalDependencies = Object.keys(pkg.optionalDependencies || {}); 25 | 26 | /** 27 | * Note: 28 | * Given we do not bundle for UMD 29 | * then all dependencies are considered external as they 30 | * will be "bundled" by the consumers bundler (e.g. webpack) or 31 | * resolved by Node.js. 32 | */ 33 | const externalDependencies = 34 | format === 'umd' 35 | ? pkgPeerDependencies 36 | : pkgDependencies 37 | .concat(pkgPeerDependencies) 38 | .concat(pkgOptionalDependencies); 39 | 40 | const config = { 41 | output: { 42 | name, 43 | sourcemap: true, 44 | exports: 'named', 45 | globals: { 46 | react: 'React', 47 | redux: 'redux', 48 | 'react-redux': 'react-redux', 49 | }, 50 | }, 51 | external: externalDependencies, 52 | plugins: [ 53 | replace({ 54 | 'process.env.NODE_ENV': JSON.stringify(env), 55 | }), 56 | globals(), 57 | builtins(), 58 | json(), 59 | nodeResolve({ 60 | extensions, 61 | mainFields: ['module', 'main', 'jsnext'], 62 | preferBuiltins: true, 63 | modulesOnly: true, 64 | }), 65 | babel({ 66 | exclude: '**/node_modules/**', 67 | extensions, 68 | babelHelpers: 'runtime', 69 | ...babelOptions, 70 | }), 71 | commonjs({ 72 | extensions, 73 | ignoreGlobal: true, 74 | exclude: ['packages/**'], 75 | include: 'node_modules/**', 76 | namedExports: { 77 | 'node_modules/react-is/index.js': ['isValidElementType'], 78 | }, 79 | }), 80 | filesize(), 81 | ], 82 | }; 83 | 84 | if (env === 'production') { 85 | config.plugins.push(terser()); 86 | } 87 | 88 | module.exports = config; 89 | -------------------------------------------------------------------------------- /packages/react-redux/modules/store/enhancer/enhancer.spec.js: -------------------------------------------------------------------------------- 1 | import { TAdapterConfigurationStatus } from '@flopflip/types'; 2 | import { updateFlags, updateStatus } from '../../ducks'; 3 | import createFlopFlipEnhancer from './enhancer'; 4 | 5 | const adapterArgs = { 6 | clientSideId: '123-abc', 7 | user: { key: 'foo-user' }, 8 | }; 9 | const adapter = { 10 | configure: jest.fn(), 11 | reconfigure: jest.fn(), 12 | }; 13 | 14 | describe('when creating enhancer', () => { 15 | let enhancer; 16 | beforeEach(() => { 17 | enhancer = createFlopFlipEnhancer(adapter, adapterArgs); 18 | }); 19 | 20 | describe('with enhanced store', () => { 21 | let dispatch; 22 | 23 | beforeEach(() => { 24 | dispatch = jest.fn(); 25 | 26 | const getState = () => ({}); 27 | const next = jest.fn(() => ({ getState, dispatch })); 28 | const args = ['']; 29 | 30 | enhancer(next)(args); 31 | }); 32 | 33 | it('should invoke `configure` on `adapter` with `onFlagsStateChange`', () => { 34 | expect(adapter.configure).toHaveBeenCalledWith( 35 | adapterArgs, 36 | expect.objectContaining({ 37 | onFlagsStateChange: expect.any(Function), 38 | }) 39 | ); 40 | }); 41 | 42 | it('should invoke `configure` on `adapter` with `onStatusStateChange`', () => { 43 | expect(adapter.configure).toHaveBeenCalledWith( 44 | adapterArgs, 45 | expect.objectContaining({ 46 | onStatusStateChange: expect.any(Function), 47 | }) 48 | ); 49 | }); 50 | 51 | describe('when invoking `onFlagsStateChange`', () => { 52 | let nextFlags = { 53 | foo: true, 54 | }; 55 | 56 | beforeEach(() => { 57 | const { onFlagsStateChange } = adapter.configure.mock.calls[ 58 | adapter.configure.mock.calls.length - 1 59 | ][1]; 60 | 61 | onFlagsStateChange(nextFlags); 62 | }); 63 | 64 | it('should invoke `dispatch`', () => { 65 | expect(dispatch).toHaveBeenCalled(); 66 | }); 67 | 68 | it('should invoke `dispatch` with `updateFlags`', () => { 69 | expect(dispatch).toHaveBeenCalledWith(updateFlags(nextFlags)); 70 | }); 71 | }); 72 | 73 | describe('when invoking `onStatusStateChange`', () => { 74 | let nextStatus = { 75 | adapterConfigurationStatus: TAdapterConfigurationStatus.Configured, 76 | }; 77 | 78 | beforeEach(() => { 79 | const { onStatusStateChange } = adapter.configure.mock.calls[ 80 | adapter.configure.mock.calls.length - 1 81 | ][1]; 82 | 83 | onStatusStateChange(nextStatus); 84 | }); 85 | 86 | it('should invoke `dispatch`', () => { 87 | expect(dispatch).toHaveBeenCalled(); 88 | }); 89 | 90 | it('should invoke `dispatch` with `updateStatus`', () => { 91 | expect(dispatch).toHaveBeenCalledWith(updateStatus(nextStatus)); 92 | }); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/toggle-feature/toggle-feature.spec.js: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux'; 2 | import { renderWithAdapter, components } from '@flopflip/test-utils'; 3 | import React from 'react'; 4 | import { createStore } from '../../../test-utils'; 5 | import Configure from '../configure'; 6 | import { STATE_SLICE } from './../../store/constants'; 7 | import ToggleFeature from './toggle-feature'; 8 | 9 | const render = (store, TestComponent) => 10 | renderWithAdapter(TestComponent, { 11 | components: { 12 | ConfigureFlopFlip: Configure, 13 | Wrapper: , 14 | }, 15 | }); 16 | 17 | describe('', () => { 18 | describe('when feature is disabled', () => { 19 | it('should not render the component representing a enabled feature', async () => { 20 | const TestComponent = () => ( 21 | 22 | 23 | 24 | ); 25 | const store = createStore({ 26 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 27 | }); 28 | 29 | const rendered = render(store, ); 30 | 31 | await rendered.waitUntilConfigured(); 32 | 33 | expect( 34 | rendered.queryByFlagName('disabledFeature') 35 | ).not.toBeInTheDocument(); 36 | }); 37 | 38 | describe('when enabling feature', () => { 39 | it('should render the component representing a enabled feature', async () => { 40 | const TestComponent = () => ( 41 | 42 | 43 | 44 | ); 45 | const store = createStore({ 46 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 47 | }); 48 | 49 | const rendered = render(store, ); 50 | 51 | await rendered.waitUntilConfigured(); 52 | 53 | rendered.changeFlagVariation('disabledFeature', true); 54 | 55 | expect(rendered.queryByFlagName('disabledFeature')).toBeInTheDocument(); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('when feature is enabled', () => { 61 | it('should render the component representing a enabled feature', async () => { 62 | const store = createStore({ 63 | [STATE_SLICE]: { flags: { enabledFeature: true } }, 64 | }); 65 | const TestComponent = () => ( 66 | 67 | 68 | 69 | ); 70 | 71 | const rendered = render(store, ); 72 | 73 | await rendered.waitUntilConfigured(); 74 | 75 | expect(rendered.queryByFlagName('enabledFeature')).toHaveAttribute( 76 | 'data-flag-status', 77 | 'enabled' 78 | ); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /packages/react/modules/hooks/use-adapter-subscription/use-adapter-subscription.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | TAdapterSubscriptionStatus, 3 | TAdapterConfigurationStatus, 4 | } from '@flopflip/types'; 5 | import React from 'react'; 6 | import { render as rtlRender } from '@flopflip/test-utils'; 7 | import useAdapterSubscription from './use-adapter-subscription'; 8 | 9 | const createAdapter = () => ({ 10 | getIsConfigurationStatus: jest.fn( 11 | () => TAdapterConfigurationStatus.Unconfigured 12 | ), 13 | configure: jest.fn(() => Promise.resolve()), 14 | reconfigure: jest.fn(() => Promise.resolve()), 15 | subscribe: jest.fn(), 16 | unsubscribe: jest.fn(), 17 | }); 18 | 19 | const TestComponent = (props) => { 20 | const getHasAdapterSubscriptionStatus = useAdapterSubscription(props.adapter); 21 | 22 | const isConfigured = props.adapter.getIsConfigurationStatus( 23 | TAdapterConfigurationStatus.Configured 24 | ); 25 | 26 | return ( 27 | <> 28 |

Test Component

; 29 |
    30 |
  • Is configured: {isConfigured ? 'Yes' : 'No'}
  • 31 |
  • 32 | Is subscribed:{' '} 33 | {getHasAdapterSubscriptionStatus( 34 | TAdapterSubscriptionStatus.Subscribed 35 | ) 36 | ? 'Yes' 37 | : 'No'} 38 |
  • 39 |
  • 40 | Is unsubscribed:{' '} 41 | {getHasAdapterSubscriptionStatus( 42 | TAdapterSubscriptionStatus.Unsubscribed 43 | ) 44 | ? 'Yes' 45 | : 'No'} 46 |
  • 47 |
48 | 49 | ); 50 | }; 51 | 52 | const render = ({ adapter }) => { 53 | const props = { adapter }; 54 | const rendered = rtlRender(); 55 | const waitUntilConfigured = () => Promise.resolve(); 56 | 57 | return { ...rendered, waitUntilConfigured, props }; 58 | }; 59 | 60 | describe('rendering', () => { 61 | it('should unsubscribe the adapter when mounting', async () => { 62 | const adapter = createAdapter(); 63 | 64 | const rendered = render({ adapter }); 65 | 66 | await rendered.waitUntilConfigured(); 67 | 68 | expect(rendered.props.adapter.subscribe).toHaveBeenCalled(); 69 | }); 70 | 71 | it('should return adapter subscribtion status indicating being subscribed', async () => { 72 | const adapter = createAdapter(); 73 | 74 | const rendered = render({ adapter }); 75 | 76 | await rendered.waitUntilConfigured(); 77 | 78 | expect(rendered.getByText(/Is subscribed: Yes/i)).toBeInTheDocument(); 79 | expect(rendered.getByText(/Is unsubscribed: No/i)).toBeInTheDocument(); 80 | }); 81 | 82 | it('should unsubscribe the adapter when unmounting', async () => { 83 | const adapter = createAdapter(); 84 | 85 | const rendered = render({ adapter }); 86 | 87 | await rendered.waitUntilConfigured(); 88 | 89 | rendered.unmount(); 90 | 91 | expect(rendered.props.adapter.unsubscribe).toHaveBeenCalled(); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /packages/react-redux/modules/ducks/status/status.spec.js: -------------------------------------------------------------------------------- 1 | import { TAdapterConfigurationStatus } from '@flopflip/types'; 2 | import { STATE_SLICE } from '../../store/constants'; 3 | import reducer, { UPDATE_STATUS, updateStatus, selectStatus } from './status'; 4 | 5 | describe('constants', () => { 6 | it('should contain `UPDATE_STATUS`', () => { 7 | expect(UPDATE_STATUS).toEqual('@flopflip/status/update'); 8 | }); 9 | }); 10 | 11 | describe('action creators', () => { 12 | describe('when updating status', () => { 13 | it('should return `UPDATE_STATUS` type', () => { 14 | expect(updateStatus({ isReady: false })).toEqual({ 15 | type: UPDATE_STATUS, 16 | payload: expect.any(Object), 17 | }); 18 | }); 19 | 20 | it('should return passed configuration status', () => { 21 | expect( 22 | updateStatus({ 23 | configurationStatus: TAdapterConfigurationStatus.Configured, 24 | }) 25 | ).toEqual({ 26 | type: expect.any(String), 27 | payload: { 28 | status: { 29 | configurationStatus: TAdapterConfigurationStatus.Configured, 30 | }, 31 | }, 32 | }); 33 | }); 34 | }); 35 | }); 36 | 37 | describe('reducers', () => { 38 | describe('when updating status', () => { 39 | describe('without previous status', () => { 40 | let payload; 41 | beforeEach(() => { 42 | payload = { 43 | status: { 44 | configurationStatus: TAdapterConfigurationStatus.Configuring, 45 | }, 46 | }; 47 | }); 48 | 49 | it('should set the new status', () => { 50 | expect(reducer(undefined, { type: UPDATE_STATUS, payload })).toEqual( 51 | expect.objectContaining({ 52 | configurationStatus: TAdapterConfigurationStatus.Configuring, 53 | }) 54 | ); 55 | }); 56 | }); 57 | 58 | describe('with previous status', () => { 59 | let payload; 60 | beforeEach(() => { 61 | payload = { 62 | status: { 63 | configurationStatus: TAdapterConfigurationStatus.Configuring, 64 | }, 65 | }; 66 | }); 67 | 68 | it('should set the new status', () => { 69 | expect( 70 | reducer( 71 | { configurationStatus: TAdapterConfigurationStatus.Configured }, 72 | { type: UPDATE_STATUS, payload } 73 | ) 74 | ).toEqual({ 75 | configurationStatus: TAdapterConfigurationStatus.Configuring, 76 | }); 77 | }); 78 | }); 79 | }); 80 | }); 81 | 82 | describe('selectors', () => { 83 | let status; 84 | let state; 85 | 86 | beforeEach(() => { 87 | status = { 88 | configurationStatus: TAdapterConfigurationStatus.Configuring, 89 | subscriptionStatus: {}, 90 | }; 91 | state = { 92 | [STATE_SLICE]: { 93 | status, 94 | }, 95 | }; 96 | }); 97 | 98 | describe('selecting status', () => { 99 | it('should return configuration and ready status', () => { 100 | expect(selectStatus(state)).toEqual( 101 | expect.objectContaining({ 102 | isConfiguring: true, 103 | isConfigured: false, 104 | }) 105 | ); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/inject-feature-toggles/inject-feature-toggles.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderWithAdapter, components } from '@flopflip/test-utils'; 3 | import injectFeatureToggles from './inject-feature-toggles'; 4 | import Configure from '../configure'; 5 | 6 | const render = (TestComponent) => 7 | renderWithAdapter(TestComponent, { 8 | components: { ConfigureFlopFlip: Configure }, 9 | }); 10 | 11 | const FlagsToComponent = (props) => ( 12 | 13 | ); 14 | const FlagsToComponentWithPropKey = (props) => ( 15 | 16 | ); 17 | 18 | describe('without `propKey`', () => { 19 | it('should have feature enabling prop for `enabledFeature`', async () => { 20 | const TestComponent = injectFeatureToggles([ 21 | 'disabledFeature', 22 | 'enabledFeature', 23 | ])(FlagsToComponent); 24 | 25 | const rendered = render(); 26 | 27 | await rendered.waitUntilConfigured(); 28 | 29 | expect(rendered.queryByFlagName('enabledFeature')).toHaveTextContent( 30 | 'true' 31 | ); 32 | }); 33 | 34 | it('should have feature disabling prop for `disabledFeature`', async () => { 35 | const TestComponent = injectFeatureToggles([ 36 | 'disabledFeature', 37 | 'enabledFeature', 38 | ])(FlagsToComponent); 39 | 40 | const rendered = render(); 41 | 42 | await rendered.waitUntilConfigured(); 43 | 44 | expect(rendered.queryByFlagName('disabledFeature')).toHaveTextContent( 45 | 'false' 46 | ); 47 | }); 48 | 49 | describe('when enabling feature', () => { 50 | it('should render the component representing a enabled feature', async () => { 51 | const TestComponent = injectFeatureToggles([ 52 | 'disabledFeature', 53 | 'enabledFeature', 54 | ])(FlagsToComponent); 55 | 56 | const rendered = render(); 57 | 58 | await rendered.waitUntilConfigured(); 59 | 60 | rendered.changeFlagVariation('disabledFeature', true); 61 | 62 | expect(rendered.queryByFlagName('disabledFeature')).toHaveTextContent( 63 | 'true' 64 | ); 65 | }); 66 | }); 67 | }); 68 | 69 | describe('with `propKey`', () => { 70 | it('should have feature enabling prop for `enabledFeature`', async () => { 71 | const TestComponent = injectFeatureToggles( 72 | ['disabledFeature', 'enabledFeature'], 73 | 'onOffs' 74 | )(FlagsToComponentWithPropKey); 75 | 76 | const rendered = render(); 77 | 78 | await rendered.waitUntilConfigured(); 79 | 80 | expect(rendered.queryByFlagName('enabledFeature')).toHaveTextContent( 81 | 'true' 82 | ); 83 | }); 84 | 85 | it('should have feature disabling prop for `disabledFeature`', async () => { 86 | const TestComponent = injectFeatureToggles( 87 | ['disabledFeature', 'enabledFeature'], 88 | 'onOffs' 89 | )(FlagsToComponentWithPropKey); 90 | 91 | const rendered = render(); 92 | 93 | await rendered.waitUntilConfigured(); 94 | 95 | expect(rendered.queryByFlagName('disabledFeature')).toHaveTextContent( 96 | 'false' 97 | ); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/inject-feature-toggle/inject-feature-toggle.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderWithAdapter, components } from '@flopflip/test-utils'; 3 | import { Provider } from 'react-redux'; 4 | import { createStore } from '../../../test-utils'; 5 | import { STATE_SLICE } from '../../store/constants'; 6 | import injectFeatureToggle from './inject-feature-toggle'; 7 | import Configure from '../configure'; 8 | 9 | const render = (store, TestComponent) => 10 | renderWithAdapter(TestComponent, { 11 | components: { 12 | ConfigureFlopFlip: Configure, 13 | Wrapper: , 14 | }, 15 | }); 16 | 17 | describe('without `propKey`', () => { 18 | describe('when feature is disabled', () => { 19 | it('should render receive the flag value as `false`', async () => { 20 | const store = createStore({ 21 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 22 | }); 23 | const TestComponent = injectFeatureToggle('disabledFeature')( 24 | components.FlagsToComponent 25 | ); 26 | 27 | const rendered = render(store, ); 28 | 29 | await rendered.waitUntilConfigured(); 30 | 31 | expect(rendered.queryByFlagName('isFeatureEnabled')).toHaveTextContent( 32 | 'false' 33 | ); 34 | }); 35 | 36 | describe('when enabling feature', () => { 37 | it('should render the component representing a enabled feature', async () => { 38 | const store = createStore({ 39 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 40 | }); 41 | const TestComponent = injectFeatureToggle('disabledFeature')( 42 | components.FlagsToComponent 43 | ); 44 | 45 | const rendered = render(store, ); 46 | 47 | await rendered.waitUntilConfigured(); 48 | 49 | rendered.changeFlagVariation('disabledFeature', true); 50 | 51 | expect(rendered.queryByFlagName('isFeatureEnabled')).toHaveTextContent( 52 | 'true' 53 | ); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('when feature is enabled', () => { 59 | it('should render receive the flag value as `true`', async () => { 60 | const store = createStore({ 61 | [STATE_SLICE]: { flags: { enabledFeature: true } }, 62 | }); 63 | const TestComponent = injectFeatureToggle('enabledFeature')( 64 | components.FlagsToComponent 65 | ); 66 | 67 | const rendered = render(store, ); 68 | 69 | await rendered.waitUntilConfigured(); 70 | 71 | expect(rendered.queryByFlagName('isFeatureEnabled')).toHaveTextContent( 72 | 'true' 73 | ); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('with `propKey`', () => { 79 | describe('when feature is disabled', () => { 80 | it('should render receive the flag value as `false`', async () => { 81 | const store = createStore({ 82 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 83 | }); 84 | const TestComponent = injectFeatureToggle( 85 | 'disabledFeature', 86 | 'customPropKey' 87 | )(components.FlagsToComponent); 88 | 89 | const rendered = render(store, ); 90 | 91 | await rendered.waitUntilConfigured(); 92 | 93 | expect(rendered.queryByFlagName('customPropKey')).toHaveTextContent( 94 | 'false' 95 | ); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /packages/react-redux/modules/ducks/flags/flags.spec.js: -------------------------------------------------------------------------------- 1 | import { STATE_SLICE } from '../../store/constants'; 2 | import reducer, { 3 | UPDATE_FLAGS, 4 | updateFlags, 5 | selectFlag, 6 | selectFlags, 7 | } from './flags'; 8 | 9 | describe('constants', () => { 10 | it('should contain `flags/updateFlags`', () => { 11 | expect(UPDATE_FLAGS).toEqual('@flopflip/flags/update'); 12 | }); 13 | }); 14 | 15 | describe('action creators', () => { 16 | describe('when updating flags', () => { 17 | let flags; 18 | beforeEach(() => { 19 | flags = { a: 'b' }; 20 | }); 21 | it('should return `flags/updateFlags` type', () => { 22 | expect(updateFlags(flags)).toEqual({ 23 | type: UPDATE_FLAGS, 24 | payload: expect.any(Object), 25 | }); 26 | }); 27 | 28 | it('should return passed `flags`', () => { 29 | expect(updateFlags(flags)).toEqual({ 30 | type: expect.any(String), 31 | payload: { flags }, 32 | }); 33 | }); 34 | }); 35 | }); 36 | 37 | describe('reducers', () => { 38 | describe('when updating flags', () => { 39 | describe('without previous flags', () => { 40 | let payload; 41 | beforeEach(() => { 42 | payload = { 43 | flags: { 44 | a: true, 45 | b: false, 46 | }, 47 | }; 48 | }); 49 | 50 | it('should set the new flags', () => { 51 | const reduced = reducer(undefined, { type: UPDATE_FLAGS, payload }); 52 | 53 | expect(reduced).toHaveProperty('a', payload.flags.a); 54 | expect(reduced).toHaveProperty('b', payload.flags.b); 55 | }); 56 | }); 57 | 58 | describe('with previous flags', () => { 59 | let payload; 60 | let state; 61 | beforeEach(() => { 62 | state = { 63 | flags: { 64 | d: false, 65 | }, 66 | }; 67 | payload = { 68 | flags: { 69 | a: true, 70 | b: false, 71 | }, 72 | }; 73 | }); 74 | 75 | it('should merge with new flags', () => { 76 | const reduced = reducer(state, { type: UPDATE_FLAGS, payload }); 77 | 78 | expect(reduced).toHaveProperty('a', payload.flags.a); 79 | expect(reduced).toHaveProperty('b', payload.flags.b); 80 | 81 | expect(reduced).toHaveProperty('c', state.flags.c); 82 | }); 83 | }); 84 | }); 85 | }); 86 | 87 | describe('selectors', () => { 88 | let flags; 89 | let state; 90 | 91 | beforeEach(() => { 92 | flags = { 93 | flagA: true, 94 | flagB: false, 95 | }; 96 | state = { 97 | [STATE_SLICE]: { 98 | flags, 99 | }, 100 | }; 101 | }); 102 | 103 | describe('selecting flags', () => { 104 | it('should return all flags', () => { 105 | expect(selectFlags(state)).toEqual(flags); 106 | }); 107 | }); 108 | 109 | describe('selecting a flag', () => { 110 | describe('when existing', () => { 111 | it('should return the flag value', () => { 112 | expect(selectFlag('flagA')(state)).toEqual(true); 113 | expect(selectFlag('flagB')(state)).toEqual(false); 114 | }); 115 | }); 116 | 117 | describe('when not existing', () => { 118 | it('should return `false`', () => { 119 | expect(selectFlag('zFlag')(state)).toEqual(false); 120 | }); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/configure/configure.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render as rtlRender, act } from '@flopflip/test-utils'; 3 | import adapter, { updateFlags } from '@flopflip/memory-adapter'; 4 | import { useFeatureToggle, useAdapterStatus } from '../../hooks'; 5 | import Configure from './configure'; 6 | 7 | const testFlagName = 'firstFlag'; 8 | const TestComponent = () => { 9 | const { isUnconfigured, isConfiguring, isConfigured } = useAdapterStatus(); 10 | 11 | const isFeatureEnabled = useFeatureToggle(testFlagName); 12 | 13 | return ( 14 |
    15 |
  • Is unconfigured: {isUnconfigured ? 'Yes' : 'No'}
  • 16 |
  • Is configuring: {isConfiguring ? 'Yes' : 'No'}
  • 17 |
  • Is configured: {isConfigured ? 'Yes' : 'No'}
  • 18 |
  • Feature enabled: {isFeatureEnabled ? 'Yes' : 'No'}
  • 19 |
20 | ); 21 | }; 22 | 23 | const createTestProps = (custom) => ({ 24 | adapter, 25 | adapterArgs: { 26 | fooId: 'foo-id', 27 | }, 28 | 29 | ...custom, 30 | }); 31 | 32 | const render = () => { 33 | const props = createTestProps(); 34 | const rendered = rtlRender( 35 | 36 | 37 | 38 | ); 39 | const waitUntilConfigured = () => rendered.findByText(/Is configured: Yes/i); 40 | 41 | return { ...rendered, waitUntilConfigured }; 42 | }; 43 | 44 | describe('when feature is disabled', () => { 45 | it('should indicate the feature being disabled', async () => { 46 | const rendered = render(); 47 | 48 | await rendered.waitUntilConfigured(); 49 | 50 | expect(rendered.getByText(/Feature enabled: No/i)).toBeInTheDocument(); 51 | }); 52 | }); 53 | 54 | describe('when enabling feature is', () => { 55 | it('should indicate the feature being enabled', async () => { 56 | const rendered = render(); 57 | 58 | await rendered.waitUntilConfigured(); 59 | 60 | act(() => 61 | updateFlags({ 62 | [testFlagName]: true, 63 | }) 64 | ); 65 | 66 | expect(rendered.getByText(/Feature enabled: Yes/i)).toBeInTheDocument(); 67 | }); 68 | }); 69 | 70 | describe('when unconfigured', () => { 71 | it('should indicate through the adapter state', async () => { 72 | const rendered = render(); 73 | 74 | expect(rendered.getByText(/Is unconfigured: Yes/i)).toBeInTheDocument(); 75 | expect(rendered.getByText(/Is configuring: No/i)).toBeInTheDocument(); 76 | expect(rendered.getByText(/Is configured: No/i)).toBeInTheDocument(); 77 | 78 | await rendered.waitUntilConfigured(); 79 | }); 80 | }); 81 | 82 | describe('when configured', () => { 83 | it('should indicate through the adapter state', async () => { 84 | const rendered = render(); 85 | 86 | await rendered.waitUntilConfigured(); 87 | 88 | expect(rendered.getByText(/Is configuring: No/i)).toBeInTheDocument(); 89 | expect(rendered.getByText(/Is configured: Yes/i)).toBeInTheDocument(); 90 | }); 91 | }); 92 | 93 | describe('statics', () => { 94 | describe('displayName', () => { 95 | it('should be set to `ConfigureFlopflip`', () => { 96 | expect(Configure.displayName).toEqual('ConfigureFlopflip'); 97 | }); 98 | }); 99 | 100 | describe('defaultProps', () => { 101 | it('should default `defaultFlags` to an empty object', () => { 102 | expect(Configure.defaultProps.defaultFlags).toEqual({}); 103 | }); 104 | 105 | it('should default `shouldDeferAdapterConfiguration` to `true`', () => { 106 | expect(Configure.defaultProps.shouldDeferAdapterConfiguration).toBe( 107 | false 108 | ); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/configure/configure.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render as rtlRender, act } from '@flopflip/test-utils'; 3 | import adapter, { updateFlags } from '@flopflip/memory-adapter'; 4 | import { Provider } from 'react-redux'; 5 | import { createStore } from '../../../test-utils'; 6 | import { STATE_SLICE } from '../../store/constants'; 7 | import { useFeatureToggle, useAdapterStatus } from '../../hooks'; 8 | import Configure from './configure'; 9 | 10 | const testFlagName = 'firstFlag'; 11 | const TestComponent = () => { 12 | const { isUnconfigured, isConfiguring, isConfigured } = useAdapterStatus(); 13 | const isFeatureEnabled = useFeatureToggle(testFlagName); 14 | 15 | return ( 16 |
    17 |
  • Is unconfigured: {isUnconfigured ? 'Yes' : 'No'}
  • 18 |
  • Is configuring: {isConfiguring ? 'Yes' : 'No'}
  • 19 |
  • Is configured: {isConfigured ? 'Yes' : 'No'}
  • 20 |
  • Feature enabled: {isFeatureEnabled ? 'Yes' : 'No'}
  • 21 |
22 | ); 23 | }; 24 | 25 | const render = () => { 26 | const props = createTestProps(); 27 | const store = createStore({ 28 | [STATE_SLICE]: { flags: { disabledFeature: false } }, 29 | }); 30 | 31 | const rtlRendered = rtlRender( 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | 39 | const waitUntilConfigured = () => 40 | rtlRendered.findByText(/Is configured: Yes/i); 41 | 42 | return { ...rtlRendered, waitUntilConfigured }; 43 | }; 44 | 45 | const createTestProps = (custom) => ({ 46 | adapter, 47 | adapterArgs: { 48 | fooId: 'foo-id', 49 | }, 50 | 51 | ...custom, 52 | }); 53 | 54 | describe('when feature is disabled', () => { 55 | it('should indicate the feature being disabled', async () => { 56 | const rendered = render(); 57 | 58 | await rendered.waitUntilConfigured(); 59 | 60 | expect(rendered.getByText(/Feature enabled: No/i)).toBeInTheDocument(); 61 | }); 62 | }); 63 | 64 | describe('when enabling feature is', () => { 65 | it('should indicate the feature being enabled', async () => { 66 | const rendered = render(); 67 | 68 | await rendered.waitUntilConfigured(); 69 | 70 | act(() => 71 | updateFlags({ 72 | [testFlagName]: true, 73 | }) 74 | ); 75 | 76 | expect(rendered.getByText(/Feature enabled: Yes/i)).toBeInTheDocument(); 77 | }); 78 | }); 79 | 80 | describe('when unconfigured', () => { 81 | it('should indicate through the adapter state', async () => { 82 | const rendered = render(); 83 | 84 | expect(rendered.getByText(/Is unconfigured: Yes/i)).toBeInTheDocument(); 85 | expect(rendered.getByText(/Is configuring: No/i)).toBeInTheDocument(); 86 | expect(rendered.getByText(/Is configured: No/i)).toBeInTheDocument(); 87 | 88 | await rendered.waitUntilConfigured(); 89 | }); 90 | }); 91 | 92 | describe('when configured', () => { 93 | it('should indicate through the adapter state', async () => { 94 | const rendered = render(); 95 | 96 | await rendered.waitUntilConfigured(); 97 | await adapter.waitUntilConfigured(); 98 | 99 | expect(rendered.getByText(/Is configuring: No/i)).toBeInTheDocument(); 100 | expect(rendered.getByText(/Is configured: Yes/i)).toBeInTheDocument(); 101 | }); 102 | }); 103 | 104 | describe('statics', () => { 105 | describe('displayName', () => { 106 | it('should be set to `ConfigureFlopflip`', () => { 107 | expect(Configure.displayName).toEqual('ConfigureFlopflip'); 108 | }); 109 | }); 110 | 111 | describe('defaultProps', () => { 112 | it('should default `defaultFlags` to an empty object', () => { 113 | expect(Configure.defaultProps.defaultFlags).toEqual({}); 114 | }); 115 | 116 | it('should default `shouldDeferAdapterConfiguration` to `true`', () => { 117 | expect(Configure.defaultProps.shouldDeferAdapterConfiguration).toBe( 118 | false 119 | ); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/configure/configure.tsx: -------------------------------------------------------------------------------- 1 | import type { DeepReadonly } from 'ts-essentials'; 2 | import type { 3 | TAdapter, 4 | TFlags, 5 | TAdapterStatus, 6 | TFlagsChange, 7 | TAdapterStatusChange, 8 | TConfigureAdapterChildren, 9 | TConfigureAdapterProps, 10 | } from '@flopflip/types'; 11 | import { 12 | TAdapterConfigurationStatus, 13 | TAdapterSubscriptionStatus, 14 | } from '@flopflip/types'; 15 | 16 | import React from 'react'; 17 | import { ConfigureAdapter, useAdapterSubscription } from '@flopflip/react'; 18 | import { FlagsContext } from '../flags-context'; 19 | 20 | type BaseProps = { 21 | children?: TConfigureAdapterChildren; 22 | shouldDeferAdapterConfiguration?: boolean; 23 | defaultFlags?: TFlags; 24 | }; 25 | type Props = BaseProps & 26 | TConfigureAdapterProps; 27 | type State = { 28 | flags: TFlags; 29 | status: TAdapterStatus; 30 | configurationId?: string; 31 | }; 32 | 33 | const initialAdapterStatus: State['status'] = { 34 | subscriptionStatus: TAdapterSubscriptionStatus.Subscribed, 35 | configurationStatus: TAdapterConfigurationStatus.Unconfigured, 36 | }; 37 | const initialFlags: State['flags'] = {}; 38 | 39 | type TUseFlagStateOptions = DeepReadonly<{ 40 | initialFlags: State['flags']; 41 | }>; 42 | const useFlagsState = ({ 43 | initialFlags, 44 | }: TUseFlagStateOptions): [ 45 | TFlags, 46 | React.Dispatch>> 47 | ] => { 48 | const [flags, setFlags] = React.useState(initialFlags); 49 | 50 | return [flags, setFlags]; 51 | }; 52 | 53 | type TUseStatusStateOptions = DeepReadonly<{ 54 | initialAdapterStatus: State['status']; 55 | }>; 56 | const useStatusState = ({ 57 | initialAdapterStatus, 58 | }: TUseStatusStateOptions): [ 59 | TAdapterStatus, 60 | React.Dispatch>> 61 | ] => { 62 | const [status, setStatus] = React.useState( 63 | initialAdapterStatus 64 | ); 65 | 66 | return [status, setStatus]; 67 | }; 68 | 69 | const Configure = ( 70 | props: DeepReadonly> 71 | ) => { 72 | const [flags, setFlags] = useFlagsState({ initialFlags }); 73 | const [status, setStatus] = useStatusState({ initialAdapterStatus }); 74 | 75 | // NOTE: 76 | // Using this prevents the callbacks being invoked 77 | // which would trigger a setState as a result on an unmounted 78 | // component. 79 | const getHasAdapterSubscriptionStatus = useAdapterSubscription(props.adapter); 80 | 81 | const handleUpdateFlags = React.useCallback< 82 | (flags: Readonly) => void 83 | >( 84 | (flags) => { 85 | if ( 86 | getHasAdapterSubscriptionStatus(TAdapterSubscriptionStatus.Unsubscribed) 87 | ) { 88 | return; 89 | } 90 | 91 | setFlags((prevFlags) => ({ 92 | ...prevFlags, 93 | ...flags, 94 | })); 95 | }, 96 | [setFlags, getHasAdapterSubscriptionStatus] 97 | ); 98 | 99 | const handleUpdateStatus = React.useCallback< 100 | (status: Readonly) => void 101 | >( 102 | (status) => { 103 | if ( 104 | getHasAdapterSubscriptionStatus(TAdapterSubscriptionStatus.Unsubscribed) 105 | ) { 106 | return; 107 | } 108 | 109 | setStatus((prevStatus) => ({ 110 | ...prevStatus, 111 | ...status, 112 | })); 113 | }, 114 | [setStatus, getHasAdapterSubscriptionStatus] 115 | ); 116 | 117 | return ( 118 | 119 | 128 | {props.children} 129 | 130 | 131 | ); 132 | }; 133 | 134 | Configure.displayName = 'ConfigureFlopflip'; 135 | Configure.defaultProps = { 136 | defaultFlags: {}, 137 | shouldDeferAdapterConfiguration: false, 138 | }; 139 | 140 | export default Configure; 141 | -------------------------------------------------------------------------------- /packages/react/modules/components/reconfigure-adapter/reconfigure-adapter.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent } from '@flopflip/test-utils'; 3 | import { AdapterStates } from './../configure-adapter'; 4 | import AdapterContext, { createAdapterContext } from './../adapter-context'; 5 | import ReconfigureAdapter from './reconfigure-adapter'; 6 | 7 | const TestComponent = (props) => { 8 | const [count, setCount] = React.useState(0); 9 | const [, setState] = React.useState(0); 10 | const increment = () => setCount(count + 1); 11 | 12 | const user = React.useMemo( 13 | () => ({ 14 | ...props.reconfiguration.user, 15 | count, 16 | }), 17 | [count, props.reconfiguration.user] 18 | ); 19 | 20 | return ( 21 | 22 | 26 | <> 27 | 30 | 33 |

Count is: {count}

34 |

Children

35 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | const createReconfiguration = () => ({ 42 | user: { 43 | id: 'test-user-id', 44 | }, 45 | shouldOverwrite: true, 46 | }); 47 | 48 | describe('with children', () => { 49 | it('should render children', () => { 50 | const adapterContext = createAdapterContext( 51 | jest.fn(), 52 | AdapterStates.UNCONFIGURED 53 | ); 54 | const reconfiguration = createReconfiguration(); 55 | const rendered = render( 56 | 60 | ); 61 | 62 | expect(rendered.getByText('Children')).toBeInTheDocument(); 63 | }); 64 | }); 65 | 66 | describe('when mounted', () => { 67 | it('should reconfigure with user and configuration', () => { 68 | const adapterContext = createAdapterContext( 69 | jest.fn(), 70 | AdapterStates.UNCONFIGURED 71 | ); 72 | const reconfiguration = createReconfiguration(); 73 | 74 | render( 75 | 79 | ); 80 | 81 | expect(adapterContext.reconfigure).toHaveBeenCalledWith( 82 | { 83 | user: expect.objectContaining(reconfiguration.user), 84 | }, 85 | { 86 | shouldOverwrite: reconfiguration.shouldOverwrite, 87 | } 88 | ); 89 | }); 90 | }); 91 | 92 | describe('when updated', () => { 93 | describe('without reconfiguration change', () => { 94 | it('should not reconfigure again with user and configuration', () => { 95 | const adapterContext = createAdapterContext( 96 | jest.fn(), 97 | AdapterStates.UNCONFIGURED 98 | ); 99 | const reconfiguration = createReconfiguration(); 100 | 101 | const rendered = render( 102 | 106 | ); 107 | 108 | fireEvent.click(rendered.queryByText(/Reconfigure without changes/i)); 109 | 110 | expect(adapterContext.reconfigure).toHaveBeenCalledTimes(1); 111 | }); 112 | }); 113 | 114 | describe('with reconfiguration change', () => { 115 | it('should reconfigure again with user and configuration', () => { 116 | const adapterContext = createAdapterContext( 117 | jest.fn(), 118 | AdapterStates.UNCONFIGURED 119 | ); 120 | const reconfiguration = createReconfiguration(); 121 | 122 | const rendered = render( 123 | 127 | ); 128 | 129 | fireEvent.click(rendered.queryByText(/Reconfigure with changes/i)); 130 | 131 | expect(adapterContext.reconfigure).toHaveBeenNthCalledWith( 132 | 2, 133 | { 134 | user: expect.objectContaining(reconfiguration.user), 135 | }, 136 | { 137 | shouldOverwrite: reconfiguration.shouldOverwrite, 138 | } 139 | ); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /packages/react-redux/modules/components/inject-feature-toggles/inject-feature-toggles.spec.js: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux'; 2 | import React from 'react'; 3 | import { renderWithAdapter, components } from '@flopflip/test-utils'; 4 | import { createStore } from '../../../test-utils'; 5 | import Configure from '../configure'; 6 | import { STATE_SLICE } from './../../store/constants'; 7 | import injectFeatureToggles from './inject-feature-toggles'; 8 | 9 | const render = (store, TestComponent) => 10 | renderWithAdapter(TestComponent, { 11 | components: { 12 | ConfigureFlopFlip: Configure, 13 | Wrapper: , 14 | }, 15 | }); 16 | const FlagsToComponent = (props) => ( 17 | 18 | ); 19 | const FlagsToComponentWithPropKey = (props) => ( 20 | 21 | ); 22 | 23 | describe('injectFeatureToggles', () => { 24 | describe('without `propKey`', () => { 25 | it('should have feature enabling prop for `enabledFeature`', async () => { 26 | const store = createStore({ 27 | [STATE_SLICE]: { 28 | flags: { enabledFeature: true, disabledFeature: false }, 29 | }, 30 | }); 31 | const TestComponent = injectFeatureToggles([ 32 | 'disabledFeature', 33 | 'enabledFeature', 34 | ])(FlagsToComponent); 35 | 36 | const rendered = render(store, ); 37 | 38 | await rendered.waitUntilConfigured(); 39 | 40 | expect(rendered.queryByFlagName('enabledFeature')).toHaveTextContent( 41 | 'true' 42 | ); 43 | }); 44 | 45 | it('should have feature disabling prop for `disabledFeature`', async () => { 46 | const store = createStore({ 47 | [STATE_SLICE]: { 48 | flags: { enabledFeature: true, disabledFeature: false }, 49 | }, 50 | }); 51 | const TestComponent = injectFeatureToggles([ 52 | 'disabledFeature', 53 | 'enabledFeature', 54 | ])(FlagsToComponent); 55 | 56 | const rendered = render(store, ); 57 | 58 | await rendered.waitUntilConfigured(); 59 | 60 | expect(rendered.queryByFlagName('disabledFeature')).toHaveTextContent( 61 | 'false' 62 | ); 63 | }); 64 | 65 | describe('when enabling feature', () => { 66 | it('should render the component representing a enabled feature', async () => { 67 | const TestComponent = injectFeatureToggles([ 68 | 'disabledFeature', 69 | 'enabledFeature', 70 | ])(FlagsToComponent); 71 | const store = createStore({ 72 | [STATE_SLICE]: { 73 | flags: { enabledFeature: true, disabledFeature: false }, 74 | }, 75 | }); 76 | 77 | const rendered = render(store, ); 78 | 79 | await rendered.waitUntilConfigured(); 80 | 81 | rendered.changeFlagVariation('disabledFeature', true); 82 | 83 | expect(rendered.queryByFlagName('disabledFeature')).toHaveTextContent( 84 | 'true' 85 | ); 86 | }); 87 | }); 88 | }); 89 | 90 | describe('with `propKey`', () => { 91 | it('should have feature enabling prop for `enabledFeature`', async () => { 92 | const store = createStore({ 93 | [STATE_SLICE]: { 94 | flags: { enabledFeature: true, disabledFeature: false }, 95 | }, 96 | }); 97 | const TestComponent = injectFeatureToggles( 98 | ['disabledFeature', 'enabledFeature'], 99 | 'onOffs' 100 | )(FlagsToComponentWithPropKey); 101 | 102 | const rendered = render(store, ); 103 | 104 | await rendered.waitUntilConfigured(); 105 | 106 | expect(rendered.queryByFlagName('enabledFeature')).toHaveTextContent( 107 | 'true' 108 | ); 109 | }); 110 | 111 | it('should have feature disabling prop for `disabledFeature`', async () => { 112 | const store = createStore({ 113 | [STATE_SLICE]: { 114 | flags: { enabledFeature: true, disabledFeature: false }, 115 | }, 116 | }); 117 | const TestComponent = injectFeatureToggles( 118 | ['disabledFeature', 'enabledFeature'], 119 | 'onOffs' 120 | )(FlagsToComponentWithPropKey); 121 | 122 | const rendered = render(store, ); 123 | 124 | await rendered.waitUntilConfigured(); 125 | 126 | expect(rendered.queryByFlagName('disabledFeature')).toHaveTextContent( 127 | 'false' 128 | ); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /packages/react-broadcast/modules/components/branch-on-feature-toggle/branch-on-feature-toggle.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderWithAdapter, components } from '@flopflip/test-utils'; 3 | import branchOnFeatureToggle from './branch-on-feature-toggle'; 4 | import Configure from '../configure'; 5 | 6 | const render = (TestComponent) => 7 | renderWithAdapter(TestComponent, { 8 | components: { ConfigureFlopFlip: Configure }, 9 | }); 10 | 11 | describe('without `untoggledComponent', () => { 12 | describe('when feature is disabled', () => { 13 | it('should render neither the component representing an disabled or enabled feature', async () => { 14 | const TestComponent = branchOnFeatureToggle({ flag: 'disabledFeature' })( 15 | components.ToggledComponent 16 | ); 17 | 18 | const rendered = render(); 19 | 20 | await rendered.waitUntilConfigured(); 21 | 22 | expect( 23 | rendered.queryByFlagName('isFeatureEnabled') 24 | ).not.toBeInTheDocument(); 25 | }); 26 | 27 | describe('when enabling feature', () => { 28 | it('should render the component representing a enabled feature', async () => { 29 | const TestComponent = branchOnFeatureToggle({ 30 | flag: 'disabledFeature', 31 | })(components.ToggledComponent); 32 | 33 | const rendered = render(); 34 | 35 | await rendered.waitUntilConfigured(); 36 | 37 | rendered.changeFlagVariation('disabledFeature', true); 38 | 39 | expect( 40 | rendered.queryByFlagName('isFeatureEnabled') 41 | ).toBeInTheDocument(); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('when feature is enabled', () => { 47 | it('should render the component representing an enabled feature', async () => { 48 | const TestComponent = branchOnFeatureToggle({ flag: 'enabledFeature' })( 49 | components.ToggledComponent 50 | ); 51 | 52 | const rendered = render(); 53 | 54 | await rendered.waitUntilConfigured(); 55 | 56 | expect(rendered.queryByFlagName('isFeatureEnabled')).toHaveAttribute( 57 | 'data-flag-status', 58 | 'enabled' 59 | ); 60 | }); 61 | }); 62 | }); 63 | 64 | describe('with `untoggledComponent', () => { 65 | describe('when feature is disabled', () => { 66 | it('should not render the component representing a enabled feature', async () => { 67 | const TestComponent = branchOnFeatureToggle( 68 | { flag: 'disabledFeature' }, 69 | components.UntoggledComponent 70 | )(components.ToggledComponent); 71 | 72 | const rendered = render(); 73 | 74 | await rendered.waitUntilConfigured(); 75 | 76 | expect(rendered.queryByFlagName('isFeatureEnabled')).not.toHaveAttribute( 77 | 'data-flag-status', 78 | 'enabled' 79 | ); 80 | }); 81 | 82 | it('should render the component representing a disabled feature', async () => { 83 | const TestComponent = branchOnFeatureToggle( 84 | { flag: 'disabledFeature' }, 85 | components.UntoggledComponent 86 | )(components.ToggledComponent); 87 | 88 | const rendered = render(); 89 | 90 | await rendered.waitUntilConfigured(); 91 | 92 | expect(rendered.queryByFlagName('isFeatureEnabled')).toHaveAttribute( 93 | 'data-flag-status', 94 | 'disabled' 95 | ); 96 | }); 97 | }); 98 | 99 | describe('when feature is enabled', () => { 100 | it('should render the component representing a enabled feature', async () => { 101 | const TestComponent = branchOnFeatureToggle( 102 | { flag: 'enabledFeature' }, 103 | components.UntoggledComponent 104 | )(components.ToggledComponent); 105 | 106 | const rendered = render(); 107 | 108 | await rendered.waitUntilConfigured(); 109 | 110 | expect(rendered.queryByFlagName('isFeatureEnabled')).toHaveAttribute( 111 | 'data-flag-status', 112 | 'enabled' 113 | ); 114 | }); 115 | 116 | it('should not render the component representing a disabled feature', async () => { 117 | const TestComponent = branchOnFeatureToggle( 118 | { flag: 'enabledFeature' }, 119 | components.UntoggledComponent 120 | )(components.ToggledComponent); 121 | 122 | const rendered = render(); 123 | 124 | await rendered.waitUntilConfigured(); 125 | 126 | expect(rendered.queryByFlagName('isFeatureEnabled')).not.toHaveAttribute( 127 | 'data-flag-status', 128 | 'disabled' 129 | ); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /demo/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import flowRight from 'lodash.flowright'; 3 | import { Provider, connect } from 'react-redux'; 4 | import classNames from 'classnames'; 5 | // Import adapter from '@flopflip/launchdarkly-adapter'; 6 | // import adapter, { updateFlags } from '@flopflip/memory-adapter'; 7 | import adapter, { updateFlags } from '@flopflip/memory-adapter'; 8 | import { 9 | ConfigureFlopFlip, 10 | branchOnFeatureToggle, 11 | injectFeatureToggle, 12 | ToggleFeature, 13 | } from '@flopflip/react-broadcast'; 14 | // Change to `from '@flopflip/react-broadcast'` and everything will just work wtihout redux 15 | import { 16 | increment, 17 | incrementAsync, 18 | decrement, 19 | decrementAsync, 20 | } from './modules/counter'; 21 | import logo from './logo.svg'; 22 | import store from './store'; 23 | import './App.css'; 24 | import allFlags, { 25 | INCREMENT_ASYNC_BUTTON, 26 | DECREMENT_ASYNC_BUTTON, 27 | INCREMENT_SYNC_BUTTON, 28 | } from './flags'; 29 | 30 | const UntoggledFeature = () =>
Disabled Feature
; 31 | 32 | const IncrementAsyncButton = (props) => ( 33 | 40 | ); 41 | const FeatureToggledIncrementAsyncButton = flowRight( 42 | branchOnFeatureToggle({ flag: INCREMENT_ASYNC_BUTTON }, UntoggledFeature) 43 | )(IncrementAsyncButton); 44 | 45 | const IncrementSyncButton = (props) => ( 46 | 59 | ); 60 | 61 | const FeatureToggledIncrementSyncButton = injectFeatureToggle( 62 | INCREMENT_SYNC_BUTTON, 63 | 'syncButtonStyle' 64 | )(IncrementSyncButton); 65 | 66 | const Counter = (props) => ( 67 |
68 |

Count around

69 |

Count: {props.count}

70 | 71 |
72 | 76 |
77 | 81 |
82 | 83 |
84 | 91 |
92 | 96 | 103 | 104 |
105 |
106 | ); 107 | 108 | const mapStateToProps = (state) => ({ 109 | count: state.counter.count, 110 | isIncrementing: state.counter.isIncrementing, 111 | isDecrementing: state.counter.isDecrementing, 112 | }); 113 | 114 | const mapDispatchToProps = { 115 | increment, 116 | incrementAsync, 117 | decrement, 118 | decrementAsync, 119 | }; 120 | 121 | const ConnectedCounter = connect(mapStateToProps, mapDispatchToProps)(Counter); 122 | 123 | class App extends Component { 124 | state = { 125 | hasError: false, 126 | }; 127 | 128 | static getDerivedStateFromError() { 129 | return { hasError: true }; 130 | } 131 | 132 | componentDidCatch(error, info) { 133 | console.log(error, info); 134 | } 135 | 136 | render() { 137 | if (this.state.hasError) { 138 | // You can render any custom fallback UI 139 | return

Something went wrong.

; 140 | } 141 | 142 | return ( 143 | 144 | 145 |
146 |
147 | logo 148 |

Welcome to flopflip

149 |
150 | 151 | 152 |
153 |
154 |
155 | ); 156 | } 157 | } 158 | 159 | window.updateFlags = updateFlags; 160 | 161 | export default App; 162 | --------------------------------------------------------------------------------