diff --git a/plugins/woocommerce-admin/client/tasks/fills/Marketing/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/Marketing/index.tsx index ebaf7f51cfb..12beb10b57a 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/Marketing/index.tsx +++ b/plugins/woocommerce-admin/client/tasks/fills/Marketing/index.tsx @@ -107,14 +107,13 @@ const Marketing: React.FC< MarketingProps > = ( { onComplete } ) => { const [ currentPlugin, setCurrentPlugin ] = useState< string | null >( null ); + const { actionTask } = useDispatch( ONBOARDING_STORE_NAME ); const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME ); - const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); const { activePlugins, freeExtensions, installedPlugins, isResolving, - trackedCompletedActions, } = useSelect( ( select: WCDataSelector ) => { const { getActivePlugins, getInstalledPlugins } = select( PLUGINS_STORE_NAME @@ -123,26 +122,11 @@ const Marketing: React.FC< MarketingProps > = ( { onComplete } ) => { ONBOARDING_STORE_NAME ); - const { - getOption, - hasFinishedResolution: optionFinishedResolution, - } = select( OPTIONS_STORE_NAME ); - - const completedActions = - getOption( 'woocommerce_task_list_tracked_completed_actions' ) || - EMPTY_ARRAY; - return { activePlugins: getActivePlugins(), freeExtensions: getFreeExtensions(), installedPlugins: getInstalledPlugins(), - isResolving: ! ( - hasFinishedResolution( 'getFreeExtensions' ) && - optionFinishedResolution( 'getOption', [ - 'woocommerce_task_list_tracked_completed_actions', - ] ) - ), - trackedCompletedActions: completedActions, + isResolving: ! hasFinishedResolution( 'getFreeExtensions' ), }; } ); @@ -158,6 +142,7 @@ const Marketing: React.FC< MarketingProps > = ( { onComplete } ) => { const installAndActivate = ( slug: string ) => { setCurrentPlugin( slug ); + actionTask( 'marketing' ); installAndActivatePlugins( [ slug ] ) .then( ( response: { errors: Record< string, string > } ) => { recordEvent( 'tasklist_marketing_install', { @@ -167,18 +152,9 @@ const Marketing: React.FC< MarketingProps > = ( { onComplete } ) => { ), } ); - if ( ! trackedCompletedActions.includes( 'marketing' ) ) { - updateOptions( { - woocommerce_task_list_tracked_completed_actions: [ - ...trackedCompletedActions, - 'marketing', - ], - } ); - onComplete(); - } - createNoticesFromResponse( response ); setCurrentPlugin( null ); + onComplete(); } ) .catch( ( response: { errors: Record< string, string > } ) => { createNoticesFromResponse( response ); diff --git a/plugins/woocommerce-admin/packages/data/src/onboarding/action-types.js b/plugins/woocommerce-admin/packages/data/src/onboarding/action-types.js index 378d20d076f..36f621ab65d 100644 --- a/plugins/woocommerce-admin/packages/data/src/onboarding/action-types.js +++ b/plugins/woocommerce-admin/packages/data/src/onboarding/action-types.js @@ -26,6 +26,9 @@ const TYPES = { HIDE_TASK_LIST_SUCCESS: 'HIDE_TASK_LIST_SUCCESS', OPTIMISTICALLY_COMPLETE_TASK_REQUEST: 'OPTIMISTICALLY_COMPLETE_TASK_REQUEST', + ACTION_TASK_ERROR: 'ACTION_TASK_ERROR', + ACTION_TASK_REQUEST: 'ACTION_TASK_REQUEST', + ACTION_TASK_SUCCESS: 'ACTION_TASK_SUCCESS', }; export default TYPES; diff --git a/plugins/woocommerce-admin/packages/data/src/onboarding/actions.js b/plugins/woocommerce-admin/packages/data/src/onboarding/actions.js index 41a879adeae..89b62ed0027 100644 --- a/plugins/woocommerce-admin/packages/data/src/onboarding/actions.js +++ b/plugins/woocommerce-admin/packages/data/src/onboarding/actions.js @@ -199,6 +199,28 @@ export function setEmailPrefill( email ) { }; } +export function actionTaskError( taskId, error ) { + return { + type: TYPES.ACTION_TASK_ERROR, + taskId, + error, + }; +} + +export function actionTaskRequest( taskId ) { + return { + type: TYPES.ACTION_TASK_REQUEST, + taskId, + }; +} + +export function actionTaskSuccess( task ) { + return { + type: TYPES.ACTION_TASK_SUCCESS, + task, + }; +} + export function* updateProfileItems( items ) { yield setIsRequesting( 'updateProfileItems', true ); yield setError( 'updateProfileItems', null ); @@ -347,3 +369,21 @@ export function* hideTaskList( id ) { export function* optimisticallyCompleteTask( id ) { yield optimisticallyCompleteTaskRequest( id ); } + +export function* actionTask( id ) { + yield actionTaskRequest( id ); + + try { + const task = yield apiFetch( { + path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/${ id }/action`, + method: 'POST', + } ); + + yield actionTaskSuccess( + possiblyPruneTaskData( task, [ 'isActioned' ] ) + ); + } catch ( error ) { + yield actionTaskError( id, error ); + throw new Error(); + } +} diff --git a/plugins/woocommerce-admin/packages/data/src/onboarding/reducer.js b/plugins/woocommerce-admin/packages/data/src/onboarding/reducer.js index 0bc7b9efec8..bfc07017651 100644 --- a/plugins/woocommerce-admin/packages/data/src/onboarding/reducer.js +++ b/plugins/woocommerce-admin/packages/data/src/onboarding/reducer.js @@ -317,6 +317,39 @@ const onboarding = ( isComplete: true, } ), }; + case TYPES.ACTION_TASK_ERROR: + return { + ...state, + errors: { + ...state.errors, + actionTask: error, + }, + taskLists: getUpdatedTaskLists( state.taskLists, { + id: taskId, + isActioned: false, + } ), + }; + case TYPES.ACTION_TASK_REQUEST: + return { + ...state, + requesting: { + ...state.requesting, + actionTask: true, + }, + taskLists: getUpdatedTaskLists( state.taskLists, { + id: taskId, + isActioned: true, + } ), + }; + case TYPES.ACTION_TASK_SUCCESS: + return { + ...state, + requesting: { + ...state.requesting, + actionTask: false, + }, + taskLists: getUpdatedTaskLists( state.taskLists, task ), + }; default: return state; } diff --git a/plugins/woocommerce-admin/src/API/OnboardingTasks.php b/plugins/woocommerce-admin/src/API/OnboardingTasks.php index 6d92451b0f3..4ee0a0e8ddc 100644 --- a/plugins/woocommerce-admin/src/API/OnboardingTasks.php +++ b/plugins/woocommerce-admin/src/API/OnboardingTasks.php @@ -209,6 +209,19 @@ class OnboardingTasks extends \WC_REST_Data_Controller { ) ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[a-z0-9_\-]+)/action', + array( + array( + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'action_task' ), + 'permission_callback' => array( $this, 'get_tasks_permission_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[a-z0-9_\-]+)/undo_snooze', @@ -951,4 +964,36 @@ class OnboardingTasks extends \WC_REST_Data_Controller { return rest_ensure_response( $json ); } + /** + * Action a single task. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function action_task( $request ) { + $id = $request->get_param( 'id' ); + $task = TaskLists::get_task( $id ); + + if ( ! $task && $id ) { + $task = new Task( + array( + 'id' => $id, + ) + ); + } + + if ( ! $task ) { + return new \WP_Error( + 'woocommerce_rest_invalid_task', + __( 'Sorry, no task with that ID was found.', 'woocommerce-admin' ), + array( + 'status' => 404, + ) + ); + } + + $task->mark_actioned(); + return rest_ensure_response( $task->get_json() ); + } + } diff --git a/plugins/woocommerce-admin/src/Features/OnboardingTasks/Task.php b/plugins/woocommerce-admin/src/Features/OnboardingTasks/Task.php index 0a169bfbcfc..4b811ef8d8a 100644 --- a/plugins/woocommerce-admin/src/Features/OnboardingTasks/Task.php +++ b/plugins/woocommerce-admin/src/Features/OnboardingTasks/Task.php @@ -114,6 +114,13 @@ class Task { */ const SNOOZED_OPTION = 'woocommerce_task_list_remind_me_later_tasks'; + /** + * Name of the actioned option. + * + * @var string + */ + const ACTIONED_OPTION = 'woocommerce_task_list_tracked_completed_actions'; + /** * Duration to milisecond mapping. * @@ -297,6 +304,7 @@ class Task { 'canView' => $this->can_view, 'time' => $this->time, 'level' => $this->level, + 'isActioned' => $this->is_actioned(), 'isDismissed' => $this->is_dismissed(), 'isDismissable' => $this->is_dismissable, 'isSnoozed' => $this->is_snoozed(), @@ -305,4 +313,32 @@ class Task { ); } + /** + * Mark a task as actioned. Used to verify an action has taken place in some tasks. + * + * @return bool + */ + public function mark_actioned() { + $actioned = get_option( self::ACTIONED_OPTION, array() ); + + $actioned[] = $this->id; + $update = update_option( self::ACTIONED_OPTION, array_unique( $actioned ) ); + + if ( $update ) { + wc_admin_record_tracks_event( 'tasklist_actioned_task', array( 'task_name' => $this->id ) ); + } + + return $update; + } + + /** + * Check if a task has been actioned. + * + * @return bool + */ + public function is_actioned() { + $actioned = get_option( self::ACTIONED_OPTION, array() ); + return in_array( $this->id, $actioned, true ); + } + } diff --git a/plugins/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Marketing.php b/plugins/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Marketing.php index a0e471428b9..fc90715483f 100644 --- a/plugins/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Marketing.php +++ b/plugins/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Marketing.php @@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks; use Automattic\WooCommerce\Admin\Features\Features; +use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task; use Automattic\WooCommerce\Admin\Features\RemoteFreeExtensions\Init as RemoteFreeExtensions; /** @@ -23,23 +24,37 @@ class Marketing { 'woocommerce-admin' ), 'is_complete' => self::has_installed_extensions(), - 'can_view' => Features::is_enabled( 'remote-free-extensions' ) && count( self::get_bundles() ) > 0, + 'can_view' => Features::is_enabled( 'remote-free-extensions' ) && count( self::get_plugins() ) > 0, 'time' => __( '1 minute', 'woocommerce-admin' ), ); } /** - * Get the marketing bundles. + * Get the marketing plugins. * * @return array */ - public static function get_bundles() { - return RemoteFreeExtensions::get_extensions( + public static function get_plugins() { + $bundles = RemoteFreeExtensions::get_extensions( array( 'reach', 'grow', ) ); + + return array_reduce( + $bundles, + function( $plugins, $bundle ) { + $visible = array(); + foreach ( $bundle['plugins'] as $plugin ) { + if ( $plugin->is_visible ) { + $visible[] = $plugin; + } + } + return array_merge( $plugins, $visible ); + }, + array() + ); } /** @@ -48,22 +63,29 @@ class Marketing { * @return bool */ public static function has_installed_extensions() { - $bundles = self::get_bundles(); + $plugins = self::get_plugins(); + $remaining = array(); + $installed = array(); - return array_reduce( - $bundles, - function( $has_installed, $bundle ) { - if ( $has_installed ) { - return true; - } - foreach ( $bundle['plugins'] as $plugin ) { - if ( $plugin->is_installed ) { - return true; - } - } - return false; - }, - false - ); + foreach ( $plugins as $plugin ) { + if ( ! $plugin->is_installed ) { + $remaining[] = $plugin; + } else { + $installed[] = $plugin; + } + } + + // All extensions installed. + if ( count( $remaining ) === 0 ) { + return true; + } + + // Make sure the task has been actioned and at least one extension is installed. + $task = new Task( array( 'id' => 'marketing' ) ); + if ( count( $installed ) > 0 && $task->is_actioned() ) { + return true; + } + + return false; } } diff --git a/plugins/woocommerce-admin/tests/features/onboarding-tasks/task.php b/plugins/woocommerce-admin/tests/features/onboarding-tasks/task.php index 55c29978232..c0b862e2663 100644 --- a/plugins/woocommerce-admin/tests/features/onboarding-tasks/task.php +++ b/plugins/woocommerce-admin/tests/features/onboarding-tasks/task.php @@ -213,5 +213,21 @@ class WC_Tests_OnboardingTasks_Task extends WC_Unit_Test_Case { $this->assertArrayHasKey( 'snoozedUntil', $json ); } + /** + * Tests that a task can be actioned. + */ + public function test_action_task() { + $task = new Task( + array( + 'id' => 'wc-unit-test-task', + ) + ); + + $update = $task->mark_actioned(); + $actioned = get_option( Task::ACTIONED_OPTION, array() ); + $this->assertEquals( true, $update ); + $this->assertContains( $task->id, $actioned ); + } + }