diff --git a/plugins/woocommerce-admin/src/Features/OnboardingTasks.php b/plugins/woocommerce-admin/src/Features/OnboardingTasks.php index 92df5fe22a8..ddf326cc9f7 100644 --- a/plugins/woocommerce-admin/src/Features/OnboardingTasks.php +++ b/plugins/woocommerce-admin/src/Features/OnboardingTasks.php @@ -46,11 +46,12 @@ class OnboardingTasks { // This hook needs to run when options are updated via REST. add_action( 'add_option_woocommerce_task_list_complete', array( $this, 'track_completion' ), 10, 2 ); add_action( 'add_option_woocommerce_extended_task_list_complete', array( $this, 'track_extended_completion' ), 10, 2 ); - add_action( 'add_option_woocommerce_task_list_tracked_completed_tasks', array( $this, 'track_task_completion' ), 10, 2 ); - add_action( 'update_option_woocommerce_task_list_tracked_completed_tasks', array( $this, 'track_task_completion' ), 10, 2 ); + add_action( 'add_option_woocommerce_task_list_tracked_completed_tasks', array( $this, 'possibly_track_completed_tasks' ), 10, 2 ); + add_action( 'update_option_woocommerce_task_list_tracked_completed_tasks', array( $this, 'possibly_track_completed_tasks' ), 10, 2 ); add_action( 'admin_enqueue_scripts', array( $this, 'update_option_extended_task_list' ), 15 ); add_action( 'woocommerce_admin_onboarding_tasks', array( $this, 'add_task_dismissal' ), 20 ); add_action( 'woocommerce_admin_onboarding_tasks', array( $this, 'add_task_snoozed' ), 20 ); + add_action( 'woocommerce_admin_onboarding_tasks', array( $this, 'record_completed_tasks' ), PHP_INT_MAX ); if ( ! is_admin() ) { return; @@ -443,13 +444,39 @@ class OnboardingTasks { } } + /** + * Record the tasks that are marked complete. + * + * @param array $task_lists Array of task lists. + */ + public static function record_completed_tasks( $task_lists ) { + $completed_tasks = array_reduce( + $task_lists, + function( $acc, $task_list ) { + foreach ( $task_list['tasks'] as $task ) { + if ( $task['isComplete'] ) { + $acc[] = $task['id']; + } + } + return $acc; + }, + array() + ); + $previously_completed = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() ); + $new_value = array_unique( array_merge( $previously_completed, $completed_tasks ) ); + + update_option( 'woocommerce_task_list_tracked_completed_tasks', $new_value ); + + return $task_lists; + } + /** * Records an event for individual task completion. * * @param mixed $old_value Old value. * @param mixed $new_value New value. */ - public static function track_task_completion( $old_value, $new_value ) { + public static function possibly_track_completed_tasks( $old_value, $new_value ) { $old_value = is_array( $old_value ) ? $old_value : array(); $new_value = is_array( $new_value ) ? $new_value : array(); $untracked_tasks = array_diff( $new_value, $old_value ); diff --git a/plugins/woocommerce-admin/src/Features/OnboardingTasks/Init.php b/plugins/woocommerce-admin/src/Features/OnboardingTasks/Init.php new file mode 100644 index 00000000000..6aa8aa95bb9 --- /dev/null +++ b/plugins/woocommerce-admin/src/Features/OnboardingTasks/Init.php @@ -0,0 +1,594 @@ +is_connected() + : false; + } + + $gateways = WC()->payment_gateways->get_available_payment_gateways(); + $enabled_gateways = array_filter( + $gateways, + function( $gateway ) { + return 'yes' === $gateway->enabled; + } + ); + + // @todo We may want to consider caching some of these and use to check against + // task completion along with cache busting for active tasks. + $settings['automatedTaxSupportedCountries'] = self::get_automated_tax_supported_countries(); + $settings['hasHomepage'] = self::check_task_completion( 'homepage' ) || 'classic' === get_option( 'classic-editor-replace' ); + $settings['hasPaymentGateway'] = ! empty( $enabled_gateways ); + $settings['enabledPaymentGateways'] = array_keys( $enabled_gateways ); + $settings['hasPhysicalProducts'] = count( + wc_get_products( + array( + 'virtual' => false, + 'limit' => 1, + ) + ) + ) > 0; + $settings['hasProducts'] = self::check_task_completion( 'products' ); + $settings['isAppearanceComplete'] = get_option( 'woocommerce_task_list_appearance_complete' ); + $settings['isTaxComplete'] = self::check_task_completion( 'tax' ); + $settings['shippingZonesCount'] = count( \WC_Shipping_Zones::get_zones() ); + $settings['stripeSupportedCountries'] = self::get_stripe_supported_countries(); + $settings['stylesheet'] = get_option( 'stylesheet' ); + $settings['taxJarActivated'] = class_exists( 'WC_Taxjar' ); + $settings['themeMods'] = get_theme_mods(); + $settings['wcPayIsConnected'] = $wc_pay_is_connected; + + return $settings; + } + + /** + * Add task items to component settings. + * + * @param array $settings Component settings. + * @return array + */ + public function component_settings( $settings ) { + // Bail early if not on a wc-admin powered page, or task list shouldn't be shown. + if ( + ! \Automattic\WooCommerce\Admin\Loader::is_admin_page() || + ! count( TaskLists::get_visible() ) + ) { + return $settings; + } + + // If onboarding isn't enabled this will throw warnings. + if ( ! isset( $settings['onboarding'] ) ) { + $settings['onboarding'] = array(); + } + + $settings['onboarding'] = array_merge( + $settings['onboarding'], + array( + 'tasksStatus' => self::get_settings(), + ) + ); + + return $settings; + } + + /** + * Temporarily store the active task to persist across page loads when neccessary (such as publishing a product). Most tasks do not need to do this. + */ + public static function set_active_task() { + if ( isset( $_GET[ self::ACTIVE_TASK_TRANSIENT ] ) ) { // phpcs:ignore csrf ok. + $task = sanitize_title_with_dashes( wp_unslash( $_GET[ self::ACTIVE_TASK_TRANSIENT ] ) ); // phpcs:ignore csrf ok. + + if ( self::check_task_completion( $task ) ) { + return; + } + + set_transient( + self::ACTIVE_TASK_TRANSIENT, + $task, + DAY_IN_SECONDS + ); + } + } + + /** + * Get the name of the active task. + * + * @return string + */ + public static function get_active_task() { + return get_transient( self::ACTIVE_TASK_TRANSIENT ); + } + + /** + * Check for active task completion, and clears the transient. + * + * @return bool + */ + public static function is_active_task_complete() { + $active_task = self::get_active_task(); + + if ( ! $active_task ) { + return false; + } + + if ( self::check_task_completion( $active_task ) ) { + delete_transient( self::ACTIVE_TASK_TRANSIENT ); + return true; + } + + return false; + } + + /** + * Check for task completion of a given task. + * + * @param string $task Name of task. + * @return bool + */ + public static function check_task_completion( $task ) { + switch ( $task ) { + case 'products': + $products = wp_count_posts( 'product' ); + return (int) $products->publish > 0; + case 'homepage': + $homepage_id = get_option( 'woocommerce_onboarding_homepage_post_id', false ); + if ( ! $homepage_id ) { + return false; + } + $post = get_post( $homepage_id ); + $completed = $post && 'publish' === $post->post_status; + return $completed; + case 'tax': + return 'yes' === get_option( 'wc_connect_taxes_enabled' ) || + count( TaxDataStore::get_taxes( array() ) ) > 0 || + false !== get_option( 'woocommerce_no_sales_tax' ); + } + return false; + } + + /** + * Hooks into the product page to add a notice to return to the task list if a product was added. + * + * @param string $hook Page hook. + */ + public static function add_onboarding_product_notice_admin_script( $hook ) { + global $post; + if ( + 'post.php' !== $hook || + 'product' !== $post->post_type || + 'products' !== self::get_active_task() || + ! self::is_active_task_complete() + ) { + return; + } + + $script_assets_filename = Loader::get_script_asset_filename( 'wp-admin-scripts', 'onboarding-product-notice' ); + $script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . 'wp-admin-scripts/' . $script_assets_filename; + + wp_enqueue_script( + 'onboarding-product-notice', + Loader::get_url( 'wp-admin-scripts/onboarding-product-notice', 'js' ), + array_merge( array( WC_ADMIN_APP ), $script_assets ['dependencies'] ), + WC_ADMIN_VERSION_NUMBER, + true + ); + } + + /** + * Hooks into the post page to display a different success notice and sets the active page as the site's home page if visted from onboarding. + * + * @param string $hook Page hook. + */ + public static function add_onboarding_homepage_notice_admin_script( $hook ) { + global $post; + if ( 'post.php' === $hook && 'page' === $post->post_type && isset( $_GET[ self::ACTIVE_TASK_TRANSIENT ] ) && 'homepage' === $_GET[ self::ACTIVE_TASK_TRANSIENT ] ) { // phpcs:ignore csrf ok. + $script_assets_filename = Loader::get_script_asset_filename( 'wp-admin-scripts', 'onboarding-homepage-notice' ); + $script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . 'wp-admin-scripts/' . $script_assets_filename; + + wp_enqueue_script( + 'onboarding-homepage-notice', + Loader::get_url( 'wp-admin-scripts/onboarding-homepage-notice', 'js' ), + array_merge( array( WC_ADMIN_APP ), $script_assets ['dependencies'] ), + WC_ADMIN_VERSION_NUMBER, + true + ); + } + } + + /** + * Adds a notice to return to the task list when the save button is clicked on tax settings pages. + */ + public static function add_onboarding_tax_notice_admin_script() { + $page = isset( $_GET['page'] ) ? $_GET['page'] : ''; // phpcs:ignore csrf ok, sanitization ok. + $tab = isset( $_GET['tab'] ) ? $_GET['tab'] : ''; // phpcs:ignore csrf ok, sanitization ok. + + if ( + 'wc-settings' === $page && + 'tax' === $tab && + 'tax' === self::get_active_task() && + ! self::is_active_task_complete() + ) { + $script_assets_filename = Loader::get_script_asset_filename( 'wp-admin-scripts', 'onboarding-tax-notice' ); + $script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . 'wp-admin-scripts/' . $script_assets_filename; + + wp_enqueue_script( + 'onboarding-tax-notice', + Loader::get_url( 'wp-admin-scripts/onboarding-tax-notice', 'js' ), + array_merge( array( WC_ADMIN_APP ), $script_assets ['dependencies'] ), + WC_ADMIN_VERSION_NUMBER, + true + ); + } + } + + /** + * Adds a notice to return to the task list when the product importeris done running. + * + * @param string $hook Page hook. + */ + public function add_onboarding_product_import_notice_admin_script( $hook ) { + $step = isset( $_GET['step'] ) ? $_GET['step'] : ''; // phpcs:ignore csrf ok, sanitization ok. + if ( 'product_page_product_importer' === $hook && 'done' === $step && 'product-import' === self::get_active_task() ) { + delete_transient( self::ACTIVE_TASK_TRANSIENT ); + + $script_assets_filename = Loader::get_script_asset_filename( 'wp-admin-scripts', 'onboarding-product-import-notice' ); + $script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . 'wp-admin-scripts/' . $script_assets_filename; + + wp_enqueue_script( + 'onboarding-product-import-notice', + Loader::get_url( 'wp-admin-scripts/onboarding-product-import-notice', 'js' ), + array_merge( array( WC_ADMIN_APP ), $script_assets ['dependencies'] ), + WC_ADMIN_VERSION_NUMBER, + true + ); + } + } + + /** + * Get an array of countries that support automated tax. + * + * @return array + */ + public static function get_automated_tax_supported_countries() { + // https://developers.taxjar.com/api/reference/#countries . + $tax_supported_countries = array_merge( + array( 'US', 'CA', 'AU' ), + WC()->countries->get_european_union_countries() + ); + + return $tax_supported_countries; + } + + /** + * Returns a list of Stripe supported countries. This method can be removed once merged to core. + * + * @return array + */ + public static function get_stripe_supported_countries() { + // https://stripe.com/global. + return array( + 'AU', + 'AT', + 'BE', + 'BG', + 'BR', + 'CA', + 'CY', + 'CZ', + 'DK', + 'EE', + 'FI', + 'FR', + 'DE', + 'GR', + 'HK', + 'IN', + 'IE', + 'IT', + 'JP', + 'LV', + 'LT', + 'LU', + 'MY', + 'MT', + 'MX', + 'NL', + 'NZ', + 'NO', + 'PL', + 'PT', + 'RO', + 'SG', + 'SK', + 'SI', + 'ES', + 'SE', + 'CH', + 'GB', + 'US', + 'PR', + ); + } + + /** + * Returns a list of WooCommerce Payments supported countries. + * + * @return array + */ + public static function get_woocommerce_payments_supported_countries() { + return array( + 'US', + 'PR', + 'AU', + 'CA', + 'DE', + 'ES', + 'FR', + 'GB', + 'IE', + 'IT', + 'NZ', + ); + } + + /** + * Records an event when all tasks are completed in the task list. + * + * @param mixed $old_value Old value. + * @param mixed $new_value New value. + */ + public static function track_completion( $old_value, $new_value ) { + if ( $new_value ) { + wc_admin_record_tracks_event( 'tasklist_tasks_completed' ); + } + } + + /** + * Records an event when all tasks are completed in the extended task list. + * + * @param mixed $old_value Old value. + * @param mixed $new_value New value. + */ + public static function track_extended_completion( $old_value, $new_value ) { + if ( $new_value ) { + wc_admin_record_tracks_event( 'extended_tasklist_tasks_completed' ); + } + } + + /** + * Records an event for individual task completion. + * + * @param mixed $old_value Old value. + * @param mixed $new_value New value. + */ + public static function track_task_completion( $old_value, $new_value ) { + $old_value = is_array( $old_value ) ? $old_value : array(); + $new_value = is_array( $new_value ) ? $new_value : array(); + $untracked_tasks = array_diff( $new_value, $old_value ); + + foreach ( $untracked_tasks as $task ) { + wc_admin_record_tracks_event( 'tasklist_task_completed', array( 'task_name' => $task ) ); + } + } + + /** + * Update registered extended task list items. + */ + public static function update_option_extended_task_list() { + if ( + ! \Automattic\WooCommerce\Admin\Loader::is_admin_page() || + ! count( TaskLists::get_visible() ) + ) { + return; + } + $extended_tasks_list_items = get_option( 'woocommerce_extended_task_list_items', array() ); + $registered_extended_tasks_list_items = apply_filters( 'woocommerce_get_registered_extended_tasks', array() ); + if ( $registered_extended_tasks_list_items !== $extended_tasks_list_items ) { + update_option( 'woocommerce_extended_task_list_items', $registered_extended_tasks_list_items ); + $extended_list = TaskLists::get_list( 'extended' ); + if ( ! $extended_list ) { + return; + } + $extended_list->show(); + } + } + + /** + * Add the dismissal status to each task. + * + * @param array $task_lists Task lists. + * @return array + */ + public function add_task_dismissal( $task_lists ) { + $dismissed = get_option( 'woocommerce_task_list_dismissed_tasks', array() ); + + foreach ( $task_lists as $task_list_key => $task_list ) { + foreach ( $task_list['tasks'] as $task_key => $task ) { + if ( isset( $task['isDismissable'] ) && in_array( $task['id'], $dismissed, true ) ) { + $task_lists[ $task_list_key ]['tasks'][ $task_key ]['isDismissed'] = true; + } + } + } + + return $task_lists; + } + + /** + * Add the snoozed status to each task. + * + * @param array $task_lists Task lists. + * @return array + */ + public function add_task_snoozed( $task_lists ) { + $snoozed_tasks = get_option( 'woocommerce_task_list_remind_me_later_tasks', array() ); + + foreach ( $task_lists as $task_list_key => $task_list ) { + foreach ( $task_list['tasks'] as $task_key => $task ) { + if ( isset( $task['isSnoozeable'] ) && in_array( $task['id'], array_keys( $snoozed_tasks ), true ) ) { + $task_lists[ $task_list_key ]['tasks'][ $task_key ]['isSnoozed'] = $snoozed_tasks[ $task['id'] ] > ( time() * 1000 ); + $task_lists[ $task_list_key ]['tasks'][ $task_key ]['snoozedUntil'] = $snoozed_tasks[ $task['id'] ]; + } + } + } + + return $task_lists; + } + + /** + * Add the task list isHidden attribute to each list. + * + * @param array $task_lists Task lists. + * @return array + */ + public function add_task_list_hidden( $task_lists ) { + $hidden = get_option( 'woocommerce_task_list_hidden_lists', array() ); + + foreach ( $task_lists as $key => $task_list ) { + $task_lists[ $key ]['isHidden'] = in_array( $task_list['id'], $hidden, true ); + } + + return $task_lists; + } + + /** + * Get the values from the correct source when attempting to retrieve deprecated options. + * + * @param string $pre_option Pre option value. + * @param string $option Option name. + * @return string + */ + public function get_deprecated_options( $pre_option, $option ) { + if ( defined( 'WC_ADMIN_INSTALLING' ) && WC_ADMIN_INSTALLING ) { + return $pre_option; + }; + + $hidden = get_option( 'woocommerce_task_list_hidden_lists', array() ); + switch ( $option ) { + case 'woocommerce_task_list_hidden': + return in_array( 'setup', $hidden, true ) ? 'yes' : 'no'; + case 'woocommerce_extended_task_list_hidden': + return in_array( 'extended', $hidden, true ) ? 'yes' : 'no'; + } + } + + /** + * Updates the new option names when deprecated options are updated. + * This is a temporary fallback until we can fully remove the old task list components. + * + * @param string $value New value. + * @param string $old_value Old value. + * @param string $option Option name. + * @return string + */ + public function update_deprecated_options( $value, $old_value, $option ) { + switch ( $option ) { + case 'woocommerce_task_list_hidden': + $task_list = TaskLists::get_list( 'setup' ); + if ( ! $task_list ) { + return; + } + $update = 'yes' === $value ? $task_list->hide() : $task_list->show(); + delete_option( 'woocommerce_task_list_hidden' ); + return false; + case 'woocommerce_extended_task_list_hidden': + $task_list = TaskLists::get_list( 'extended' ); + if ( ! $task_list ) { + return; + } + $update = 'yes' === $value ? $task_list->hide() : $task_list->show(); + delete_option( 'woocommerce_extended_task_list_hidden' ); + return false; + } + } +}