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/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..1746556042f
--- /dev/null
+++ b/src/features/data/actions.js
@@ -0,0 +1,51 @@
+/**
+ * External dependencies
+ */
+import { apiFetch } from '@wordpress/data-controls';
+
+/**
+ * Internal dependencies
+ */
+import TYPES from './action-types';
+import { API_NAMESPACE } 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' },
+ });
+ return yield setFeatures(response);
+ } 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..1bc4844476c
--- /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..9b705333922
--- /dev/null
+++ b/src/features/data/resolvers.js
@@ -0,0 +1,34 @@
+/**
+ * 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: `${API_NAMESPACE}/options?search=` + OPTION_NAME_PREFIX,
+ });
+
+ yield setModifiedFeatures(Object.keys(response));
+ } 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..73bbacb2f49
--- /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..784815b181b
--- /dev/null
+++ b/src/features/index.js
@@ -0,0 +1,66 @@
+/**
+ * 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;