diff --git a/api/api.php b/api/api.php
index 9a3a02173bf..cab00d3441b 100644
--- a/api/api.php
+++ b/api/api.php
@@ -36,3 +36,4 @@ require( 'tools/delete-all-products.php');
require( 'tools/disable-wc-email.php' );
require( 'tools/trigger-update-callbacks.php' );
require( 'tracks/tracks-debug-log.php' );
+require( 'features/features.php' );
diff --git a/api/features/features.php b/api/features/features.php
new file mode 100644
index 00000000000..f19db0cb94f
--- /dev/null
+++ b/api/features/features.php
@@ -0,0 +1,59 @@
+[a-z0-9_\-]+)/toggle',
+ 'toggle_feature',
+ array(
+ 'methods' => 'POST',
+ )
+);
+
+register_woocommerce_admin_test_helper_rest_route(
+ '/features',
+ 'get_features',
+ array(
+ 'methods' => 'GET',
+ )
+);
+
+register_woocommerce_admin_test_helper_rest_route(
+ '/features/reset',
+ 'reset_features',
+ array(
+ 'methods' => 'POST',
+ )
+);
+
+function toggle_feature( $request ) {
+ $features = get_features();
+ $custom_feature_values = get_option( OPTION_NAME_PREFIX, array() );
+ $feature_name = $request->get_param( 'feature_name' );
+
+ if ( ! isset( $features[$feature_name ]) ) {
+ return new WP_REST_Response( $features, 204 );
+ }
+
+ if ( isset( $custom_feature_values[$feature_name] ) ) {
+ unset( $custom_feature_values[$feature_name] );
+ } else {
+ $custom_feature_values[$feature_name] = ! $features[ $feature_name ];
+ }
+
+ update_option(OPTION_NAME_PREFIX, $custom_feature_values );
+ return new WP_REST_Response( get_features(), 200 );
+}
+
+function reset_features() {
+ delete_option( OPTION_NAME_PREFIX );
+ return new WP_REST_Response( get_features(), 200 );
+}
+
+function get_features() {
+ if ( function_exists( 'wc_admin_get_feature_config' ) ) {
+ return apply_filters( 'woocommerce_admin_get_feature_config', wc_admin_get_feature_config() );
+ }
+ return array();
+}
diff --git a/plugin.php b/plugin.php
index 04ba8574c2f..0d01e8f77d8 100644
--- a/plugin.php
+++ b/plugin.php
@@ -14,3 +14,13 @@ add_action( 'admin_menu', function() {
add_action( 'wp_loaded', function() {
require( 'api/api.php' );
} );
+
+add_filter( 'woocommerce_admin_get_feature_config', function( $feature_config ) {
+ $custom_feature_values = get_option( 'wc_admin_helper_feature_values', array() );
+ foreach ( $custom_feature_values as $feature => $value ) {
+ if ( isset( $feature_config[$feature] ) ) {
+ $feature_config[$feature] = $value;
+ }
+ }
+ return $feature_config;
+} );
\ No newline at end of file
diff --git a/src/app/app.js b/src/app/app.js
index 69d70987a8d..acc8790b431 100644
--- a/src/app/app.js
+++ b/src/app/app.js
@@ -11,6 +11,7 @@ import { AdminNotes } from '../admin-notes';
import { default as Tools } from '../tools';
import { default as Options } from '../options';
import { default as Experiments } from '../experiments';
+import { default as Features } from '../features';
const tabs = applyFilters('woocommerce_admin_test_helper_tabs', [
{
@@ -33,6 +34,11 @@ const tabs = applyFilters('woocommerce_admin_test_helper_tabs', [
title: 'Experiments',
content: ,
},
+ {
+ name: 'features',
+ title: 'Features',
+ content: ,
+ },
]);
export function App() {
diff --git a/src/experiments/data/actions.js b/src/experiments/data/actions.js
index 18e74e00b06..0db766f5760 100644
--- a/src/experiments/data/actions.js
+++ b/src/experiments/data/actions.js
@@ -9,42 +9,42 @@ import { apiFetch } from '@wordpress/data-controls';
import TYPES from './action-types';
import { EXPERIMENT_NAME_PREFIX, TRANSIENT_NAME_PREFIX } from './constants';
-function toggleFrontendExperiment( experimentName, newVariation ) {
+function toggleFrontendExperiment(experimentName, newVariation) {
const storageItem = JSON.parse(
- window.localStorage.getItem( EXPERIMENT_NAME_PREFIX + experimentName )
+ window.localStorage.getItem(EXPERIMENT_NAME_PREFIX + experimentName)
);
storageItem.variationName = newVariation;
window.localStorage.setItem(
EXPERIMENT_NAME_PREFIX + experimentName,
- JSON.stringify( storageItem )
+ JSON.stringify(storageItem)
);
}
-function* toggleBackendExperiment( experimentName, newVariation ) {
+function* toggleBackendExperiment(experimentName, newVariation) {
try {
const payload = {};
- payload[ TRANSIENT_NAME_PREFIX + experimentName ] = newVariation;
- yield apiFetch( {
+ payload[TRANSIENT_NAME_PREFIX + experimentName] = newVariation;
+ yield apiFetch({
method: 'POST',
path: '/wc-admin/options',
headers: { 'content-type': 'application/json' },
- body: JSON.stringify( payload ),
- } );
- } catch ( error ) {
+ body: JSON.stringify(payload),
+ });
+ } catch (error) {
throw new Error();
}
}
-export function* toggleExperiment( experimentName, currentVariation, source ) {
+export function* toggleExperiment(experimentName, currentVariation, source) {
const newVariation =
currentVariation === 'control' ? 'treatment' : 'control';
- if ( source === 'frontend' ) {
- toggleFrontendExperiment( experimentName, newVariation );
+ if (source === 'frontend') {
+ toggleFrontendExperiment(experimentName, newVariation);
} else {
- yield toggleBackendExperiment( experimentName, newVariation );
+ yield toggleBackendExperiment(experimentName, newVariation);
}
return {
@@ -55,7 +55,7 @@ export function* toggleExperiment( experimentName, currentVariation, source ) {
};
}
-export function setExperiments( experiments ) {
+export function setExperiments(experiments) {
return {
type: TYPES.SET_EXPERIMENTS,
experiments,
diff --git a/src/features/data/action-types.js b/src/features/data/action-types.js
new file mode 100644
index 00000000000..1e6a4d8a3bf
--- /dev/null
+++ b/src/features/data/action-types.js
@@ -0,0 +1,7 @@
+const TYPES = {
+ TOGGLE_FEATURE: 'TOGGLE_FEATURE',
+ SET_FEATURES: 'SET_FEATURES',
+ SET_MODIFIED_FEATURES: 'SET_MODIFIED_FEATURES',
+};
+
+export default TYPES;
diff --git a/src/features/data/actions.js b/src/features/data/actions.js
new file mode 100644
index 00000000000..25bc100d791
--- /dev/null
+++ b/src/features/data/actions.js
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import { apiFetch } from '@wordpress/data-controls';
+import { controls } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import TYPES from './action-types';
+import { API_NAMESPACE, STORE_KEY } from './constants';
+
+export function* resetModifiedFeatures() {
+ try {
+ const response = yield apiFetch( {
+ path: `${ API_NAMESPACE }/features/reset`,
+ method: 'POST',
+ } );
+
+ yield setModifiedFeatures( [] );
+ yield setFeatures( response );
+ } catch ( error ) {
+ throw new Error();
+ }
+}
+
+export function* toggleFeature( featureName ) {
+ try {
+ const response = yield apiFetch( {
+ method: 'POST',
+ path: API_NAMESPACE + '/features/' + featureName + '/toggle',
+ headers: { 'content-type': 'application/json' },
+ } );
+ yield setFeatures( response );
+ yield controls.dispatch(
+ STORE_KEY,
+ 'invalidateResolutionForStoreSelector',
+ 'getModifiedFeatures'
+ );
+ } catch ( error ) {
+ throw new Error();
+ }
+}
+
+export function setFeatures( features ) {
+ return {
+ type: TYPES.SET_FEATURES,
+ features,
+ };
+}
+
+export function setModifiedFeatures( modifiedFeatures ) {
+ return {
+ type: TYPES.SET_MODIFIED_FEATURES,
+ modifiedFeatures,
+ };
+}
diff --git a/src/features/data/constants.js b/src/features/data/constants.js
new file mode 100644
index 00000000000..c9da14951e5
--- /dev/null
+++ b/src/features/data/constants.js
@@ -0,0 +1,3 @@
+export const STORE_KEY = 'wc-admin-helper/features';
+export const OPTION_NAME_PREFIX = 'wc_admin_helper_feature_values';
+export const API_NAMESPACE = '/wc-admin-test-helper';
diff --git a/src/features/data/index.js b/src/features/data/index.js
new file mode 100644
index 00000000000..e476479ea88
--- /dev/null
+++ b/src/features/data/index.js
@@ -0,0 +1,22 @@
+/**
+ * External dependencies
+ */
+import { registerStore } from '@wordpress/data';
+import { controls } from '@wordpress/data-controls';
+
+/**
+ * Internal dependencies
+ */
+import * as actions from './actions';
+import * as resolvers from './resolvers';
+import * as selectors from './selectors';
+import reducer from './reducer';
+import { STORE_KEY } from './constants';
+
+export default registerStore( STORE_KEY, {
+ actions,
+ selectors,
+ resolvers,
+ controls,
+ reducer,
+} );
diff --git a/src/features/data/reducer.js b/src/features/data/reducer.js
new file mode 100644
index 00000000000..61999300c24
--- /dev/null
+++ b/src/features/data/reducer.js
@@ -0,0 +1,28 @@
+/**
+ * Internal dependencies
+ */
+import TYPES from './action-types';
+
+const DEFAULT_STATE = {
+ features: {},
+ modifiedFeatures: [],
+};
+
+const reducer = ( state = DEFAULT_STATE, action ) => {
+ switch ( action.type ) {
+ case TYPES.SET_MODIFIED_FEATURES:
+ return {
+ ...state,
+ modifiedFeatures: action.modifiedFeatures,
+ };
+ case TYPES.SET_FEATURES:
+ return {
+ ...state,
+ features: action.features,
+ };
+ default:
+ return state;
+ }
+};
+
+export default reducer;
diff --git a/src/features/data/resolvers.js b/src/features/data/resolvers.js
new file mode 100644
index 00000000000..29d7ccfbf19
--- /dev/null
+++ b/src/features/data/resolvers.js
@@ -0,0 +1,38 @@
+/**
+ * External dependencies
+ */
+import { apiFetch } from '@wordpress/data-controls';
+
+/**
+ * Internal dependencies
+ */
+import { setFeatures, setModifiedFeatures } from './actions';
+import { API_NAMESPACE, OPTION_NAME_PREFIX } from './constants';
+
+export function* getModifiedFeatures() {
+ try {
+ const response = yield apiFetch( {
+ path: `wc-admin/options?options=` + OPTION_NAME_PREFIX,
+ } );
+
+ yield setModifiedFeatures(
+ response && response[ OPTION_NAME_PREFIX ]
+ ? Object.keys( response[ OPTION_NAME_PREFIX ] )
+ : []
+ );
+ } catch ( error ) {
+ throw new Error();
+ }
+}
+
+export function* getFeatures() {
+ try {
+ const response = yield apiFetch( {
+ path: `${ API_NAMESPACE }/features`,
+ } );
+
+ yield setFeatures( response );
+ } catch ( error ) {
+ throw new Error();
+ }
+}
diff --git a/src/features/data/selectors.js b/src/features/data/selectors.js
new file mode 100644
index 00000000000..6fa3938dafb
--- /dev/null
+++ b/src/features/data/selectors.js
@@ -0,0 +1,7 @@
+export function getFeatures( state ) {
+ return state.features;
+}
+
+export function getModifiedFeatures( state ) {
+ return state.modifiedFeatures;
+}
diff --git a/src/features/index.js b/src/features/index.js
new file mode 100644
index 00000000000..f89defecc1c
--- /dev/null
+++ b/src/features/index.js
@@ -0,0 +1,72 @@
+/**
+ * External dependencies
+ */
+import { useDispatch, useSelect } from '@wordpress/data';
+import { Button } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { STORE_KEY } from './data/constants';
+import './data';
+
+function Features() {
+ const { features = {}, modifiedFeatures = [] } = useSelect( ( select ) => {
+ const { getFeatures, getModifiedFeatures } = select( STORE_KEY );
+ return {
+ features: getFeatures(),
+ modifiedFeatures: getModifiedFeatures(),
+ };
+ } );
+
+ const { toggleFeature, resetModifiedFeatures } = useDispatch( STORE_KEY );
+
+ return (
+
+
+ Features
+
+
+
+
+
+ Feature Name |
+ Enabled? |
+ Toggle |
+
+
+
+ { Object.keys( features ).map( ( feature_name ) => {
+ return (
+
+
+ { feature_name }
+ |
+ { features[ feature_name ].toString() } |
+
+
+ |
+
+ );
+ } ) }
+
+
+
+ );
+}
+
+export default Features;