From 6acd69e404349bee923b8640dc70960b9f67bcf6 Mon Sep 17 00:00:00 2001 From: Nima Karimi <73110514+nima-karimi@users.noreply.github.com> Date: Wed, 9 Nov 2022 10:41:18 +0000 Subject: [PATCH 01/84] Multichannel Marketing - Core Library (#35099) * Create channel interface and campaign value class * Create MarketingChannels class * Register MarketingChannels class in DI container * Use the new MarketingChannels class to get the installed marketing extensions' data * Use DI container to access InstalledExtensions class * Add InstalledExtensions to the $provides array * Hint that campaign cost should also indicate the currency * Initialize the channels array * Add unit tests for MarketingCampaign * Add unit tests for MarketingChannels * Add Price class to represent a price with currency * Use Price class for marketing campaign's cost * Define a constant to indicate the MCM classes exist This constant will be checked by third-party extensions before utilizing any of the classes/interfaces defined for this feature. * Create MarketingSpecs class to include WC.com API calls * Remove WC.com API calls from Marketing class And replace them with calls from MarketingSpecs class. * Use the const from MarketingSpecs * Fix MarketingChannels unit tests * Add missing settings URL to the channel data Co-authored-by: Nima --- .../includes/wc-update-functions.php | 4 +- .../woocommerce/src/Admin/API/Marketing.php | 20 +- .../src/Admin/API/MarketingOverview.php | 9 +- .../Admin/Marketing/InstalledExtensions.php | 618 +----------------- .../src/Admin/Marketing/MarketingCampaign.php | 110 ++++ .../Marketing/MarketingChannelInterface.php | 82 +++ .../src/Admin/Marketing/MarketingChannels.php | 131 ++++ .../woocommerce/src/Admin/Marketing/Price.php | 70 ++ plugins/woocommerce/src/Container.php | 2 + .../src/Internal/Admin/Marketing.php | 140 +--- .../Admin/Marketing/MarketingSpecs.php | 145 ++++ .../MarketingServiceProvider.php | 44 ++ .../Admin/Marketing/MarketingCampaignTest.php | 58 ++ .../Admin/Marketing/MarketingChannelsTest.php | 120 ++++ 14 files changed, 830 insertions(+), 723 deletions(-) create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php create mode 100644 plugins/woocommerce/src/Admin/Marketing/Price.php create mode 100644 plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php create mode 100644 plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php index 20d966b2e86..2d0c94a14fd 100644 --- a/plugins/woocommerce/includes/wc-update-functions.php +++ b/plugins/woocommerce/includes/wc-update-functions.php @@ -18,7 +18,7 @@ defined( 'ABSPATH' ) || exit; -use Automattic\WooCommerce\Internal\Admin\Marketing; +use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; use Automattic\WooCommerce\Internal\AssignDefaultCategory; use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator; use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore; @@ -2469,5 +2469,5 @@ function wc_update_700_remove_download_log_fk() { * Remove the transient data for recommended marketing extensions. */ function wc_update_700_remove_recommended_marketing_plugins_transient() { - delete_transient( Marketing::RECOMMENDED_PLUGINS_TRANSIENT ); + delete_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT ); } diff --git a/plugins/woocommerce/src/Admin/API/Marketing.php b/plugins/woocommerce/src/Admin/API/Marketing.php index de06c4b7071..a417170f00a 100644 --- a/plugins/woocommerce/src/Admin/API/Marketing.php +++ b/plugins/woocommerce/src/Admin/API/Marketing.php @@ -7,8 +7,8 @@ namespace Automattic\WooCommerce\Admin\API; -use Automattic\WooCommerce\Internal\Admin\Marketing as MarketingFeature; use Automattic\WooCommerce\Admin\PluginsHelper; +use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; defined( 'ABSPATH' ) || exit; @@ -103,9 +103,16 @@ class Marketing extends \WC_REST_Data_Controller { * @return \WP_Error|\WP_REST_Response */ public function get_recommended_plugins( $request ) { + /** + * MarketingSpecs class. + * + * @var MarketingSpecs $marketing_specs + */ + $marketing_specs = wc_get_container()->get( MarketingSpecs::class ); + // Default to marketing category (if no category set). $category = ( ! empty( $request->get_param( 'category' ) ) ) ? $request->get_param( 'category' ) : 'marketing'; - $all_plugins = MarketingFeature::get_instance()->get_recommended_plugins(); + $all_plugins = $marketing_specs->get_recommended_plugins(); $valid_plugins = []; $per_page = $request->get_param( 'per_page' ); @@ -130,7 +137,14 @@ class Marketing extends \WC_REST_Data_Controller { * @return \WP_Error|\WP_REST_Response */ public function get_knowledge_base_posts( $request ) { + /** + * MarketingSpecs class. + * + * @var MarketingSpecs $marketing_specs + */ + $marketing_specs = wc_get_container()->get( MarketingSpecs::class ); + $category = $request->get_param( 'category' ); - return rest_ensure_response( MarketingFeature::get_instance()->get_knowledge_base_posts( $category ) ); + return rest_ensure_response( $marketing_specs->get_knowledge_base_posts( $category ) ); } } diff --git a/plugins/woocommerce/src/Admin/API/MarketingOverview.php b/plugins/woocommerce/src/Admin/API/MarketingOverview.php index 930dcb4c0fc..883ce04c1bb 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingOverview.php +++ b/plugins/woocommerce/src/Admin/API/MarketingOverview.php @@ -125,7 +125,14 @@ class MarketingOverview extends \WC_REST_Data_Controller { * @return \WP_Error|\WP_REST_Response */ public function get_installed_plugins( $request ) { - return rest_ensure_response( InstalledExtensions::get_data() ); + /** + * InstalledExtensions + * + * @var InstalledExtensions $installed_extensions + */ + $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); + + return rest_ensure_response( $installed_extensions->get_data() ); } } diff --git a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php index 3f90c964d28..9669b9014c6 100644 --- a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php +++ b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php @@ -5,598 +5,46 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use Automattic\WooCommerce\Admin\PluginsHelper; - /** * Installed Marketing Extensions class. */ class InstalledExtensions { + /** + * MarketingChannels repository + * + * @var MarketingChannels + */ + protected $marketing_channels; + + /** + * Class initialization, invoked by the DI container. + * + * @param MarketingChannels $marketing_channels The MarketingChannels repository. + * + * @internal + */ + final public function init( MarketingChannels $marketing_channels ) { + $this->marketing_channels = $marketing_channels; + } /** * Gets an array of plugin data for the "Installed marketing extensions" card. - * - * Valid extensions statuses are: installed, activated, configured */ - public static function get_data() { - $data = []; - - $automatewoo = self::get_automatewoo_extension_data(); - $aw_referral = self::get_aw_referral_extension_data(); - $aw_birthdays = self::get_aw_birthdays_extension_data(); - $mailchimp = self::get_mailchimp_extension_data(); - $facebook = self::get_facebook_extension_data(); - $pinterest = self::get_pinterest_extension_data(); - $google = self::get_google_extension_data(); - $amazon_ebay = self::get_amazon_ebay_extension_data(); - $mailpoet = self::get_mailpoet_extension_data(); - $creative_mail = self::get_creative_mail_extension_data(); - $tiktok = self::get_tiktok_extension_data(); - $jetpack_crm = self::get_jetpack_crm_extension_data(); - $zapier = self::get_zapier_extension_data(); - $salesforce = self::get_salesforce_extension_data(); - $vimeo = self::get_vimeo_extension_data(); - $trustpilot = self::get_trustpilot_extension_data(); - - if ( $automatewoo ) { - $data[] = $automatewoo; - } - - if ( $aw_referral ) { - $data[] = $aw_referral; - } - - if ( $aw_birthdays ) { - $data[] = $aw_birthdays; - } - - if ( $mailchimp ) { - $data[] = $mailchimp; - } - - if ( $facebook ) { - $data[] = $facebook; - } - - if ( $pinterest ) { - $data[] = $pinterest; - } - - if ( $google ) { - $data[] = $google; - } - - if ( $amazon_ebay ) { - $data[] = $amazon_ebay; - } - - if ( $mailpoet ) { - $data[] = $mailpoet; - } - - if ( $creative_mail ) { - $data[] = $creative_mail; - } - - if ( $tiktok ) { - $data[] = $tiktok; - } - - if ( $jetpack_crm ) { - $data[] = $jetpack_crm; - } - - if ( $zapier ) { - $data[] = $zapier; - } - - if ( $salesforce ) { - $data[] = $salesforce; - } - - if ( $vimeo ) { - $data[] = $vimeo; - } - - if ( $trustpilot ) { - $data[] = $trustpilot; - } - - return $data; + public function get_data(): array { + return array_map( + function ( MarketingChannelInterface $channel ) { + return [ + 'slug' => $channel->get_slug(), + 'status' => $channel->is_setup_completed() ? 'configured' : 'activated', + 'settingsUrl' => $channel->get_setup_url(), + 'name' => $channel->get_name(), + 'description' => $channel->get_description(), + 'product_listings_status' => $channel->get_product_listings_status(), + 'errors_no' => $channel->get_errors_no(), + 'icon' => $channel->get_icon_url(), + ]; + }, + $this->marketing_channels->get_registered_channels() + ); } - - /** - * Get allowed plugins. - * - * @return array - */ - public static function get_allowed_plugins() { - return [ - 'automatewoo', - 'mailchimp-for-woocommerce', - 'creative-mail-by-constant-contact', - 'facebook-for-woocommerce', - 'pinterest-for-woocommerce', - 'google-listings-and-ads', - 'hubspot-for-woocommerce', - 'woocommerce-amazon-ebay-integration', - 'mailpoet', - ]; - } - - /** - * Get AutomateWoo extension data. - * - * @return array|bool - */ - protected static function get_automatewoo_extension_data() { - $slug = 'automatewoo'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; - - if ( 'activated' === $data['status'] && function_exists( 'AW' ) ) { - $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings' ); - $data['docsUrl'] = 'https://automatewoo.com/docs/'; - $data['status'] = 'configured'; // Currently no configuration step. - } - - return $data; - } - - /** - * Get AutomateWoo Refer a Friend extension data. - * - * @return array|bool - */ - protected static function get_aw_referral_extension_data() { - $slug = 'automatewoo-referrals'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; - - if ( 'activated' === $data['status'] ) { - $data['docsUrl'] = 'https://automatewoo.com/docs/refer-a-friend/'; - $data['status'] = 'configured'; - if ( function_exists( 'AW_Referrals' ) ) { - $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings&tab=referrals' ); - } - } - - return $data; - } - - /** - * Get AutomateWoo Birthdays extension data. - * - * @return array|bool - */ - protected static function get_aw_birthdays_extension_data() { - $slug = 'automatewoo-birthdays'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; - - if ( 'activated' === $data['status'] ) { - $data['docsUrl'] = 'https://automatewoo.com/docs/getting-started-with-birthdays/'; - $data['status'] = 'configured'; - if ( function_exists( 'AW_Birthdays' ) ) { - $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings&tab=birthdays' ); - } - } - - return $data; - } - - /** - * Get MailChimp extension data. - * - * @return array|bool - */ - protected static function get_mailchimp_extension_data() { - $slug = 'mailchimp-for-woocommerce'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/mailchimp.svg'; - - if ( 'activated' === $data['status'] && function_exists( 'mailchimp_is_configured' ) ) { - $data['docsUrl'] = 'https://mailchimp.com/help/connect-or-disconnect-mailchimp-for-woocommerce/'; - $data['settingsUrl'] = admin_url( 'admin.php?page=mailchimp-woocommerce' ); - - if ( mailchimp_is_configured() ) { - $data['status'] = 'configured'; - } - } - - return $data; - } - - /** - * Get Facebook extension data. - * - * @return array|bool - */ - protected static function get_facebook_extension_data() { - $slug = 'facebook-for-woocommerce'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/facebook-icon.svg'; - - if ( $data['status'] === 'activated' && function_exists( 'facebook_for_woocommerce' ) ) { - $integration = facebook_for_woocommerce()->get_integration(); - - if ( $integration->is_configured() ) { - $data['status'] = 'configured'; - } - - $data['settingsUrl'] = facebook_for_woocommerce()->get_settings_url(); - $data['docsUrl'] = facebook_for_woocommerce()->get_documentation_url(); - } - - return $data; - } - - /** - * Get Pinterest extension data. - * - * @return array|bool - */ - protected static function get_pinterest_extension_data() { - $slug = 'pinterest-for-woocommerce'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/pinterest.svg'; - - // TODO: Finalise docs url. - $data['docsUrl'] = 'https://woocommerce.com/document/pinterest-for-woocommerce/?utm_medium=product'; - - if ( 'activated' === $data['status'] && class_exists( 'Pinterest_For_Woocommerce' ) ) { - $pinterest_onboarding_completed = Pinterest_For_Woocommerce()::is_setup_complete(); - if ( $pinterest_onboarding_completed ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/pinterest/settings' ); - } else { - $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/pinterest/landing' ); - } - } - - return $data; - } - - /** - * Get Google extension data. - * - * @return array|bool - */ - protected static function get_google_extension_data() { - $slug = 'google-listings-and-ads'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/google.svg'; - - if ( 'activated' === $data['status'] && function_exists( 'woogle_get_container' ) && class_exists( '\Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService' ) ) { - - $merchant_center = woogle_get_container()->get( \Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService::class ); - - if ( $merchant_center->is_setup_complete() ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/google/settings' ); - } else { - $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/google/start' ); - } - - $data['docsUrl'] = 'https://woocommerce.com/document/google-listings-and-ads/?utm_medium=product'; - } - - return $data; - } - - /** - * Get Amazon / Ebay extension data. - * - * @return array|bool - */ - protected static function get_amazon_ebay_extension_data() { - $slug = 'woocommerce-amazon-ebay-integration'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/amazon-ebay.svg'; - - if ( 'activated' === $data['status'] && class_exists( '\CodistoConnect' ) ) { - - $codisto_merchantid = get_option( 'codisto_merchantid' ); - - // Use same check as codisto admin tabs. - if ( is_numeric( $codisto_merchantid ) ) { - $data['status'] = 'configured'; - } - - $data['settingsUrl'] = admin_url( 'admin.php?page=codisto-settings' ); - $data['docsUrl'] = 'https://woocommerce.com/document/multichannel-for-woocommerce-google-amazon-ebay-walmart-integration/?utm_medium=product'; - } - - return $data; - } - - /** - * Get MailPoet extension data. - * - * @return array|bool - */ - protected static function get_mailpoet_extension_data() { - $slug = 'mailpoet'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/mailpoet.svg'; - - if ( 'activated' === $data['status'] && class_exists( '\MailPoet\API\API' ) ) { - $mailpoet_api = \MailPoet\API\API::MP( 'v1' ); - - if ( ! method_exists( $mailpoet_api, 'isSetupComplete' ) || $mailpoet_api->isSetupComplete() ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=mailpoet-settings' ); - } else { - $data['settingsUrl'] = admin_url( 'admin.php?page=mailpoet-newsletters' ); - } - - $data['docsUrl'] = 'https://kb.mailpoet.com/'; - $data['supportUrl'] = 'https://www.mailpoet.com/support/'; - } - - return $data; - } - - /** - * Get Creative Mail for WooCommerce extension data. - * - * @return array|bool - */ - protected static function get_creative_mail_extension_data() { - $slug = 'creative-mail-by-constant-contact'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/creative-mail-by-constant-contact.png'; - - if ( 'activated' === $data['status'] && class_exists( '\CreativeMail\Helpers\OptionsHelper' ) ) { - if ( ! method_exists( '\CreativeMail\Helpers\OptionsHelper', 'get_instance_id' ) || \CreativeMail\Helpers\OptionsHelper::get_instance_id() !== null ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=creativemail_settings' ); - } else { - $data['settingsUrl'] = admin_url( 'admin.php?page=creativemail' ); - } - - $data['docsUrl'] = 'https://app.creativemail.com/kb/help/WooCommerce'; - $data['supportUrl'] = 'https://app.creativemail.com/kb/help/'; - } - - return $data; - } - - /** - * Get TikTok for WooCommerce extension data. - * - * @return array|bool - */ - protected static function get_tiktok_extension_data() { - $slug = 'tiktok-for-business'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/tiktok.jpg'; - - if ( 'activated' === $data['status'] ) { - if ( false !== get_option( 'tt4b_access_token' ) ) { - $data['status'] = 'configured'; - } - - $data['settingsUrl'] = admin_url( 'admin.php?page=tiktok' ); - $data['docsUrl'] = 'https://woocommerce.com/document/tiktok-for-woocommerce/'; - $data['supportUrl'] = 'https://ads.tiktok.com/athena/user-feedback/?identify_key=6a1e079024806640c5e1e695d13db80949525168a052299b4970f9c99cb5ac78'; - } - - return $data; - } - - /** - * Get Jetpack CRM for WooCommerce extension data. - * - * @return array|bool - */ - protected static function get_jetpack_crm_extension_data() { - $slug = 'zero-bs-crm'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/jetpack-crm.png'; - - if ( 'activated' === $data['status'] ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=zerobscrm-plugin-settings' ); - $data['docsUrl'] = 'https://kb.jetpackcrm.com/'; - $data['supportUrl'] = 'https://kb.jetpackcrm.com/crm-support/'; - } - - return $data; - } - - /** - * Get WooCommerce Zapier extension data. - * - * @return array|bool - */ - protected static function get_zapier_extension_data() { - $slug = 'woocommerce-zapier'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/zapier.png'; - - if ( 'activated' === $data['status'] ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=wc-settings&tab=wc_zapier' ); - $data['docsUrl'] = 'https://docs.om4.io/woocommerce-zapier/'; - } - - return $data; - } - - /** - * Get Salesforce extension data. - * - * @return array|bool - */ - protected static function get_salesforce_extension_data() { - $slug = 'integration-with-salesforce'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/salesforce.jpg'; - - if ( 'activated' === $data['status'] && class_exists( '\Integration_With_Salesforce_Admin' ) ) { - if ( ! method_exists( '\Integration_With_Salesforce_Admin', 'get_connection_status' ) || \Integration_With_Salesforce_Admin::get_connection_status() ) { - $data['status'] = 'configured'; - } - - $data['settingsUrl'] = admin_url( 'admin.php?page=integration-with-salesforce' ); - $data['docsUrl'] = 'https://woocommerce.com/document/salesforce-integration/'; - $data['supportUrl'] = 'https://wpswings.com/submit-query/'; - } - - return $data; - } - - /** - * Get Vimeo extension data. - * - * @return array|bool - */ - protected static function get_vimeo_extension_data() { - $slug = 'vimeo'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/vimeo.png'; - - if ( 'activated' === $data['status'] && class_exists( '\Tribe\Vimeo_WP\Vimeo\Vimeo_Auth' ) ) { - if ( method_exists( '\Tribe\Vimeo_WP\Vimeo\Vimeo_Auth', 'has_access_token' ) ) { - $vimeo_auth = new \Tribe\Vimeo_WP\Vimeo\Vimeo_Auth(); - if ( $vimeo_auth->has_access_token() ) { - $data['status'] = 'configured'; - } - } else { - $data['status'] = 'configured'; - } - - $data['settingsUrl'] = admin_url( 'options-general.php?page=vimeo_settings' ); - $data['docsUrl'] = 'https://woocommerce.com/document/vimeo/'; - $data['supportUrl'] = 'https://vimeo.com/help/contact'; - } - - return $data; - } - - /** - * Get Trustpilot extension data. - * - * @return array|bool - */ - protected static function get_trustpilot_extension_data() { - $slug = 'trustpilot-reviews'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/trustpilot.png'; - - if ( 'activated' === $data['status'] ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=woocommerce-trustpilot-settings-page' ); - $data['docsUrl'] = 'https://woocommerce.com/document/trustpilot-reviews/'; - $data['supportUrl'] = 'https://support.trustpilot.com/hc/en-us/requests/new'; - } - - return $data; - } - - - /** - * Get an array of basic data for a given extension. - * - * @param string $slug Plugin slug. - * - * @return array|false - */ - protected static function get_extension_base_data( $slug ) { - $status = PluginsHelper::is_plugin_active( $slug ) ? 'activated' : 'installed'; - $plugin_data = PluginsHelper::get_plugin_data( $slug ); - - if ( ! $plugin_data ) { - return false; - } - - return [ - 'slug' => $slug, - 'status' => $status, - 'name' => $plugin_data['Name'], - 'description' => html_entity_decode( wp_trim_words( $plugin_data['Description'], 20 ) ), - 'supportUrl' => 'https://woocommerce.com/my-account/create-a-ticket/?utm_medium=product', - ]; - } - } diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php new file mode 100644 index 00000000000..7b3f99a4b3a --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php @@ -0,0 +1,110 @@ +id = $id; + $this->title = $title; + $this->manage_url = $manage_url; + $this->cost = $cost; + } + + /** + * Returns the marketing campaign's unique identifier. + * + * @return string + */ + public function get_id(): string { + return $this->id; + } + + /** + * Returns the title of the marketing campaign. + * + * @return string + */ + public function get_title(): string { + return $this->title; + } + + /** + * Returns the URL to manage the marketing campaign. + * + * @return string + */ + public function get_manage_url(): string { + return $this->manage_url; + } + + /** + * Returns the cost of the marketing campaign with the currency. + * + * @return Price|null + */ + public function get_cost(): ?Price { + return $this->cost; + } + + /** + * Serialize the marketing campaign data. + * + * @return array + */ + public function jsonSerialize() { + return [ + 'id' => $this->get_id(), + 'title' => $this->get_title(), + 'manage_url' => $this->get_manage_url(), + 'cost' => $this->get_cost(), + ]; + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php new file mode 100644 index 00000000000..3a7233f073b --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -0,0 +1,82 @@ +marketing_specs = $marketing_specs; + $this->allowed_channels = $this->get_allowed_channels(); + } + + /** + * Registers a marketing channel. + * + * Note that only a predetermined list of third party extensions can be registered as a marketing channel. + * + * @param MarketingChannelInterface $channel The marketing channel to register. + * + * @return void + * + * @see MarketingChannels::is_channel_allowed() Checks if the marketing channel is allowed to be registered or not. + */ + public function register( MarketingChannelInterface $channel ): void { + if ( ! $this->is_channel_allowed( $channel ) ) { + // Silently log an error and bail. + wc_get_logger()->error( sprintf( 'Marketing channel %s (%s) cannot be registered!', $channel->get_name(), $channel->get_slug() ) ); + + return; + } + + $this->registered_channels[ $channel->get_slug() ] = $channel; + } + + /** + * Returns an array of all registered marketing channels. + * + * @return MarketingChannelInterface[] + */ + public function get_registered_channels(): array { + /** + * Filter the list of registered marketing channels. + * + * Note that only a predetermined list of third party extensions can be registered as a marketing channel. + * Any new plugins added to this array will be cross-checked with that list, which is obtained from WooCommerce.com API. + * + * @param MarketingChannelInterface[] $channels Array of registered marketing channels. + * + * @since x.x.x + */ + $channels = apply_filters( 'woocommerce_marketing_channels', $this->registered_channels ); + + // Only return allowed channels. + $allowed_channels = array_filter( + $channels, + function ( MarketingChannelInterface $channel ) { + if ( ! $this->is_channel_allowed( $channel ) ) { + // Silently log an error and bail. + wc_get_logger()->error( sprintf( 'Marketing channel %s (%s) cannot be registered!', $channel->get_name(), $channel->get_slug() ) ); + + return false; + } + + return true; + } + ); + + return array_values( $allowed_channels ); + } + + /** + * Returns an array of plugin slugs for the marketing channels that are allowed to be registered. + * + * @return array + */ + protected function get_allowed_channels(): array { + $recommended_channels = $this->marketing_specs->get_recommended_plugins(); + if ( empty( $recommended_channels ) ) { + return []; + } + + return array_column( $recommended_channels, 'product', 'product' ); + } + + /** + * Determines whether the given marketing channel is allowed to be registered. + * + * @param MarketingChannelInterface $channel The marketing channel object. + * + * @return bool + */ + protected function is_channel_allowed( MarketingChannelInterface $channel ): bool { + return isset( $this->allowed_channels[ $channel->get_slug() ] ); + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/Price.php b/plugins/woocommerce/src/Admin/Marketing/Price.php new file mode 100644 index 00000000000..9dbb00837ae --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/Price.php @@ -0,0 +1,70 @@ +value = $value; + $this->currency = $currency; + } + + /** + * Get value of the price. + * + * @return string + */ + public function get_value(): string { + return $this->value; + } + + /** + * Get the currency of the price. + * + * @return string + */ + public function get_currency(): string { + return $this->currency; + } + + /** + * Serialize the price data. + * + * @return array + */ + public function jsonSerialize() { + return [ + 'value' => $this->get_value(), + 'currency' => $this->get_currency(), + ]; + } +} diff --git a/plugins/woocommerce/src/Container.php b/plugins/woocommerce/src/Container.php index 3815db81840..0e64a37d322 100644 --- a/plugins/woocommerce/src/Container.php +++ b/plugins/woocommerce/src/Container.php @@ -10,6 +10,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\COTMig use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\FeaturesServiceProvider; +use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\MarketingServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrdersControllersServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrderAdminServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrderMetaBoxServiceProvider; @@ -65,6 +66,7 @@ final class Container { OrderMetaBoxServiceProvider::class, OrderAdminServiceProvider::class, FeaturesServiceProvider::class, + MarketingServiceProvider::class, ); /** diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing.php b/plugins/woocommerce/src/Internal/Admin/Marketing.php index 65cf4a5b464..f11cd2d31d9 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing.php @@ -7,7 +7,6 @@ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\Marketing\InstalledExtensions; -use Automattic\WooCommerce\Internal\Admin\Loader; use Automattic\WooCommerce\Admin\PageController; /** @@ -17,20 +16,6 @@ class Marketing { use CouponsMovedTrait; - /** - * Name of recommended plugins transient. - * - * @var string - */ - const RECOMMENDED_PLUGINS_TRANSIENT = 'wc_marketing_recommended_plugins'; - - /** - * Name of knowledge base post transient. - * - * @var string - */ - const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base'; - /** * Class instance. * @@ -180,124 +165,15 @@ class Marketing { return $settings; } - $settings['marketing']['installedExtensions'] = InstalledExtensions::get_data(); + /** + * InstalledExtensions helper class. + * + * @var InstalledExtensions $installed_extensions + */ + $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); + + $settings['marketing']['installedExtensions'] = $installed_extensions->get_data(); return $settings; } - - /** - * Load recommended plugins from WooCommerce.com - * - * @return array - */ - public function get_recommended_plugins() { - $plugins = get_transient( self::RECOMMENDED_PLUGINS_TRANSIENT ); - - if ( false === $plugins ) { - $request = wp_remote_get( - 'https://woocommerce.com/wp-json/wccom/marketing-tab/1.2/recommendations.json', - array( - 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), - ) - ); - $plugins = []; - - if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { - $plugins = json_decode( $request['body'], true ); - } - - set_transient( - self::RECOMMENDED_PLUGINS_TRANSIENT, - $plugins, - // Expire transient in 15 minutes if remote get failed. - // Cache an empty result to avoid repeated failed requests. - empty( $plugins ) ? 900 : 3 * DAY_IN_SECONDS - ); - } - - return array_values( $plugins ); - } - - /** - * Load knowledge base posts from WooCommerce.com - * - * @param string $category Category of posts to retrieve. - * @return array - */ - public function get_knowledge_base_posts( $category ) { - - $kb_transient = self::KNOWLEDGE_BASE_TRANSIENT; - - $categories = array( - 'marketing' => 1744, - 'coupons' => 25202, - ); - - // Default to marketing category (if no category set on the kb component). - if ( ! empty( $category ) && array_key_exists( $category, $categories ) ) { - $category_id = $categories[ $category ]; - $kb_transient = $kb_transient . '_' . strtolower( $category ); - } else { - $category_id = $categories['marketing']; - } - - $posts = get_transient( $kb_transient ); - - if ( false === $posts ) { - $request_url = add_query_arg( - array( - 'categories' => $category_id, - 'page' => 1, - 'per_page' => 8, - '_embed' => 1, - ), - 'https://woocommerce.com/wp-json/wp/v2/posts?utm_medium=product' - ); - - $request = wp_remote_get( - $request_url, - array( - 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), - ) - ); - $posts = []; - - if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { - $raw_posts = json_decode( $request['body'], true ); - - foreach ( $raw_posts as $raw_post ) { - $post = [ - 'title' => html_entity_decode( $raw_post['title']['rendered'] ), - 'date' => $raw_post['date_gmt'], - 'link' => $raw_post['link'], - 'author_name' => isset( $raw_post['author_name'] ) ? html_entity_decode( $raw_post['author_name'] ) : '', - 'author_avatar' => isset( $raw_post['author_avatar_url'] ) ? $raw_post['author_avatar_url'] : '', - ]; - - $featured_media = $raw_post['_embedded']['wp:featuredmedia'] ?? []; - if ( count( $featured_media ) > 0 ) { - $image = current( $featured_media ); - $post['image'] = add_query_arg( - array( - 'resize' => '650,340', - 'crop' => 1, - ), - $image['source_url'] - ); - } - - $posts[] = $post; - } - } - - set_transient( - $kb_transient, - $posts, - // Expire transient in 15 minutes if remote get failed. - empty( $posts ) ? 900 : DAY_IN_SECONDS - ); - } - - return $posts; - } } diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php new file mode 100644 index 00000000000..ee36ba1375c --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php @@ -0,0 +1,145 @@ + 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), + ) + ); + $plugins = []; + + if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { + $plugins = json_decode( $request['body'], true ); + } + + set_transient( + self::RECOMMENDED_PLUGINS_TRANSIENT, + $plugins, + // Expire transient in 15 minutes if remote get failed. + // Cache an empty result to avoid repeated failed requests. + empty( $plugins ) ? 900 : 3 * DAY_IN_SECONDS + ); + } + + return array_values( $plugins ); + } + + /** + * Load knowledge base posts from WooCommerce.com + * + * @param string|null $category Category of posts to retrieve. + * @return array + */ + public function get_knowledge_base_posts( ?string $category ): array { + $kb_transient = self::KNOWLEDGE_BASE_TRANSIENT; + + $categories = array( + 'marketing' => 1744, + 'coupons' => 25202, + ); + + // Default to marketing category (if no category set on the kb component). + if ( ! empty( $category ) && array_key_exists( $category, $categories ) ) { + $category_id = $categories[ $category ]; + $kb_transient = $kb_transient . '_' . strtolower( $category ); + } else { + $category_id = $categories['marketing']; + } + + $posts = get_transient( $kb_transient ); + + if ( false === $posts ) { + $request_url = add_query_arg( + array( + 'categories' => $category_id, + 'page' => 1, + 'per_page' => 8, + '_embed' => 1, + ), + 'https://woocommerce.com/wp-json/wp/v2/posts?utm_medium=product' + ); + + $request = wp_remote_get( + $request_url, + array( + 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), + ) + ); + $posts = []; + + if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { + $raw_posts = json_decode( $request['body'], true ); + + foreach ( $raw_posts as $raw_post ) { + $post = [ + 'title' => html_entity_decode( $raw_post['title']['rendered'] ), + 'date' => $raw_post['date_gmt'], + 'link' => $raw_post['link'], + 'author_name' => isset( $raw_post['author_name'] ) ? html_entity_decode( $raw_post['author_name'] ) : '', + 'author_avatar' => isset( $raw_post['author_avatar_url'] ) ? $raw_post['author_avatar_url'] : '', + ]; + + $featured_media = $raw_post['_embedded']['wp:featuredmedia'] ?? []; + if ( count( $featured_media ) > 0 ) { + $image = current( $featured_media ); + $post['image'] = add_query_arg( + array( + 'resize' => '650,340', + 'crop' => 1, + ), + $image['source_url'] + ); + } + + $posts[] = $post; + } + } + + set_transient( + $kb_transient, + $posts, + // Expire transient in 15 minutes if remote get failed. + empty( $posts ) ? 900 : DAY_IN_SECONDS + ); + } + + return $posts; + } +} diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php new file mode 100644 index 00000000000..8e27386ae86 --- /dev/null +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php @@ -0,0 +1,44 @@ +share( MarketingSpecs::class ); + $this->share( MarketingChannels::class )->addArgument( MarketingSpecs::class ); + $this->share( InstalledExtensions::class )->addArgument( MarketingChannels::class ); + } +} diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php new file mode 100644 index 00000000000..401e1630294 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php @@ -0,0 +1,58 @@ +assertEquals( '1234', $marketing_campaign->get_id() ); + $this->assertEquals( 'Ad #1234', $marketing_campaign->get_title() ); + $this->assertEquals( 'https://example.com/manage-campaigns', $marketing_campaign->get_manage_url() ); + $this->assertNotNull( $marketing_campaign->get_cost() ); + $this->assertEquals( 'USD', $marketing_campaign->get_cost()->get_currency() ); + $this->assertEquals( '1000', $marketing_campaign->get_cost()->get_value() ); + } + + /** + * @testdox `cost` property can be null. + */ + public function test_cost_can_be_null() { + $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns' ); + + $this->assertNull( $marketing_campaign->get_cost() ); + } + + /** + * @testdox It can be serialized to JSON including all its properties. + */ + public function test_can_be_serialized_to_json() { + $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); + + $json = wp_json_encode( $marketing_campaign ); + $this->assertNotEmpty( $json ); + $this->assertEqualSets( + [ + 'id' => $marketing_campaign->get_id(), + 'title' => $marketing_campaign->get_title(), + 'manage_url' => $marketing_campaign->get_manage_url(), + 'cost' => [ + 'value' => $marketing_campaign->get_cost()->get_value(), + 'currency' => $marketing_campaign->get_cost()->get_currency(), + ], + ], + json_decode( $json, true ) + ); + } +} diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php new file mode 100644 index 00000000000..cf5885a0557 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php @@ -0,0 +1,120 @@ +createMock( MarketingChannelInterface::class ); + $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_specs = $this->createMock( MarketingSpecs::class ); + $marketing_specs->expects( $this->once() ) + ->method( 'get_recommended_plugins' ) + ->willReturn( + [ + [ + 'product' => 'test-channel-1', + ], + ] + ); + + $marketing_channels = new MarketingChannels(); + $marketing_channels->init( $marketing_specs ); + $marketing_channels->register( $test_channel ); + + $this->assertNotEmpty( $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel, $marketing_channels->get_registered_channels()[0] ); + } + + /** + * @testdox A marketing channel can NOT be registered using the `register` method if it is NOT in the allowed list. + */ + public function test_does_not_register_disallowed_channels() { + $test_channel = $this->createMock( MarketingChannelInterface::class ); + $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_specs = $this->createMock( MarketingSpecs::class ); + $marketing_specs->expects( $this->once() )->method( 'get_recommended_plugins' )->willReturn( [] ); + + $marketing_channels = new MarketingChannels(); + $marketing_channels->init( $marketing_specs ); + $marketing_channels->register( $test_channel ); + + $this->assertEmpty( $marketing_channels->get_registered_channels() ); + } + + /** + * @testdox A marketing channel can be registered using the `woocommerce_marketing_channels` WordPress filter if it is in the allowed list. + */ + public function test_registers_allowed_channels_using_wp_filter() { + $test_channel = $this->createMock( MarketingChannelInterface::class ); + $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_specs = $this->createMock( MarketingSpecs::class ); + $marketing_specs->expects( $this->once() ) + ->method( 'get_recommended_plugins' ) + ->willReturn( + [ + [ + 'product' => 'test-channel-1', + ], + ] + ); + + $marketing_channels = new MarketingChannels(); + $marketing_channels->init( $marketing_specs ); + + add_filter( + 'woocommerce_marketing_channels', + function ( array $channels ) use ( $test_channel ) { + $channels[ $test_channel->get_slug() ] = $test_channel; + + return $channels; + } + ); + + $this->assertNotEmpty( $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel, $marketing_channels->get_registered_channels()[0] ); + } + + /** + * @testdox A marketing channel can NOT be registered using the `woocommerce_marketing_channels` WordPress filter if it NOT is in the allowed list. + */ + public function test_does_not_register_disallowed_channels_using_wp_filter() { + $test_channel = $this->createMock( MarketingChannelInterface::class ); + $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + set_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, [] ); + + add_filter( + 'woocommerce_marketing_channels', + function ( array $channels ) use ( $test_channel ) { + $channels[ $test_channel->get_slug() ] = $test_channel; + + return $channels; + } + ); + + $marketing_channels = new MarketingChannels(); + $this->assertEmpty( $marketing_channels->get_registered_channels() ); + } +} From dc7a233cb15a79e883fea6aea634e13765183020 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 8 Dec 2022 23:53:36 +0800 Subject: [PATCH 02/84] Add Channels card into Marketing page. --- .../Channels/Channels.tsx | 44 +++++++++++++++ .../overview-multichannel/Channels/index.ts | 1 + .../overview-multichannel/Channels/types.ts | 25 +++++++++ .../Channels/useChannels.ts | 20 +++++++ .../Channels/useRecommendedChannels.ts | 54 +++++++++++++++++++ .../Channels/useRegisteredChannels.ts | 18 +++++++ .../MarketingOverviewMultichannel.tsx | 2 + 7 files changed, 164 insertions(+) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx new file mode 100644 index 00000000000..d1bb669719f --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { CardBody } from '@wordpress/components'; +import { Spinner } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { Card } from '~/marketing/components'; +import { useChannels } from './useChannels'; + +export const Channels = () => { + const { + loading, + data: { registeredChannels, recommendedChannels }, + } = useChannels(); + + if ( loading ) { + return ( + + + + + + ); + } + + const description = + registeredChannels.length === 0 && + recommendedChannels.length > 0 && + __( 'Start by adding a channel to your store', 'woocommerce' ); + + return ( + + { /* TODO: */ } + Body + + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts new file mode 100644 index 00000000000..da0d9c56072 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts @@ -0,0 +1 @@ +export { Channels } from './Channels'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts new file mode 100644 index 00000000000..b0af93dd395 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts @@ -0,0 +1,25 @@ +// TODO: The following types are copied from plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/types.ts. +// They are may be changed later, depending on the outcome of API development. + +type Subcategory = { + slug: string; + name: string; +}; + +type Tag = { + slug: string; + name: string; +}; + +export type RecommendedChannel = { + title: string; + description: string; + url: string; + direct_install: boolean; + icon: string; + product: string; + plugin: string; + categories: Array< string >; + subcategories: Array< Subcategory >; + tags: Array< Tag >; +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts new file mode 100644 index 00000000000..122b466ddf7 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ +import { useRecommendedChannels } from './useRecommendedChannels'; +import { useRegisteredChannels } from './useRegisteredChannels'; + +export const useChannels = () => { + const { loading: loadingRegistered, data: dataRegistered } = + useRegisteredChannels(); + const { loading: loadingRecommended, data: dataRecommended } = + useRecommendedChannels(); + + return { + loading: loadingRegistered || loadingRecommended, + data: { + registeredChannels: dataRegistered, + recommendedChannels: dataRecommended, + }, + }; +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts new file mode 100644 index 00000000000..a7bf23df8d5 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts @@ -0,0 +1,54 @@ +/** + * Internal dependencies + */ +import { RecommendedChannel } from './types'; + +type UseRecommendedChannels = { + loading: boolean; + data: Array< RecommendedChannel >; +}; + +export const useRecommendedChannels = (): UseRecommendedChannels => { + // TODO: call API here to get data. + // The following are just dummy data for testing now. + return { + loading: false, + data: [ + { + title: 'Facebook for WooCommerce', + description: + 'List your products and create ads on Facebook and Instagram.', + url: 'https://woocommerce.com/products/facebook/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: true, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/facebook.svg', + product: 'facebook-for-woocommerce', + plugin: 'facebook-for-woocommerce/facebook-for-woocommerce.php', + categories: [ 'marketing' ], + subcategories: [ + { slug: 'sales-channels', name: 'Sales channels' }, + ], + tags: [ + { + slug: 'built-by-woocommerce', + name: 'Built by WooCommerce', + }, + ], + }, + { + title: 'Amazon, eBay & Walmart Integration for WooCommerce', + description: + 'Get the official Amazon, eBay and Walmart extension and create, sync and manage multichannel listings directly from WooCommerce.', + url: 'https://woocommerce.com/products/amazon-ebay-integration/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: false, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/amazon-ebay.svg', + product: 'amazon-ebay-integration', + plugin: 'woocommerce-amazon-ebay-integration/woocommerce-amazon-ebay-integration.php', + categories: [ 'marketing' ], + subcategories: [ + { slug: 'sales-channels', name: 'Sales channels' }, + ], + tags: [], + }, + ], + }; +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts new file mode 100644 index 00000000000..ee7849e8f70 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts @@ -0,0 +1,18 @@ +export const useRegisteredChannels = () => { + // TODO: call API here to get data. + // The following are just dummy data for testing now. + return { + loading: false, + data: [ + { + name: 'Google Listings and Ads', + description: + 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + isSetupCompleted: true, + setupUrl: 'www.google.com/setup', + manageUrl: 'www.google.com/manage', + }, + ], + }; +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index 3afe9c3295a..cffa5655873 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -7,6 +7,7 @@ import { useUser } from '@woocommerce/data'; * Internal dependencies */ import { getAdminSetting } from '~/utils/admin-settings'; +import { Channels } from './Channels'; import { InstalledExtensions } from './InstalledExtensions'; import { DiscoverTools } from './DiscoverTools'; import { LearnMarketing } from './LearnMarketing'; @@ -22,6 +23,7 @@ export const MarketingOverviewMultichannel: React.FC = () => { return (
+ { shouldShowExtensions && } From 52166434658e68e25f2d1536448a56376e67ac1e Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 9 Dec 2022 02:19:36 +0800 Subject: [PATCH 03/84] Code refactor with CenteredSpinner. --- .../CenteredSpinner/CenteredSpinner.scss | 4 ++++ .../CenteredSpinner/CenteredSpinner.tsx | 17 +++++++++++++++++ .../components/CenteredSpinner/index.ts | 1 + .../client/marketing/components/index.js | 1 + .../DiscoverTools/DiscoverTools.scss | 8 -------- .../DiscoverTools/DiscoverTools.tsx | 10 ++++++---- 6 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.scss create mode 100644 plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.tsx create mode 100644 plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/index.ts diff --git a/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.scss b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.scss new file mode 100644 index 00000000000..de66e759c34 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.scss @@ -0,0 +1,4 @@ +.woocommerce-centered-spinner { + display: flex; + justify-content: center; +} diff --git a/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.tsx b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.tsx new file mode 100644 index 00000000000..19ec4959533 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/CenteredSpinner.tsx @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import { Spinner } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import './CenteredSpinner.scss'; + +export const CenteredSpinner = () => { + return ( +
+ +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/index.ts b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/index.ts new file mode 100644 index 00000000000..cb45370f30c --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CenteredSpinner/index.ts @@ -0,0 +1 @@ +export { CenteredSpinner } from './CenteredSpinner'; diff --git a/plugins/woocommerce-admin/client/marketing/components/index.js b/plugins/woocommerce-admin/client/marketing/components/index.js index c514c3ca633..9131b5f57a5 100644 --- a/plugins/woocommerce-admin/client/marketing/components/index.js +++ b/plugins/woocommerce-admin/client/marketing/components/index.js @@ -5,3 +5,4 @@ export { default as Slider } from './slider'; export { default as ReadBlogMessage } from './ReadBlogMessage'; export { CollapsibleCard, CardBody, CardDivider } from './CollapsibleCard'; export { PluginCardBody } from './PluginCardBody'; +export { CenteredSpinner } from './CenteredSpinner'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss index c11516aec58..ab21090dc41 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss @@ -1,11 +1,3 @@ -.woocommerce-marketing-discover-tools-card { - // place the spinner in the center of the card. - .woocommerce-spinner { - display: block; - margin: auto; - } -} - .woocommerce-marketing-discover-tools-card-body-empty-content { width: 50%; margin: auto; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx index ca79f53b085..a116640fbe0 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx @@ -5,12 +5,15 @@ import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { Icon, trendingUp } from '@wordpress/icons'; import { recordEvent } from '@woocommerce/tracks'; -import { Spinner } from '@woocommerce/components'; /** * Internal dependencies */ -import { CollapsibleCard, CardBody } from '~/marketing/components'; +import { + CollapsibleCard, + CardBody, + CenteredSpinner, +} from '~/marketing/components'; import { useRecommendedPlugins } from './useRecommendedPlugins'; import { PluginsTabPanel } from './PluginsTabPanel'; import './DiscoverTools.scss'; @@ -30,7 +33,7 @@ export const DiscoverTools = () => { if ( isInitializing ) { return ( - + ); } @@ -72,7 +75,6 @@ export const DiscoverTools = () => { return ( { renderCardContent() } From a8c8be7c266a3e14bcc0174a35a0de8a63228cfa Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 9 Dec 2022 02:20:05 +0800 Subject: [PATCH 04/84] Use CenteredSpinner in Channels. --- .../marketing/overview-multichannel/Channels/Channels.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index d1bb669719f..a13552a11fa 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -3,12 +3,11 @@ */ import { __ } from '@wordpress/i18n'; import { CardBody } from '@wordpress/components'; -import { Spinner } from '@woocommerce/components'; /** * Internal dependencies */ -import { Card } from '~/marketing/components'; +import { Card, CenteredSpinner } from '~/marketing/components'; import { useChannels } from './useChannels'; export const Channels = () => { @@ -21,7 +20,7 @@ export const Channels = () => { return ( - + ); From 995fb7e02d80b721376562727416317534ca177d Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 9 Dec 2022 19:30:24 +0800 Subject: [PATCH 05/84] Code refactor with CardHeaderTitle component. This component will be used in other components later. --- .../components/CardHeaderTitle/CardHeaderTitle.scss | 6 ++++++ .../components/CardHeaderTitle/CardHeaderTitle.tsx | 12 ++++++++++++ .../marketing/components/CardHeaderTitle/index.ts | 1 + .../components/CollapsibleCard/CollapsibleCard.scss | 4 ---- .../components/CollapsibleCard/CollapsibleCard.tsx | 3 ++- .../client/marketing/components/index.js | 1 + 6 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.scss create mode 100644 plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.tsx create mode 100644 plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/index.ts diff --git a/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.scss b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.scss new file mode 100644 index 00000000000..b5b6e6ced57 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.scss @@ -0,0 +1,6 @@ +.woocommerce-marketing-card-header-title { + font-size: 20px; + font-weight: 400; + line-height: 28px; + letter-spacing: 0; +} diff --git a/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.tsx b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.tsx new file mode 100644 index 00000000000..6861b8fbe0f --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/CardHeaderTitle.tsx @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import './CardHeaderTitle.scss'; + +export const CardHeaderTitle: React.FC = ( { children } ) => { + return ( +
+ { children } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/index.ts b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/index.ts new file mode 100644 index 00000000000..424e3f75a78 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CardHeaderTitle/index.ts @@ -0,0 +1 @@ +export { CardHeaderTitle } from './CardHeaderTitle'; diff --git a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.scss b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.scss index 5c1aef987c1..4303cbd3e65 100644 --- a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.scss +++ b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.scss @@ -1,9 +1,5 @@ .woocommerce-collapsible-card { .components-card-header { - font-size: 20px; - font-weight: 400; - line-height: 28px; - letter-spacing: 0; cursor: pointer; } } diff --git a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx index c0a767b1738..c7044810222 100644 --- a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx @@ -17,6 +17,7 @@ import classnames from 'classnames'; /** * Internal dependencies */ +import { CardHeaderTitle } from '~/marketing/components'; import './CollapsibleCard.scss'; type CollapsibleCardProps = { @@ -48,7 +49,7 @@ const CollapsibleCard: React.FC< CollapsibleCardProps > = ( { ) } > -
{ header }
+ { header } + ); + } + + return ( + + ); + }; + + return ( + } + name={ plugin.title } + pills={ plugin.tags.map( ( tag ) => ( + { tag.name } + ) ) } + description={ plugin.description } + button={ renderButton() } + /> + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts index e3497c710ba..5e2c782db33 100644 --- a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts @@ -1 +1,2 @@ export { PluginCardBody } from './PluginCardBody'; +export { SmartPluginCardBody } from './SmartPluginCardBody'; diff --git a/plugins/woocommerce-admin/client/marketing/components/index.js b/plugins/woocommerce-admin/client/marketing/components/index.js index a1679d3b906..dc971c75160 100644 --- a/plugins/woocommerce-admin/client/marketing/components/index.js +++ b/plugins/woocommerce-admin/client/marketing/components/index.js @@ -4,7 +4,7 @@ export { default as ProductIcon } from './product-icon'; export { default as Slider } from './slider'; export { default as ReadBlogMessage } from './ReadBlogMessage'; export { CollapsibleCard, CardBody, CardDivider } from './CollapsibleCard'; -export { PluginCardBody } from './PluginCardBody'; +export { PluginCardBody, SmartPluginCardBody } from './PluginCardBody'; export { CardHeaderTitle } from './CardHeaderTitle'; export { CardHeaderDescription } from './CardHeaderDescription'; export { CenteredSpinner } from './CenteredSpinner'; From 7852448a7a090b11dbac1dbfceac8416940b6956 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 10 Dec 2022 00:51:57 +0800 Subject: [PATCH 11/84] Use SmartPluginCardBody in Channels component. --- .../Channels/Channels.tsx | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 927fd7c5d3d..c9f33c4c3eb 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -1,8 +1,9 @@ /** * External dependencies */ +import { Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Card, CardHeader, CardBody } from '@wordpress/components'; +import { Card, CardHeader, CardBody, CardDivider } from '@wordpress/components'; /** * Internal dependencies @@ -11,6 +12,7 @@ import { CardHeaderTitle, CardHeaderDescription, CenteredSpinner, + SmartPluginCardBody, } from '~/marketing/components'; import { useChannels } from './useChannels'; import './Channels.scss'; @@ -21,6 +23,11 @@ export const Channels = () => { data: { registeredChannels, recommendedChannels }, } = useChannels(); + /** + * TODO: we may need to filter the channels against + * `@woocommerce/data` installed plugins. + */ + if ( loading ) { return ( @@ -36,21 +43,58 @@ export const Channels = () => { ); } - const description = - registeredChannels.length === 0 && - recommendedChannels.length > 0 && - __( 'Start by adding a channel to your store', 'woocommerce' ); + /* + * If users have no registered channels, + * we display recommended channels without collapsible list. + */ + if ( registeredChannels.length === 0 && recommendedChannels.length > 0 ) { + return ( + + + + { __( 'Channels', 'woocommerce' ) } + + + { __( + 'Start by adding a channel to your store', + 'woocommerce' + ) } + + + { recommendedChannels.map( ( el, idx ) => { + return ( + + + { idx < recommendedChannels.length - 1 && ( + + ) } + + ); + } ) } + + ); + } + /* + * TODO: Users have registered channels, + * display the registered channels. + * If there are recommended channels, + * display them in a collapsible list. + */ return ( { __( 'Channels', 'woocommerce' ) } - { description } - { /* TODO: */ } - Body + + { /* TODO: registered channels here. */ } + + { /* TODO: recommended channels here. */ } + { recommendedChannels.length > 0 && ( + recommended + ) } ); }; From 92112e27a161dd3bddb2746b40d3dfeef5bf1f10 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 10 Dec 2022 01:36:28 +0800 Subject: [PATCH 12/84] Add InstalledChannel type. --- .../client/marketing/types/InstalledChannel.ts | 13 +++++++++++++ .../client/marketing/types/index.ts | 5 +++++ 2 files changed, 18 insertions(+) create mode 100644 plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts diff --git a/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts b/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts new file mode 100644 index 00000000000..1901ed59f37 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts @@ -0,0 +1,13 @@ +export type SyncStatusType = 'synced' | 'syncing' | 'failed'; +export type IssueTypeType = 'error' | 'warning' | 'none'; + +export type InstalledChannel = { + slug: string; + title: string; + description: string; + icon: string; + syncStatus: SyncStatusType; + issueType: IssueTypeType; + issueText: string; + manageUrl: string; +}; diff --git a/plugins/woocommerce-admin/client/marketing/types/index.ts b/plugins/woocommerce-admin/client/marketing/types/index.ts index 146295d936e..24fbe739fd0 100644 --- a/plugins/woocommerce-admin/client/marketing/types/index.ts +++ b/plugins/woocommerce-admin/client/marketing/types/index.ts @@ -1,2 +1,7 @@ export { InstalledPlugin } from './InstalledPlugin'; export { RecommendedPlugin } from './RecommendedPlugin'; +export { + SyncStatusType, + IssueTypeType, + InstalledChannel, +} from './InstalledChannel'; From a9010ffd68b009e9c51de82e845e3af4ff0dc88c Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 10 Dec 2022 01:37:21 +0800 Subject: [PATCH 13/84] Add InstalledChannelCardBody to Channels. --- .../Channels/Channels.tsx | 16 ++++++++++++- .../Channels/InstalledChannelCardBody.scss | 2 ++ .../Channels/InstalledChannelCardBody.tsx | 24 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index c9f33c4c3eb..3b7b62b1670 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -16,6 +16,7 @@ import { } from '~/marketing/components'; import { useChannels } from './useChannels'; import './Channels.scss'; +import { InstalledChannelCardBody } from './InstalledChannelCardBody'; export const Channels = () => { const { @@ -90,10 +91,23 @@ export const Channels = () => {
{ /* TODO: registered channels here. */ } + { registeredChannels.map( ( el, idx ) => { + return ( + + + { idx < registeredChannels.length - 1 && ( + + ) } + + ); + } ) } { /* TODO: recommended channels here. */ } { recommendedChannels.length > 0 && ( - recommended + <> + + recommended + ) } ); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss new file mode 100644 index 00000000000..2f710f93e97 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss @@ -0,0 +1,2 @@ +.woocommerce-marketing-installed-channel-card-body { +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx new file mode 100644 index 00000000000..03daeed32ff --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { CardBody } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { InstalledChannel } from '~/marketing/types'; +import './InstalledChannelCardBody.scss'; + +type InstalledChannelCardBodyProps = { + installedChannel: InstalledChannel; +}; + +export const InstalledChannelCardBody: React.FC< + InstalledChannelCardBodyProps +> = ( { installedChannel } ) => { + return ( + + InstalledChannelCardBody + + ); +}; From be621df8da1cf327c88c17b80c1065b5abe2f5b8 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 10 Dec 2022 02:28:13 +0800 Subject: [PATCH 14/84] Add more data for testing. --- .../Channels/useRecommendedChannels.ts | 74 ++++++++++++++++++- .../Channels/useRegisteredChannels.ts | 5 ++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts index a7bf23df8d5..40a43823b47 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts @@ -25,7 +25,10 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { plugin: 'facebook-for-woocommerce/facebook-for-woocommerce.php', categories: [ 'marketing' ], subcategories: [ - { slug: 'sales-channels', name: 'Sales channels' }, + { + slug: 'sales-channels', + name: 'Sales channels', + }, ], tags: [ { @@ -34,6 +37,70 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { }, ], }, + { + title: 'Google Listings and Ads', + description: + 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', + url: 'https://woocommerce.com/products/google-listings-and-ads/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: true, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', + product: 'google-listings-and-ads', + plugin: 'google-listings-and-ads/google-listings-and-ads.php', + categories: [ 'marketing' ], + subcategories: [ + { + slug: 'sales-channels', + name: 'Sales channels', + }, + ], + tags: [ + { + slug: 'built-by-woocommerce', + name: 'Built by WooCommerce', + }, + ], + }, + { + title: 'Pinterest for WooCommerce', + description: + 'Grow your business on Pinterest! Use this official plugin to allow shoppers to Pin products while browsing your store, track conversions, and advertise on Pinterest.', + url: 'https://woocommerce.com/products/pinterest-for-woocommerce/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: true, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/pinterest.svg', + product: 'pinterest-for-woocommerce', + plugin: 'pinterest-for-woocommerce/pinterest-for-woocommerce.php', + categories: [ 'marketing' ], + subcategories: [ + { + slug: 'sales-channels', + name: 'Sales channels', + }, + ], + tags: [ + { + slug: 'built-by-woocommerce', + name: 'Built by WooCommerce', + }, + ], + }, + { + title: 'TikTok for WooCommerce', + description: + 'Create advertising campaigns and reach one billion global users with TikTok for WooCommerce.', + url: 'https://woocommerce.com/products/tiktok-for-woocommerce/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: true, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/tiktok.jpg', + product: 'tiktok-for-business', + plugin: 'tiktok-for-business/tiktok-for-woocommerce.php', + categories: [ 'marketing' ], + subcategories: [ + { + slug: 'sales-channels', + name: 'Sales channels', + }, + ], + tags: [], + }, { title: 'Amazon, eBay & Walmart Integration for WooCommerce', description: @@ -45,7 +112,10 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { plugin: 'woocommerce-amazon-ebay-integration/woocommerce-amazon-ebay-integration.php', categories: [ 'marketing' ], subcategories: [ - { slug: 'sales-channels', name: 'Sales channels' }, + { + slug: 'sales-channels', + name: 'Sales channels', + }, ], tags: [], }, diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts index ee7849e8f70..65a8b7289d0 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts @@ -5,13 +5,18 @@ export const useRegisteredChannels = () => { loading: false, data: [ { + slug: 'google-listings-and-ads', name: 'Google Listings and Ads', + title: 'Google Listings and Ads', description: 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', isSetupCompleted: true, setupUrl: 'www.google.com/setup', manageUrl: 'www.google.com/manage', + syncStatus: 'synced' as const, + issueType: 'none' as const, + issueText: '', }, ], }; From 36cdf7fed5fe789d7a4f418acf725ea2bb7023f3 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 10 Dec 2022 02:41:24 +0800 Subject: [PATCH 15/84] Add changelog. --- ...-34903-multichannel-marketing-frontend-34906-channels-card | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/feature-34903-multichannel-marketing-frontend-34906-channels-card diff --git a/plugins/woocommerce/changelog/feature-34903-multichannel-marketing-frontend-34906-channels-card b/plugins/woocommerce/changelog/feature-34903-multichannel-marketing-frontend-34906-channels-card new file mode 100644 index 00000000000..f1602b854c7 --- /dev/null +++ b/plugins/woocommerce/changelog/feature-34903-multichannel-marketing-frontend-34906-channels-card @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add a new Channels card in multichannel marketing page. From 02ce7cccc5ece0ba6e7aead365836ced0bdf8358 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 13 Dec 2022 21:35:01 +0800 Subject: [PATCH 16/84] Modify PluginCardBody to accept className, and change description type. --- .../components/PluginCardBody/PluginCardBody.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/PluginCardBody.tsx b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/PluginCardBody.tsx index 1e67edeb3ed..6b0b4acd0c8 100644 --- a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/PluginCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/PluginCardBody.tsx @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * Internal dependencies */ @@ -5,6 +10,7 @@ import { CardBody } from '~/marketing/components'; import './PluginCardBody.scss'; type PluginCardBodyProps = { + className?: string; icon: JSX.Element; name: string; @@ -13,7 +19,7 @@ type PluginCardBodyProps = { */ pills?: Array< JSX.Element >; - description: string; + description: React.ReactNode; button?: JSX.Element; }; @@ -21,6 +27,7 @@ type PluginCardBodyProps = { * Renders a CardBody layout component to display plugin info and button. */ export const PluginCardBody: React.FC< PluginCardBodyProps > = ( { + className, icon, name, pills, @@ -28,7 +35,12 @@ export const PluginCardBody: React.FC< PluginCardBodyProps > = ( { button, } ) => { return ( - +
{ icon }
From 437ebb20a8ea566d8b37033a11afb87f3793f2ca Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 13 Dec 2022 21:36:09 +0800 Subject: [PATCH 17/84] Display sync status in Channels card. --- .../Channels/InstalledChannelCardBody.scss | 20 ++++++ .../Channels/InstalledChannelCardBody.tsx | 71 +++++++++++++++++-- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss index 2f710f93e97..16eff2f99db 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss @@ -1,2 +1,22 @@ .woocommerce-marketing-installed-channel-card-body { + .woocommerce-marketing-sync-status { + display: flex; + align-items: center; + gap: $gap-smallest; + + &__failed { + color: $alert-red; + fill: $alert-red; + } + + &__syncing { + color: #008a20; + fill: #008a20; + } + + &__synced { + color: #008a20; + fill: #008a20; + } + } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx index 03daeed32ff..5552e7601a5 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx @@ -1,24 +1,85 @@ /** * External dependencies */ -import { CardBody } from '@wordpress/components'; +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import GridiconCheckmarkCircle from 'gridicons/dist/checkmark-circle'; +import GridiconSync from 'gridicons/dist/sync'; +import GridiconNotice from 'gridicons/dist/notice'; +import classnames from 'classnames'; /** * Internal dependencies */ -import { InstalledChannel } from '~/marketing/types'; +import { PluginCardBody } from '~/marketing/components'; +import { InstalledChannel, SyncStatusType } from '~/marketing/types'; import './InstalledChannelCardBody.scss'; type InstalledChannelCardBodyProps = { installedChannel: InstalledChannel; }; +type SyncStatusPropsType = { + status: SyncStatusType; +}; + +const iconSize = 18; +const className = 'woocommerce-marketing-sync-status'; + +const SyncStatus: React.FC< SyncStatusPropsType > = ( { status } ) => { + if ( status === 'failed' ) { + return ( +
+ + { __( 'Sync failed', 'woocommerce' ) } +
+ ); + } + + if ( status === 'syncing' ) { + return ( +
+ + { __( 'Syncing', 'woocommerce' ) } +
+ ); + } + + return ( +
+ + { __( 'Synced', 'woocommerce' ) } +
+ ); +}; + export const InstalledChannelCardBody: React.FC< InstalledChannelCardBodyProps > = ( { installedChannel } ) => { return ( - - InstalledChannelCardBody - + + } + name={ installedChannel.title } + description={ +
+ +
+ } + button={ + + } + /> ); }; From e6e6dc19efa38444a811bec6b91552ad3e89397f Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 13 Dec 2022 22:47:16 +0800 Subject: [PATCH 18/84] Display issue status in Channels card. --- .../Channels/InstalledChannelCardBody.scss | 25 ++++++++++ .../Channels/InstalledChannelCardBody.tsx | 48 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss index 16eff2f99db..3a80fa7ef64 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.scss @@ -1,4 +1,13 @@ .woocommerce-marketing-installed-channel-card-body { + .woocommerce-marketing-installed-channel-description { + display: flex; + gap: $gap-smaller; + + &__separator::before { + content: '•'; + } + } + .woocommerce-marketing-sync-status { display: flex; align-items: center; @@ -19,4 +28,20 @@ fill: #008a20; } } + + .woocommerce-marketing-issue-status { + display: flex; + align-items: center; + gap: $gap-smallest; + + &__error { + color: $alert-red; + fill: $alert-red; + } + + &__warning { + color: $alert-yellow; + fill: $alert-yellow; + } + } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx index 5552e7601a5..7a734c7669b 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx @@ -57,6 +57,50 @@ const SyncStatus: React.FC< SyncStatusPropsType > = ( { status } ) => { ); }; +type IssueStatusPropsType = { + installedChannel: InstalledChannel; +}; + +const issueStatusClassName = 'woocommerce-marketing-issue-status'; + +const IssueStatus: React.FC< IssueStatusPropsType > = ( { + installedChannel, +} ) => { + if ( installedChannel.issueType === 'error' ) { + return ( +
+ + { installedChannel.issueText } +
+ ); + } + + if ( installedChannel.issueType === 'warning' ) { + return ( +
+ + { installedChannel.issueText } +
+ ); + } + + return ( +
+ { installedChannel.issueText } +
+ ); +}; + export const InstalledChannelCardBody: React.FC< InstalledChannelCardBodyProps > = ( { installedChannel } ) => { @@ -71,8 +115,10 @@ export const InstalledChannelCardBody: React.FC< } name={ installedChannel.title } description={ -
+
+
+
} button={ From f713c01f563fc389864f98a997fb2bd5978dbbdb Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 13 Dec 2022 23:22:27 +0800 Subject: [PATCH 19/84] Add more properties into InstalledChannel type. --- .../client/marketing/types/InstalledChannel.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts b/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts index 1901ed59f37..c4e735089ac 100644 --- a/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts +++ b/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts @@ -6,8 +6,10 @@ export type InstalledChannel = { title: string; description: string; icon: string; + isSetupCompleted: boolean; + setupUrl: string; + manageUrl: string; syncStatus: SyncStatusType; issueType: IssueTypeType; issueText: string; - manageUrl: string; }; From 9963fd07ab29c9197a532d5454b5c2004de7ccef Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 13 Dec 2022 23:23:13 +0800 Subject: [PATCH 20/84] Display description and button based on channel setup status in Channels card. --- .../Channels/InstalledChannelCardBody.tsx | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx index 7a734c7669b..abd7f85dc5f 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx @@ -104,6 +104,40 @@ const IssueStatus: React.FC< IssueStatusPropsType > = ( { export const InstalledChannelCardBody: React.FC< InstalledChannelCardBodyProps > = ( { installedChannel } ) => { + /** + * The description section in the channel card. + * + * If setup is not completed, this would be the channel description. + * + * If setup is completed, this would be an element with sync status and issue status. + */ + const description = ! installedChannel.isSetupCompleted ? ( + installedChannel.description + ) : ( +
+ +
+ +
+ ); + + /** + * The action button in the channel card. + * + * If setup is not completed, this would be a "Finish setup" primary button. + * + * If setup is completed, this would be a "Manage" secondary button. + */ + const button = ! installedChannel.isSetupCompleted ? ( + + ) : ( + + ); + return ( } name={ installedChannel.title } - description={ -
- -
- -
- } - button={ - - } + description={ description } + button={ button } /> ); }; From 4a2205bcd1707625d9f968a5e572b3fec526775f Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 14 Dec 2022 01:13:00 +0800 Subject: [PATCH 21/84] Display recommended channels in a collapsible list in Channels card. --- .../Channels/Channels.scss | 4 ++ .../Channels/Channels.tsx | 21 ++++---- .../CollapsibleRecommendedChannels.tsx | 53 +++++++++++++++++++ 3 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss index aff331de94f..a9d5343d54a 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss @@ -4,4 +4,8 @@ align-items: flex-start; gap: $gap-smallest; } + + .components-button.is-link { + text-decoration: none; + } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 3b7b62b1670..7013bc34143 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -17,6 +17,7 @@ import { import { useChannels } from './useChannels'; import './Channels.scss'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; +import { CollapsibleRecommendedChannels } from './CollapsibleRecommendedChannels'; export const Channels = () => { const { @@ -46,7 +47,8 @@ export const Channels = () => { /* * If users have no registered channels, - * we display recommended channels without collapsible list. + * we display recommended channels without collapsible list + * and with a description in the card header. */ if ( registeredChannels.length === 0 && recommendedChannels.length > 0 ) { return ( @@ -77,10 +79,10 @@ export const Channels = () => { } /* - * TODO: Users have registered channels, - * display the registered channels. + * Users have registered channels, + * so here we display the registered channels first. * If there are recommended channels, - * display them in a collapsible list. + * we display them next in a collapsible list. */ return ( @@ -90,7 +92,7 @@ export const Channels = () => { - { /* TODO: registered channels here. */ } + { /* Registered channels section. */ } { registeredChannels.map( ( el, idx ) => { return ( @@ -102,12 +104,11 @@ export const Channels = () => { ); } ) } - { /* TODO: recommended channels here. */ } + { /* Recommended channels section. */ } { recommendedChannels.length > 0 && ( - <> - - recommended - + ) } ); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx new file mode 100644 index 00000000000..99308d4c4db --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { Fragment, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { CardBody, CardDivider, Button, Icon } from '@wordpress/components'; +import { chevronUp, chevronDown } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { SmartPluginCardBody } from '~/marketing/components'; +import { RecommendedChannel } from './types'; +import './Channels.scss'; + +type RecommendedChannelsType = { + recommendedChannels: Array< RecommendedChannel >; +}; + +export const CollapsibleRecommendedChannels: React.FC< + RecommendedChannelsType +> = ( { recommendedChannels } ) => { + const [ collapsed, setCollapsed ] = useState( true ); + + return ( + <> + + + + + { ! collapsed && + recommendedChannels.map( ( el, idx ) => { + return ( + + + { idx < recommendedChannels.length - 1 && ( + + ) } + + ); + } ) } + + ); +}; From 6b801097541b0b6ef371c01eb6b79da15534c340 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 14 Dec 2022 02:07:35 +0800 Subject: [PATCH 22/84] Code refactor with RecommendedChannelsList. --- .../Channels/Channels.tsx | 15 +++----- .../CollapsibleRecommendedChannels.tsx | 18 ++++------ .../Channels/RecommendedChannelsList.tsx | 36 +++++++++++++++++++ 3 files changed, 46 insertions(+), 23 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 7013bc34143..5511a5c0957 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -12,12 +12,12 @@ import { CardHeaderTitle, CardHeaderDescription, CenteredSpinner, - SmartPluginCardBody, } from '~/marketing/components'; import { useChannels } from './useChannels'; import './Channels.scss'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; import { CollapsibleRecommendedChannels } from './CollapsibleRecommendedChannels'; +import { RecommendedChannelsList } from './RecommendedChannelsList'; export const Channels = () => { const { @@ -64,16 +64,9 @@ export const Channels = () => { ) } - { recommendedChannels.map( ( el, idx ) => { - return ( - - - { idx < recommendedChannels.length - 1 && ( - - ) } - - ); - } ) } + ); } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx index 99308d4c4db..57fc3c688b9 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx @@ -9,8 +9,8 @@ import { chevronUp, chevronDown } from '@wordpress/icons'; /** * Internal dependencies */ -import { SmartPluginCardBody } from '~/marketing/components'; import { RecommendedChannel } from './types'; +import { RecommendedChannelsList } from './RecommendedChannelsList'; import './Channels.scss'; type RecommendedChannelsType = { @@ -37,17 +37,11 @@ export const CollapsibleRecommendedChannels: React.FC< /> - { ! collapsed && - recommendedChannels.map( ( el, idx ) => { - return ( - - - { idx < recommendedChannels.length - 1 && ( - - ) } - - ); - } ) } + { ! collapsed && ( + + ) } ); }; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx new file mode 100644 index 00000000000..7b61170f74c --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { Fragment } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { CardDivider } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { SmartPluginCardBody } from '~/marketing/components'; +import { RecommendedChannel } from './types'; +import './Channels.scss'; + +type RecommendedChannelListPropsType = { + recommendedChannels: Array< RecommendedChannel >; +}; + +export const RecommendedChannelsList: React.FC< + RecommendedChannelListPropsType +> = ( { recommendedChannels } ) => { + return ( + <> + { recommendedChannels.map( ( el, idx ) => { + return ( + + + { idx < recommendedChannels.length - 1 && ( + + ) } + + ); + } ) } + + ); +}; From 6c127d70c830b288e187551fe5b015bf394dfabe Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 14 Dec 2022 20:11:40 +0800 Subject: [PATCH 23/84] Add dummy data for testing. --- .../Channels/useRegisteredChannels.ts | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts index 65a8b7289d0..d02667433e9 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts @@ -1,3 +1,64 @@ +// // TODO: To be removed. This is for testing loading state. +// export const useRegisteredChannels = () => { +// // TODO: call API here to get data. +// // The following are just dummy data for testing now. +// return { +// loading: true, +// data: [], +// }; +// }; + +// // TODO: To be removed. This is for testing isSetupCompleted = false. +// export const useRegisteredChannels = () => { +// // TODO: call API here to get data. +// // The following are just dummy data for testing now. +// return { +// loading: false, +// data: [ +// { +// slug: 'google-listings-and-ads', +// name: 'Google Listings and Ads', +// title: 'Google Listings and Ads', +// description: +// 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', +// icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', +// isSetupCompleted: false, +// setupUrl: 'https://www.example.com/setup', +// manageUrl: 'https://www.example.com/manage', +// syncStatus: 'synced' as const, +// issueType: 'none' as const, +// issueText: 'No issues to resolve', +// }, +// ], +// }; +// }; + +// // TODO: To be removed. This is for testing error state. +// export const useRegisteredChannels = () => { +// // TODO: call API here to get data. +// // The following are just dummy data for testing now. +// return { +// loading: false, +// data: [ +// { +// slug: 'google-listings-and-ads', +// name: 'Google Listings and Ads', +// title: 'Google Listings and Ads', +// description: +// 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', +// icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', +// isSetupCompleted: true, +// setupUrl: 'https://www.example.com/setup', +// manageUrl: 'https://www.example.com/manage', +// syncStatus: 'failed' as const, +// issueType: 'error' as const, +// issueText: '3 issues to resolve', +// }, +// ], +// }; +// }; + +// TODO: To be removed. This is for testing everything works okay. export const useRegisteredChannels = () => { // TODO: call API here to get data. // The following are just dummy data for testing now. @@ -12,11 +73,11 @@ export const useRegisteredChannels = () => { 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', isSetupCompleted: true, - setupUrl: 'www.google.com/setup', - manageUrl: 'www.google.com/manage', + setupUrl: 'https://www.example.com/setup', + manageUrl: 'https://www.example.com/manage', syncStatus: 'synced' as const, issueType: 'none' as const, - issueText: '', + issueText: 'No issues to resolve', }, ], }; From 6f4e05945bab0cf3b27b0d2eeb4f2ba51f6363a7 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 14 Dec 2022 20:19:20 +0800 Subject: [PATCH 24/84] Add dummy data for testing in useRecommendedChannels. --- .../Channels/useRecommendedChannels.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts index 40a43823b47..79f84e5e25d 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts @@ -8,6 +8,26 @@ type UseRecommendedChannels = { data: Array< RecommendedChannel >; }; +// // TODO: to be removed. This is to test for loading state. +// export const useRecommendedChannels = (): UseRecommendedChannels => { +// // TODO: call API here to get data. +// // The following are just dummy data for testing now. +// return { +// loading: true, +// data: [], +// }; +// }; + +// // TODO: to be removed. This is to test for empty data. +// export const useRecommendedChannels = (): UseRecommendedChannels => { +// TODO: call API here to get data. +// The following are just dummy data for testing now. +// return { +// loading: false, +// data: [], +// }; +// }; + export const useRecommendedChannels = (): UseRecommendedChannels => { // TODO: call API here to get data. // The following are just dummy data for testing now. From 2e8b5227e42c396a8f37df4c4e93babb9862b01b Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 14 Dec 2022 20:54:29 +0800 Subject: [PATCH 25/84] Move RecommendedChannel type. --- .../Channels/CollapsibleRecommendedChannels.tsx | 2 +- .../overview-multichannel/Channels/RecommendedChannelsList.tsx | 2 +- .../overview-multichannel/Channels/useRecommendedChannels.ts | 2 +- .../Channels/types.ts => types/RecommendedChannel.ts} | 0 plugins/woocommerce-admin/client/marketing/types/index.ts | 1 + 5 files changed, 4 insertions(+), 3 deletions(-) rename plugins/woocommerce-admin/client/marketing/{overview-multichannel/Channels/types.ts => types/RecommendedChannel.ts} (100%) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx index 57fc3c688b9..ff32dedde99 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx @@ -9,7 +9,7 @@ import { chevronUp, chevronDown } from '@wordpress/icons'; /** * Internal dependencies */ -import { RecommendedChannel } from './types'; +import { RecommendedChannel } from '~/marketing/types'; import { RecommendedChannelsList } from './RecommendedChannelsList'; import './Channels.scss'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx index 7b61170f74c..d88bef12f29 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx @@ -9,7 +9,7 @@ import { CardDivider } from '@wordpress/components'; * Internal dependencies */ import { SmartPluginCardBody } from '~/marketing/components'; -import { RecommendedChannel } from './types'; +import { RecommendedChannel } from '~/marketing/types'; import './Channels.scss'; type RecommendedChannelListPropsType = { diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts index 79f84e5e25d..88982ec5bed 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { RecommendedChannel } from './types'; +import { RecommendedChannel } from '~/marketing/types'; type UseRecommendedChannels = { loading: boolean; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts b/plugins/woocommerce-admin/client/marketing/types/RecommendedChannel.ts similarity index 100% rename from plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/types.ts rename to plugins/woocommerce-admin/client/marketing/types/RecommendedChannel.ts diff --git a/plugins/woocommerce-admin/client/marketing/types/index.ts b/plugins/woocommerce-admin/client/marketing/types/index.ts index 24fbe739fd0..6a822418804 100644 --- a/plugins/woocommerce-admin/client/marketing/types/index.ts +++ b/plugins/woocommerce-admin/client/marketing/types/index.ts @@ -5,3 +5,4 @@ export { IssueTypeType, InstalledChannel, } from './InstalledChannel'; +export { RecommendedChannel } from './RecommendedChannel'; From 8d8f416fceab7de3f2a15d6dadc5f03e4c26c90d Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 14 Dec 2022 14:33:49 +0000 Subject: [PATCH 26/84] Rename `get_errors_no` to `get_errors_count` --- plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php | 2 +- .../src/Admin/Marketing/MarketingChannelInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php index 9669b9014c6..eecd0debd8d 100644 --- a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php +++ b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php @@ -40,7 +40,7 @@ class InstalledExtensions { 'name' => $channel->get_name(), 'description' => $channel->get_description(), 'product_listings_status' => $channel->get_product_listings_status(), - 'errors_no' => $channel->get_errors_no(), + 'errors_count' => $channel->get_errors_count(), 'icon' => $channel->get_icon_url(), ]; }, diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php index 3a7233f073b..b2dbc3819a2 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -71,7 +71,7 @@ interface MarketingChannelInterface { * * @return int The number of issues to resolve, or 0 if there are no issues with the channel. */ - public function get_errors_no(): int; + public function get_errors_count(): int; /** * Returns an array of the channel's marketing campaigns. From 7734d41887c27dc37eb4607593d450e554da6048 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 14 Dec 2022 14:38:05 +0000 Subject: [PATCH 27/84] Remove the validation for marketing channel slugs Do not check if the marketing channel's slug exists in the list returned by WooCommerce.com Recommendation API. This essentially allows any third-party extension to register as a marketing channel. --- .../src/Admin/Marketing/MarketingChannels.php | 79 ++----------------- .../MarketingServiceProvider.php | 2 +- .../Admin/Marketing/MarketingChannelsTest.php | 79 ++++++++----------- 3 files changed, 37 insertions(+), 123 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index 0fcb6fd1e5e..a5fde99eb32 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; +use Exception; /** * MarketingChannels repository class @@ -20,32 +20,6 @@ class MarketingChannels { */ private $registered_channels = []; - /** - * Array of plugin slugs for allowed marketing channels. - * - * @var string[] - */ - private $allowed_channels; - - /** - * MarketingSpecs repository - * - * @var MarketingSpecs - */ - protected $marketing_specs; - - /** - * Class initialization, invoked by the DI container. - * - * @param MarketingSpecs $marketing_specs The MarketingSpecs class. - * - * @internal - */ - final public function init( MarketingSpecs $marketing_specs ) { - $this->marketing_specs = $marketing_specs; - $this->allowed_channels = $this->get_allowed_channels(); - } - /** * Registers a marketing channel. * @@ -55,14 +29,11 @@ class MarketingChannels { * * @return void * - * @see MarketingChannels::is_channel_allowed() Checks if the marketing channel is allowed to be registered or not. + * @throws Exception If the given marketing channel is already registered. */ public function register( MarketingChannelInterface $channel ): void { - if ( ! $this->is_channel_allowed( $channel ) ) { - // Silently log an error and bail. - wc_get_logger()->error( sprintf( 'Marketing channel %s (%s) cannot be registered!', $channel->get_name(), $channel->get_slug() ) ); - - return; + if ( isset( $this->registered_channels[ $channel->get_slug() ] ) ) { + throw new Exception( 'Marketing channel cannot be registered because there is already a channel registered with the same slug!' ); } $this->registered_channels[ $channel->get_slug() ] = $channel; @@ -86,46 +57,6 @@ class MarketingChannels { */ $channels = apply_filters( 'woocommerce_marketing_channels', $this->registered_channels ); - // Only return allowed channels. - $allowed_channels = array_filter( - $channels, - function ( MarketingChannelInterface $channel ) { - if ( ! $this->is_channel_allowed( $channel ) ) { - // Silently log an error and bail. - wc_get_logger()->error( sprintf( 'Marketing channel %s (%s) cannot be registered!', $channel->get_name(), $channel->get_slug() ) ); - - return false; - } - - return true; - } - ); - - return array_values( $allowed_channels ); - } - - /** - * Returns an array of plugin slugs for the marketing channels that are allowed to be registered. - * - * @return array - */ - protected function get_allowed_channels(): array { - $recommended_channels = $this->marketing_specs->get_recommended_plugins(); - if ( empty( $recommended_channels ) ) { - return []; - } - - return array_column( $recommended_channels, 'product', 'product' ); - } - - /** - * Determines whether the given marketing channel is allowed to be registered. - * - * @param MarketingChannelInterface $channel The marketing channel object. - * - * @return bool - */ - protected function is_channel_allowed( MarketingChannelInterface $channel ): bool { - return isset( $this->allowed_channels[ $channel->get_slug() ] ); + return array_values( $channels ); } } diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php index 8e27386ae86..3fd7677c84c 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php @@ -38,7 +38,7 @@ class MarketingServiceProvider extends AbstractServiceProvider { */ public function register() { $this->share( MarketingSpecs::class ); - $this->share( MarketingChannels::class )->addArgument( MarketingSpecs::class ); + $this->share( MarketingChannels::class ); $this->share( InstalledExtensions::class )->addArgument( MarketingChannels::class ); } } diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php index cf5885a0557..fdc112ae370 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php @@ -4,7 +4,6 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingChannelInterface; use Automattic\WooCommerce\Admin\Marketing\MarketingChannels; -use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; use WC_Unit_Test_Case; /** @@ -16,29 +15,17 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { * Runs before each test. */ public function setUp(): void { - delete_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT ); + remove_all_filters( 'woocommerce_marketing_channels' ); } /** - * @testdox A marketing channel can be registered using the `register` method if it is in the allowed list. + * @testdox A marketing channel can be registered using the `register` method if the same channel slug is NOT previously registered. */ - public function test_registers_allowed_channels() { + public function test_registers_channel() { $test_channel = $this->createMock( MarketingChannelInterface::class ); $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); - $marketing_specs = $this->createMock( MarketingSpecs::class ); - $marketing_specs->expects( $this->once() ) - ->method( 'get_recommended_plugins' ) - ->willReturn( - [ - [ - 'product' => 'test-channel-1', - ], - ] - ); - $marketing_channels = new MarketingChannels(); - $marketing_channels->init( $marketing_specs ); $marketing_channels->register( $test_channel ); $this->assertNotEmpty( $marketing_channels->get_registered_channels() ); @@ -46,42 +33,33 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { } /** - * @testdox A marketing channel can NOT be registered using the `register` method if it is NOT in the allowed list. + * @testdox A marketing channel can NOT be registered using the `register` method if it is previously registered. */ - public function test_does_not_register_disallowed_channels() { - $test_channel = $this->createMock( MarketingChannelInterface::class ); - $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + public function test_throws_exception_if_registering_existing_channels() { + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); - $marketing_specs = $this->createMock( MarketingSpecs::class ); - $marketing_specs->expects( $this->once() )->method( 'get_recommended_plugins' )->willReturn( [] ); + $test_channel_1_duplicate = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1_duplicate->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); $marketing_channels = new MarketingChannels(); - $marketing_channels->init( $marketing_specs ); - $marketing_channels->register( $test_channel ); + $marketing_channels->register( $test_channel_1 ); - $this->assertEmpty( $marketing_channels->get_registered_channels() ); + $this->expectException( \Exception::class ); + $marketing_channels->register( $test_channel_1_duplicate ); + + $this->assertCount( 1, $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel_1, $marketing_channels->get_registered_channels()[0] ); } /** - * @testdox A marketing channel can be registered using the `woocommerce_marketing_channels` WordPress filter if it is in the allowed list. + * @testdox A marketing channel can be registered using the `woocommerce_marketing_channels` WordPress filter if the same channel slug is NOT previously registered. */ - public function test_registers_allowed_channels_using_wp_filter() { + public function test_registers_channel_using_wp_filter() { $test_channel = $this->createMock( MarketingChannelInterface::class ); $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); - $marketing_specs = $this->createMock( MarketingSpecs::class ); - $marketing_specs->expects( $this->once() ) - ->method( 'get_recommended_plugins' ) - ->willReturn( - [ - [ - 'product' => 'test-channel-1', - ], - ] - ); - $marketing_channels = new MarketingChannels(); - $marketing_channels->init( $marketing_specs ); add_filter( 'woocommerce_marketing_channels', @@ -97,24 +75,29 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { } /** - * @testdox A marketing channel can NOT be registered using the `woocommerce_marketing_channels` WordPress filter if it NOT is in the allowed list. + * @testdox A marketing channel can NOT be registered using the `woocommerce_marketing_channels` WordPress filter if it is previously registered. */ - public function test_does_not_register_disallowed_channels_using_wp_filter() { - $test_channel = $this->createMock( MarketingChannelInterface::class ); - $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + public function test_overrides_existing_channel_if_registered_using_wp_filter() { + $marketing_channels = new MarketingChannels(); - set_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, [] ); + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_channels->register( $test_channel_1 ); + + $test_channel_1_duplicate = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1_duplicate->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); add_filter( 'woocommerce_marketing_channels', - function ( array $channels ) use ( $test_channel ) { - $channels[ $test_channel->get_slug() ] = $test_channel; + function ( array $channels ) use ( $test_channel_1_duplicate ) { + $channels[ $test_channel_1_duplicate->get_slug() ] = $test_channel_1_duplicate; return $channels; } ); - $marketing_channels = new MarketingChannels(); - $this->assertEmpty( $marketing_channels->get_registered_channels() ); + $this->assertCount( 1, $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel_1_duplicate, $marketing_channels->get_registered_channels()[0] ); } } From 91e0a0c065102dfd3a2c89412f798688f52c65e9 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 14 Dec 2022 17:37:34 +0000 Subject: [PATCH 28/84] Revert InstalledExtensions The InstalledExtensions class will be used by the previous generation of Marketing dashboard (if the user has not enabled the new "Marketing" feature); therefore, it's best to restore it to the original code. --- .../src/Admin/API/MarketingOverview.php | 9 +- .../Admin/Marketing/InstalledExtensions.php | 617 +++++++++++++++++- .../src/Internal/Admin/Marketing.php | 9 +- .../MarketingServiceProvider.php | 3 - 4 files changed, 586 insertions(+), 52 deletions(-) diff --git a/plugins/woocommerce/src/Admin/API/MarketingOverview.php b/plugins/woocommerce/src/Admin/API/MarketingOverview.php index 883ce04c1bb..930dcb4c0fc 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingOverview.php +++ b/plugins/woocommerce/src/Admin/API/MarketingOverview.php @@ -125,14 +125,7 @@ class MarketingOverview extends \WC_REST_Data_Controller { * @return \WP_Error|\WP_REST_Response */ public function get_installed_plugins( $request ) { - /** - * InstalledExtensions - * - * @var InstalledExtensions $installed_extensions - */ - $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); - - return rest_ensure_response( $installed_extensions->get_data() ); + return rest_ensure_response( InstalledExtensions::get_data() ); } } diff --git a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php index eecd0debd8d..abf2d6fa26f 100644 --- a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php +++ b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php @@ -5,46 +5,597 @@ namespace Automattic\WooCommerce\Admin\Marketing; +use Automattic\WooCommerce\Admin\PluginsHelper; + /** * Installed Marketing Extensions class. */ class InstalledExtensions { - /** - * MarketingChannels repository - * - * @var MarketingChannels - */ - protected $marketing_channels; - - /** - * Class initialization, invoked by the DI container. - * - * @param MarketingChannels $marketing_channels The MarketingChannels repository. - * - * @internal - */ - final public function init( MarketingChannels $marketing_channels ) { - $this->marketing_channels = $marketing_channels; - } /** * Gets an array of plugin data for the "Installed marketing extensions" card. + * + * Valid extensions statuses are: installed, activated, configured */ - public function get_data(): array { - return array_map( - function ( MarketingChannelInterface $channel ) { - return [ - 'slug' => $channel->get_slug(), - 'status' => $channel->is_setup_completed() ? 'configured' : 'activated', - 'settingsUrl' => $channel->get_setup_url(), - 'name' => $channel->get_name(), - 'description' => $channel->get_description(), - 'product_listings_status' => $channel->get_product_listings_status(), - 'errors_count' => $channel->get_errors_count(), - 'icon' => $channel->get_icon_url(), - ]; - }, - $this->marketing_channels->get_registered_channels() - ); + public static function get_data() { + $data = []; + + $automatewoo = self::get_automatewoo_extension_data(); + $aw_referral = self::get_aw_referral_extension_data(); + $aw_birthdays = self::get_aw_birthdays_extension_data(); + $mailchimp = self::get_mailchimp_extension_data(); + $facebook = self::get_facebook_extension_data(); + $pinterest = self::get_pinterest_extension_data(); + $google = self::get_google_extension_data(); + $amazon_ebay = self::get_amazon_ebay_extension_data(); + $mailpoet = self::get_mailpoet_extension_data(); + $creative_mail = self::get_creative_mail_extension_data(); + $tiktok = self::get_tiktok_extension_data(); + $jetpack_crm = self::get_jetpack_crm_extension_data(); + $zapier = self::get_zapier_extension_data(); + $salesforce = self::get_salesforce_extension_data(); + $vimeo = self::get_vimeo_extension_data(); + $trustpilot = self::get_trustpilot_extension_data(); + + if ( $automatewoo ) { + $data[] = $automatewoo; + } + + if ( $aw_referral ) { + $data[] = $aw_referral; + } + + if ( $aw_birthdays ) { + $data[] = $aw_birthdays; + } + + if ( $mailchimp ) { + $data[] = $mailchimp; + } + + if ( $facebook ) { + $data[] = $facebook; + } + + if ( $pinterest ) { + $data[] = $pinterest; + } + + if ( $google ) { + $data[] = $google; + } + + if ( $amazon_ebay ) { + $data[] = $amazon_ebay; + } + + if ( $mailpoet ) { + $data[] = $mailpoet; + } + + if ( $creative_mail ) { + $data[] = $creative_mail; + } + + if ( $tiktok ) { + $data[] = $tiktok; + } + + if ( $jetpack_crm ) { + $data[] = $jetpack_crm; + } + + if ( $zapier ) { + $data[] = $zapier; + } + + if ( $salesforce ) { + $data[] = $salesforce; + } + + if ( $vimeo ) { + $data[] = $vimeo; + } + + if ( $trustpilot ) { + $data[] = $trustpilot; + } + + return $data; } + + /** + * Get allowed plugins. + * + * @return array + */ + public static function get_allowed_plugins() { + return [ + 'automatewoo', + 'mailchimp-for-woocommerce', + 'creative-mail-by-constant-contact', + 'facebook-for-woocommerce', + 'pinterest-for-woocommerce', + 'google-listings-and-ads', + 'hubspot-for-woocommerce', + 'woocommerce-amazon-ebay-integration', + 'mailpoet', + ]; + } + + /** + * Get AutomateWoo extension data. + * + * @return array|bool + */ + protected static function get_automatewoo_extension_data() { + $slug = 'automatewoo'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; + + if ( 'activated' === $data['status'] && function_exists( 'AW' ) ) { + $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings' ); + $data['docsUrl'] = 'https://automatewoo.com/docs/'; + $data['status'] = 'configured'; // Currently no configuration step. + } + + return $data; + } + + /** + * Get AutomateWoo Refer a Friend extension data. + * + * @return array|bool + */ + protected static function get_aw_referral_extension_data() { + $slug = 'automatewoo-referrals'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; + + if ( 'activated' === $data['status'] ) { + $data['docsUrl'] = 'https://automatewoo.com/docs/refer-a-friend/'; + $data['status'] = 'configured'; + if ( function_exists( 'AW_Referrals' ) ) { + $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings&tab=referrals' ); + } + } + + return $data; + } + + /** + * Get AutomateWoo Birthdays extension data. + * + * @return array|bool + */ + protected static function get_aw_birthdays_extension_data() { + $slug = 'automatewoo-birthdays'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; + + if ( 'activated' === $data['status'] ) { + $data['docsUrl'] = 'https://automatewoo.com/docs/getting-started-with-birthdays/'; + $data['status'] = 'configured'; + if ( function_exists( 'AW_Birthdays' ) ) { + $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings&tab=birthdays' ); + } + } + + return $data; + } + + /** + * Get MailChimp extension data. + * + * @return array|bool + */ + protected static function get_mailchimp_extension_data() { + $slug = 'mailchimp-for-woocommerce'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/mailchimp.svg'; + + if ( 'activated' === $data['status'] && function_exists( 'mailchimp_is_configured' ) ) { + $data['docsUrl'] = 'https://mailchimp.com/help/connect-or-disconnect-mailchimp-for-woocommerce/'; + $data['settingsUrl'] = admin_url( 'admin.php?page=mailchimp-woocommerce' ); + + if ( mailchimp_is_configured() ) { + $data['status'] = 'configured'; + } + } + + return $data; + } + + /** + * Get Facebook extension data. + * + * @return array|bool + */ + protected static function get_facebook_extension_data() { + $slug = 'facebook-for-woocommerce'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/facebook-icon.svg'; + + if ( $data['status'] === 'activated' && function_exists( 'facebook_for_woocommerce' ) ) { + $integration = facebook_for_woocommerce()->get_integration(); + + if ( $integration->is_configured() ) { + $data['status'] = 'configured'; + } + + $data['settingsUrl'] = facebook_for_woocommerce()->get_settings_url(); + $data['docsUrl'] = facebook_for_woocommerce()->get_documentation_url(); + } + + return $data; + } + + /** + * Get Pinterest extension data. + * + * @return array|bool + */ + protected static function get_pinterest_extension_data() { + $slug = 'pinterest-for-woocommerce'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/pinterest.svg'; + + $data['docsUrl'] = 'https://woocommerce.com/document/pinterest-for-woocommerce/?utm_medium=product'; + + if ( 'activated' === $data['status'] && class_exists( 'Pinterest_For_Woocommerce' ) ) { + $pinterest_onboarding_completed = Pinterest_For_Woocommerce()::is_setup_complete(); + if ( $pinterest_onboarding_completed ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/pinterest/settings' ); + } else { + $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/pinterest/landing' ); + } + } + + return $data; + } + + /** + * Get Google extension data. + * + * @return array|bool + */ + protected static function get_google_extension_data() { + $slug = 'google-listings-and-ads'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/google.svg'; + + if ( 'activated' === $data['status'] && function_exists( 'woogle_get_container' ) && class_exists( '\Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService' ) ) { + + $merchant_center = woogle_get_container()->get( \Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService::class ); + + if ( $merchant_center->is_setup_complete() ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/google/settings' ); + } else { + $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/google/start' ); + } + + $data['docsUrl'] = 'https://woocommerce.com/document/google-listings-and-ads/?utm_medium=product'; + } + + return $data; + } + + /** + * Get Amazon / Ebay extension data. + * + * @return array|bool + */ + protected static function get_amazon_ebay_extension_data() { + $slug = 'woocommerce-amazon-ebay-integration'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/amazon-ebay.svg'; + + if ( 'activated' === $data['status'] && class_exists( '\CodistoConnect' ) ) { + + $codisto_merchantid = get_option( 'codisto_merchantid' ); + + // Use same check as codisto admin tabs. + if ( is_numeric( $codisto_merchantid ) ) { + $data['status'] = 'configured'; + } + + $data['settingsUrl'] = admin_url( 'admin.php?page=codisto-settings' ); + $data['docsUrl'] = 'https://woocommerce.com/document/multichannel-for-woocommerce-google-amazon-ebay-walmart-integration/?utm_medium=product'; + } + + return $data; + } + + /** + * Get MailPoet extension data. + * + * @return array|bool + */ + protected static function get_mailpoet_extension_data() { + $slug = 'mailpoet'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/mailpoet.svg'; + + if ( 'activated' === $data['status'] && class_exists( '\MailPoet\API\API' ) ) { + $mailpoet_api = \MailPoet\API\API::MP( 'v1' ); + + if ( ! method_exists( $mailpoet_api, 'isSetupComplete' ) || $mailpoet_api->isSetupComplete() ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=mailpoet-settings' ); + } else { + $data['settingsUrl'] = admin_url( 'admin.php?page=mailpoet-newsletters' ); + } + + $data['docsUrl'] = 'https://kb.mailpoet.com/'; + $data['supportUrl'] = 'https://www.mailpoet.com/support/'; + } + + return $data; + } + + /** + * Get Creative Mail for WooCommerce extension data. + * + * @return array|bool + */ + protected static function get_creative_mail_extension_data() { + $slug = 'creative-mail-by-constant-contact'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/creative-mail-by-constant-contact.png'; + + if ( 'activated' === $data['status'] && class_exists( '\CreativeMail\Helpers\OptionsHelper' ) ) { + if ( ! method_exists( '\CreativeMail\Helpers\OptionsHelper', 'get_instance_id' ) || \CreativeMail\Helpers\OptionsHelper::get_instance_id() !== null ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=creativemail_settings' ); + } else { + $data['settingsUrl'] = admin_url( 'admin.php?page=creativemail' ); + } + + $data['docsUrl'] = 'https://app.creativemail.com/kb/help/WooCommerce'; + $data['supportUrl'] = 'https://app.creativemail.com/kb/help/'; + } + + return $data; + } + + /** + * Get TikTok for WooCommerce extension data. + * + * @return array|bool + */ + protected static function get_tiktok_extension_data() { + $slug = 'tiktok-for-business'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/tiktok.jpg'; + + if ( 'activated' === $data['status'] ) { + if ( false !== get_option( 'tt4b_access_token' ) ) { + $data['status'] = 'configured'; + } + + $data['settingsUrl'] = admin_url( 'admin.php?page=tiktok' ); + $data['docsUrl'] = 'https://woocommerce.com/document/tiktok-for-woocommerce/'; + $data['supportUrl'] = 'https://ads.tiktok.com/athena/user-feedback/?identify_key=6a1e079024806640c5e1e695d13db80949525168a052299b4970f9c99cb5ac78'; + } + + return $data; + } + + /** + * Get Jetpack CRM for WooCommerce extension data. + * + * @return array|bool + */ + protected static function get_jetpack_crm_extension_data() { + $slug = 'zero-bs-crm'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/jetpack-crm.png'; + + if ( 'activated' === $data['status'] ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=zerobscrm-plugin-settings' ); + $data['docsUrl'] = 'https://kb.jetpackcrm.com/'; + $data['supportUrl'] = 'https://kb.jetpackcrm.com/crm-support/'; + } + + return $data; + } + + /** + * Get WooCommerce Zapier extension data. + * + * @return array|bool + */ + protected static function get_zapier_extension_data() { + $slug = 'woocommerce-zapier'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/zapier.png'; + + if ( 'activated' === $data['status'] ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=wc-settings&tab=wc_zapier' ); + $data['docsUrl'] = 'https://docs.om4.io/woocommerce-zapier/'; + } + + return $data; + } + + /** + * Get Salesforce extension data. + * + * @return array|bool + */ + protected static function get_salesforce_extension_data() { + $slug = 'integration-with-salesforce'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/salesforce.jpg'; + + if ( 'activated' === $data['status'] && class_exists( '\Integration_With_Salesforce_Admin' ) ) { + if ( ! method_exists( '\Integration_With_Salesforce_Admin', 'get_connection_status' ) || \Integration_With_Salesforce_Admin::get_connection_status() ) { + $data['status'] = 'configured'; + } + + $data['settingsUrl'] = admin_url( 'admin.php?page=integration-with-salesforce' ); + $data['docsUrl'] = 'https://woocommerce.com/document/salesforce-integration/'; + $data['supportUrl'] = 'https://wpswings.com/submit-query/'; + } + + return $data; + } + + /** + * Get Vimeo extension data. + * + * @return array|bool + */ + protected static function get_vimeo_extension_data() { + $slug = 'vimeo'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/vimeo.png'; + + if ( 'activated' === $data['status'] && class_exists( '\Tribe\Vimeo_WP\Vimeo\Vimeo_Auth' ) ) { + if ( method_exists( '\Tribe\Vimeo_WP\Vimeo\Vimeo_Auth', 'has_access_token' ) ) { + $vimeo_auth = new \Tribe\Vimeo_WP\Vimeo\Vimeo_Auth(); + if ( $vimeo_auth->has_access_token() ) { + $data['status'] = 'configured'; + } + } else { + $data['status'] = 'configured'; + } + + $data['settingsUrl'] = admin_url( 'options-general.php?page=vimeo_settings' ); + $data['docsUrl'] = 'https://woocommerce.com/document/vimeo/'; + $data['supportUrl'] = 'https://vimeo.com/help/contact'; + } + + return $data; + } + + /** + * Get Trustpilot extension data. + * + * @return array|bool + */ + protected static function get_trustpilot_extension_data() { + $slug = 'trustpilot-reviews'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/trustpilot.png'; + + if ( 'activated' === $data['status'] ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=woocommerce-trustpilot-settings-page' ); + $data['docsUrl'] = 'https://woocommerce.com/document/trustpilot-reviews/'; + $data['supportUrl'] = 'https://support.trustpilot.com/hc/en-us/requests/new'; + } + + return $data; + } + + + /** + * Get an array of basic data for a given extension. + * + * @param string $slug Plugin slug. + * + * @return array|false + */ + protected static function get_extension_base_data( $slug ) { + $status = PluginsHelper::is_plugin_active( $slug ) ? 'activated' : 'installed'; + $plugin_data = PluginsHelper::get_plugin_data( $slug ); + + if ( ! $plugin_data ) { + return false; + } + + return [ + 'slug' => $slug, + 'status' => $status, + 'name' => $plugin_data['Name'], + 'description' => html_entity_decode( wp_trim_words( $plugin_data['Description'], 20 ) ), + 'supportUrl' => 'https://woocommerce.com/my-account/create-a-ticket/?utm_medium=product', + ]; + } + } diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing.php b/plugins/woocommerce/src/Internal/Admin/Marketing.php index f11cd2d31d9..47d4ab241bb 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing.php @@ -165,14 +165,7 @@ class Marketing { return $settings; } - /** - * InstalledExtensions helper class. - * - * @var InstalledExtensions $installed_extensions - */ - $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); - - $settings['marketing']['installedExtensions'] = $installed_extensions->get_data(); + $settings['marketing']['installedExtensions'] = InstalledExtensions::get_data(); return $settings; } diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php index 3fd7677c84c..c00e484fe89 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php @@ -5,7 +5,6 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; -use Automattic\WooCommerce\Admin\Marketing\InstalledExtensions; use Automattic\WooCommerce\Admin\Marketing\MarketingChannels; use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; @@ -30,7 +29,6 @@ class MarketingServiceProvider extends AbstractServiceProvider { protected $provides = array( MarketingSpecs::class, MarketingChannels::class, - InstalledExtensions::class, ); /** @@ -39,6 +37,5 @@ class MarketingServiceProvider extends AbstractServiceProvider { public function register() { $this->share( MarketingSpecs::class ); $this->share( MarketingChannels::class ); - $this->share( InstalledExtensions::class )->addArgument( MarketingChannels::class ); } } From 53dac1d8e37a1be878a66bdf14f48910c2dd2f0c Mon Sep 17 00:00:00 2001 From: Nima Date: Thu, 15 Dec 2022 15:10:25 +0000 Subject: [PATCH 29/84] Fix code style --- plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php index abf2d6fa26f..b1e9ad82b80 100644 --- a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php +++ b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php @@ -241,7 +241,7 @@ class InstalledExtensions { $data = self::get_extension_base_data( $slug ); $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/facebook-icon.svg'; - if ( $data['status'] === 'activated' && function_exists( 'facebook_for_woocommerce' ) ) { + if ( 'activated' === $data['status'] && function_exists( 'facebook_for_woocommerce' ) ) { $integration = facebook_for_woocommerce()->get_integration(); if ( $integration->is_configured() ) { From 4fa4f802e947ba2502a857514ad7eefb82d8adc7 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 21:19:00 +0800 Subject: [PATCH 30/84] Check for empty recommended channels. --- .../overview-multichannel/Channels/Channels.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 5511a5c0957..38ecb794cd9 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -47,10 +47,18 @@ export const Channels = () => { /* * If users have no registered channels, - * we display recommended channels without collapsible list + * we should display recommended channels without collapsible list * and with a description in the card header. */ - if ( registeredChannels.length === 0 && recommendedChannels.length > 0 ) { + if ( registeredChannels.length === 0 ) { + /** + * If for some reasons we don't have recommended channels, + * then we should not show the Channels card at all. + */ + if ( recommendedChannels.length === 0 ) { + return null; + } + return ( From 06a303fc33da0a9691fa47168faa1418f199f661 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 21:21:52 +0800 Subject: [PATCH 31/84] Types for useRegisteredChannels. --- .../Channels/useRegisteredChannels.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts index d02667433e9..d512f1d4d32 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts @@ -1,3 +1,13 @@ +/** + * Internal dependencies + */ +import { InstalledChannel } from '~/marketing/types'; + +type UseRegisteredChannels = { + loading: boolean; + data: Array< InstalledChannel >; +}; + // // TODO: To be removed. This is for testing loading state. // export const useRegisteredChannels = () => { // // TODO: call API here to get data. @@ -17,7 +27,6 @@ // data: [ // { // slug: 'google-listings-and-ads', -// name: 'Google Listings and Ads', // title: 'Google Listings and Ads', // description: // 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', @@ -42,7 +51,6 @@ // data: [ // { // slug: 'google-listings-and-ads', -// name: 'Google Listings and Ads', // title: 'Google Listings and Ads', // description: // 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', @@ -59,7 +67,7 @@ // }; // TODO: To be removed. This is for testing everything works okay. -export const useRegisteredChannels = () => { +export const useRegisteredChannels = (): UseRegisteredChannels => { // TODO: call API here to get data. // The following are just dummy data for testing now. return { @@ -67,7 +75,6 @@ export const useRegisteredChannels = () => { data: [ { slug: 'google-listings-and-ads', - name: 'Google Listings and Ads', title: 'Google Listings and Ads', description: 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', From 31b635b88865a3f5cb5410ee63591765f86d262b Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 21:53:42 +0800 Subject: [PATCH 32/84] Code refactor: move CSS code to corresponding SCSS file. --- .../overview-multichannel/Channels/Channels.scss | 4 ---- .../Channels/CollapsibleRecommendedChannels.scss | 5 +++++ .../Channels/CollapsibleRecommendedChannels.tsx | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss index a9d5343d54a..aff331de94f 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss @@ -4,8 +4,4 @@ align-items: flex-start; gap: $gap-smallest; } - - .components-button.is-link { - text-decoration: none; - } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss new file mode 100644 index 00000000000..eea3ae685b9 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss @@ -0,0 +1,5 @@ +.woocommerce-marketing-recommended-channels { + .components-button.is-link { + text-decoration: none; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx index ff32dedde99..48319ad41ef 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { Fragment, useState } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { CardBody, CardDivider, Button, Icon } from '@wordpress/components'; import { chevronUp, chevronDown } from '@wordpress/icons'; @@ -11,7 +11,7 @@ import { chevronUp, chevronDown } from '@wordpress/icons'; */ import { RecommendedChannel } from '~/marketing/types'; import { RecommendedChannelsList } from './RecommendedChannelsList'; -import './Channels.scss'; +import './CollapsibleRecommendedChannels.scss'; type RecommendedChannelsType = { recommendedChannels: Array< RecommendedChannel >; @@ -23,7 +23,7 @@ export const CollapsibleRecommendedChannels: React.FC< const [ collapsed, setCollapsed ] = useState( true ); return ( - <> +
); }; From af2f4eb904eda7ff31d3c21663b5851cb6a878df Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 22:08:50 +0800 Subject: [PATCH 33/84] CSS for "Add channels" button. --- .../Channels/CollapsibleRecommendedChannels.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss index eea3ae685b9..9a02c3861dc 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss @@ -1,5 +1,8 @@ .woocommerce-marketing-recommended-channels { .components-button.is-link { + font-size: 14px; + font-weight: 600; + line-height: 17px; text-decoration: none; } } From 9e0b71ff1ccfc8a63bdff130e8a3b69aaa98dde1 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 23 Dec 2022 22:12:16 +0800 Subject: [PATCH 34/84] Rename CollapsibleRecommendedChannels to RecommendedChannels. --- .../marketing/overview-multichannel/Channels/Channels.tsx | 4 ++-- ...eRecommendedChannels.scss => RecommendedChannels.scss} | 0 ...bleRecommendedChannels.tsx => RecommendedChannels.tsx} | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) rename plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/{CollapsibleRecommendedChannels.scss => RecommendedChannels.scss} (100%) rename plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/{CollapsibleRecommendedChannels.tsx => RecommendedChannels.tsx} (86%) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 38ecb794cd9..fcae43a11dd 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -16,7 +16,7 @@ import { import { useChannels } from './useChannels'; import './Channels.scss'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; -import { CollapsibleRecommendedChannels } from './CollapsibleRecommendedChannels'; +import { RecommendedChannels } from './RecommendedChannels'; import { RecommendedChannelsList } from './RecommendedChannelsList'; export const Channels = () => { @@ -107,7 +107,7 @@ export const Channels = () => { { /* Recommended channels section. */ } { recommendedChannels.length > 0 && ( - ) } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss similarity index 100% rename from plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.scss rename to plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx similarity index 86% rename from plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx rename to plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx index 48319ad41ef..94bdd0bf557 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/CollapsibleRecommendedChannels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx @@ -11,15 +11,15 @@ import { chevronUp, chevronDown } from '@wordpress/icons'; */ import { RecommendedChannel } from '~/marketing/types'; import { RecommendedChannelsList } from './RecommendedChannelsList'; -import './CollapsibleRecommendedChannels.scss'; +import './RecommendedChannels.scss'; type RecommendedChannelsType = { recommendedChannels: Array< RecommendedChannel >; }; -export const CollapsibleRecommendedChannels: React.FC< - RecommendedChannelsType -> = ( { recommendedChannels } ) => { +export const RecommendedChannels: React.FC< RecommendedChannelsType > = ( { + recommendedChannels, +} ) => { const [ collapsed, setCollapsed ] = useState( true ); return ( From 7bd32ba638b5aeaeb9caa4078350d682af781692 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 24 Dec 2022 00:32:23 +0800 Subject: [PATCH 35/84] Remove useChannels and use useRegisteredChannels and useRecommendedChannels directly. --- .../Channels/Channels.tsx | 33 +++++++++---------- .../Channels/useChannels.ts | 20 ----------- 2 files changed, 15 insertions(+), 38 deletions(-) delete mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index fcae43a11dd..c485bef0bce 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -13,24 +13,25 @@ import { CardHeaderDescription, CenteredSpinner, } from '~/marketing/components'; -import { useChannels } from './useChannels'; -import './Channels.scss'; +import { useRegisteredChannels } from './useRegisteredChannels'; +import { useRecommendedChannels } from './useRecommendedChannels'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; import { RecommendedChannels } from './RecommendedChannels'; import { RecommendedChannelsList } from './RecommendedChannelsList'; +import './Channels.scss'; export const Channels = () => { - const { - loading, - data: { registeredChannels, recommendedChannels }, - } = useChannels(); + const { loading: loadingRegistered, data: dataRegistered } = + useRegisteredChannels(); + const { loading: loadingRecommended, data: dataRecommended } = + useRecommendedChannels(); /** * TODO: we may need to filter the channels against * `@woocommerce/data` installed plugins. */ - if ( loading ) { + if ( loadingRegistered || loadingRecommended ) { return ( @@ -50,12 +51,12 @@ export const Channels = () => { * we should display recommended channels without collapsible list * and with a description in the card header. */ - if ( registeredChannels.length === 0 ) { + if ( dataRegistered.length === 0 ) { /** * If for some reasons we don't have recommended channels, * then we should not show the Channels card at all. */ - if ( recommendedChannels.length === 0 ) { + if ( dataRecommended.length === 0 ) { return null; } @@ -73,7 +74,7 @@ export const Channels = () => { ); @@ -94,22 +95,18 @@ export const Channels = () => {
{ /* Registered channels section. */ } - { registeredChannels.map( ( el, idx ) => { + { dataRegistered.map( ( el, idx ) => { return ( - { idx < registeredChannels.length - 1 && ( - - ) } + { idx < dataRegistered.length - 1 && } ); } ) } { /* Recommended channels section. */ } - { recommendedChannels.length > 0 && ( - + { dataRecommended.length > 0 && ( + ) }
); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts deleted file mode 100644 index 122b466ddf7..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useChannels.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Internal dependencies - */ -import { useRecommendedChannels } from './useRecommendedChannels'; -import { useRegisteredChannels } from './useRegisteredChannels'; - -export const useChannels = () => { - const { loading: loadingRegistered, data: dataRegistered } = - useRegisteredChannels(); - const { loading: loadingRecommended, data: dataRecommended } = - useRecommendedChannels(); - - return { - loading: loadingRegistered || loadingRecommended, - data: { - registeredChannels: dataRegistered, - recommendedChannels: dataRecommended, - }, - }; -}; From e950417542514b85325338552d8023c5756862fe Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 24 Dec 2022 00:48:03 +0800 Subject: [PATCH 36/84] Move useRegisteredChannels and useRecommendedChannels into shared hooks directory. --- plugins/woocommerce-admin/client/marketing/hooks/index.ts | 2 ++ .../Channels => hooks}/useRecommendedChannels.ts | 0 .../Channels => hooks}/useRegisteredChannels.ts | 0 .../marketing/overview-multichannel/Channels/Channels.tsx | 6 ++++-- 4 files changed, 6 insertions(+), 2 deletions(-) rename plugins/woocommerce-admin/client/marketing/{overview-multichannel/Channels => hooks}/useRecommendedChannels.ts (100%) rename plugins/woocommerce-admin/client/marketing/{overview-multichannel/Channels => hooks}/useRegisteredChannels.ts (100%) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/index.ts b/plugins/woocommerce-admin/client/marketing/hooks/index.ts index a504ae73f17..0a80d7f7f22 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/index.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/index.ts @@ -1 +1,3 @@ export { useInstalledPlugins } from './useInstalledPlugins'; +export { useRegisteredChannels } from './useRegisteredChannels'; +export { useRecommendedChannels } from './useRecommendedChannels'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts similarity index 100% rename from plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRecommendedChannels.ts rename to plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts similarity index 100% rename from plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/useRegisteredChannels.ts rename to plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index c485bef0bce..32b5d3a2879 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -13,8 +13,10 @@ import { CardHeaderDescription, CenteredSpinner, } from '~/marketing/components'; -import { useRegisteredChannels } from './useRegisteredChannels'; -import { useRecommendedChannels } from './useRecommendedChannels'; +import { + useRegisteredChannels, + useRecommendedChannels, +} from '~/marketing/hooks'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; import { RecommendedChannels } from './RecommendedChannels'; import { RecommendedChannelsList } from './RecommendedChannelsList'; From 52dd8845cc6e4ce66d2e408e290085173b20f34a Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 24 Dec 2022 01:14:02 +0800 Subject: [PATCH 37/84] Load registered and recommended channels in MarketingOverviewMultichannel. The data will be used to conditionally display Campaigns card later. --- .../Channels/Channels.tsx | 70 ++++++------------- .../MarketingOverviewMultichannel.tsx | 20 +++++- 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 32b5d3a2879..0f3a07e140a 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -3,65 +3,33 @@ */ import { Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Card, CardHeader, CardBody, CardDivider } from '@wordpress/components'; +import { Card, CardHeader, CardDivider } from '@wordpress/components'; /** * Internal dependencies */ -import { - CardHeaderTitle, - CardHeaderDescription, - CenteredSpinner, -} from '~/marketing/components'; -import { - useRegisteredChannels, - useRecommendedChannels, -} from '~/marketing/hooks'; +import { CardHeaderTitle, CardHeaderDescription } from '~/marketing/components'; +import { InstalledChannel, RecommendedChannel } from '~/marketing/types'; import { InstalledChannelCardBody } from './InstalledChannelCardBody'; import { RecommendedChannels } from './RecommendedChannels'; import { RecommendedChannelsList } from './RecommendedChannelsList'; import './Channels.scss'; -export const Channels = () => { - const { loading: loadingRegistered, data: dataRegistered } = - useRegisteredChannels(); - const { loading: loadingRecommended, data: dataRecommended } = - useRecommendedChannels(); - - /** - * TODO: we may need to filter the channels against - * `@woocommerce/data` installed plugins. - */ - - if ( loadingRegistered || loadingRecommended ) { - return ( - - - - { __( 'Channels', 'woocommerce' ) } - - - - - - - ); - } +type ChannelsProps = { + registeredChannels: Array< InstalledChannel >; + recommendedChannels: Array< RecommendedChannel >; +}; +export const Channels: React.FC< ChannelsProps > = ( { + registeredChannels, + recommendedChannels, +} ) => { /* * If users have no registered channels, * we should display recommended channels without collapsible list * and with a description in the card header. */ - if ( dataRegistered.length === 0 ) { - /** - * If for some reasons we don't have recommended channels, - * then we should not show the Channels card at all. - */ - if ( dataRecommended.length === 0 ) { - return null; - } - + if ( registeredChannels.length === 0 ) { return ( @@ -76,7 +44,7 @@ export const Channels = () => { ); @@ -97,18 +65,22 @@ export const Channels = () => { { /* Registered channels section. */ } - { dataRegistered.map( ( el, idx ) => { + { registeredChannels.map( ( el, idx ) => { return ( - { idx < dataRegistered.length - 1 && } + { idx < registeredChannels.length - 1 && ( + + ) } ); } ) } { /* Recommended channels section. */ } - { dataRecommended.length > 0 && ( - + { recommendedChannels.length >= 1 && ( + ) } ); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index cffa5655873..7cbad05d6a2 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -12,18 +12,36 @@ import { InstalledExtensions } from './InstalledExtensions'; import { DiscoverTools } from './DiscoverTools'; import { LearnMarketing } from './LearnMarketing'; import '~/marketing/data'; +import { + useRegisteredChannels, + useRecommendedChannels, +} from '~/marketing/hooks'; import './MarketingOverviewMultichannel.scss'; +import { CenteredSpinner } from '../components'; export const MarketingOverviewMultichannel: React.FC = () => { + const { loading: loadingRegistered, data: dataRegistered } = + useRegisteredChannels(); + const { loading: loadingRecommended, data: dataRecommended } = + useRecommendedChannels(); const { currentUserCan } = useUser(); const shouldShowExtensions = getAdminSetting( 'allowMarketplaceSuggestions', false ) && currentUserCan( 'install_plugins' ); + if ( loadingRegistered || loadingRecommended ) { + return ; + } + return (
- + { ( dataRegistered.length >= 1 || dataRecommended.length >= 1 ) && ( + + ) } { shouldShowExtensions && } From 6415f3f9111cf4bf115f684181d0140cd86967f2 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:23:26 +0000 Subject: [PATCH 38/84] Add channel property to MarketingCampaign --- .../src/Admin/Marketing/MarketingCampaign.php | 29 +++++++++++++++---- .../Admin/Marketing/MarketingCampaignTest.php | 16 ++++++++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php index 7b3f99a4b3a..45037fea367 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php @@ -22,6 +22,13 @@ class MarketingCampaign implements JsonSerializable { */ protected $id; + /** + * The marketing channel that this campaign belongs to. + * + * @var MarketingChannelInterface + */ + protected $channel; + /** * Title of the marketing campaign. * @@ -46,13 +53,15 @@ class MarketingCampaign implements JsonSerializable { /** * MarketingCampaign constructor. * - * @param string $id The marketing campaign's unique identifier. - * @param string $title The title of the marketing campaign. - * @param string $manage_url The URL to the channel's campaign management page. - * @param Price|null $cost The cost of the marketing campaign with the currency. + * @param string $id The marketing campaign's unique identifier. + * @param MarketingChannelInterface $channel The marketing channel that this campaign belongs to. + * @param string $title The title of the marketing campaign. + * @param string $manage_url The URL to the channel's campaign management page. + * @param Price|null $cost The cost of the marketing campaign with the currency. */ - public function __construct( string $id, string $title, string $manage_url, Price $cost = null ) { + public function __construct( string $id, MarketingChannelInterface $channel, string $title, string $manage_url, Price $cost = null ) { $this->id = $id; + $this->channel = $channel; $this->title = $title; $this->manage_url = $manage_url; $this->cost = $cost; @@ -67,6 +76,15 @@ class MarketingCampaign implements JsonSerializable { return $this->id; } + /** + * Returns the marketing channel that this campaign belongs to. + * + * @return MarketingChannelInterface + */ + public function get_channel(): MarketingChannelInterface { + return $this->channel; + } + /** * Returns the title of the marketing campaign. * @@ -102,6 +120,7 @@ class MarketingCampaign implements JsonSerializable { public function jsonSerialize() { return [ 'id' => $this->get_id(), + 'channel' => $this->get_channel()->get_slug(), 'title' => $this->get_title(), 'manage_url' => $this->get_manage_url(), 'cost' => $this->get_cost(), diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php index 401e1630294..30d8c243ba5 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php @@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingCampaign; +use Automattic\WooCommerce\Admin\Marketing\MarketingChannelInterface; use Automattic\WooCommerce\Admin\Marketing\Price; use WC_Unit_Test_Case; @@ -15,7 +16,10 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @testdox `get_id`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. */ public function test_get_methods_return_properties() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); $this->assertEquals( '1234', $marketing_campaign->get_id() ); $this->assertEquals( 'Ad #1234', $marketing_campaign->get_title() ); @@ -29,7 +33,9 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @testdox `cost` property can be null. */ public function test_cost_can_be_null() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns' ); + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, 'Ad #1234', 'https://example.com/manage-campaigns' ); $this->assertNull( $marketing_campaign->get_cost() ); } @@ -38,13 +44,17 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @testdox It can be serialized to JSON including all its properties. */ public function test_can_be_serialized_to_json() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); $json = wp_json_encode( $marketing_campaign ); $this->assertNotEmpty( $json ); $this->assertEqualSets( [ 'id' => $marketing_campaign->get_id(), + 'channel' => 'test-channel-1', 'title' => $marketing_campaign->get_title(), 'manage_url' => $marketing_campaign->get_manage_url(), 'cost' => [ From 7dcdbd871e68cedd24b2c82b50199e64ee0d454d Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:23:42 +0000 Subject: [PATCH 39/84] Add methods to filter the recommended marketing channels and extensions --- .../Admin/Marketing/MarketingSpecs.php | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php index ee36ba1375c..e6ed4e875d9 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php @@ -28,6 +28,20 @@ class MarketingSpecs { */ const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base'; + /** + * Slug of the category specifying marketing extensions on the WooCommerce.com store. + * + * @var string + */ + const MARKETING_EXTENSION_CATEGORY_SLUG = 'marketing'; + + /** + * Slug of the subcategory specifying marketing channels on the WooCommerce.com store. + * + * @var string + */ + const MARKETING_CHANNEL_SUBCATEGORY_SLUG = 'sales-channels'; + /** * Load recommended plugins from WooCommerce.com * @@ -61,6 +75,64 @@ class MarketingSpecs { return array_values( $plugins ); } + /** + * Return only the recommended marketing channels from WooCommerce.com. + * + * @return array + */ + public function get_recommended_marketing_channels(): array { + return array_filter( $this->get_recommended_plugins(), [ $this, 'is_marketing_channel_plugin' ] ); + } + + /** + * Return all recommended marketing extensions EXCEPT the marketing channels from WooCommerce.com. + * + * @return array + */ + public function get_recommended_marketing_extensions_excluding_channels(): array { + return array_filter( + $this->get_recommended_plugins(), + function ( array $plugin_data ) { + return $this->is_marketing_plugin( $plugin_data ) && ! $this->is_marketing_channel_plugin( $plugin_data ); + } + ); + } + + /** + * Returns whether a plugin is a marketing extension. + * + * @param array $plugin_data The plugin properties returned by the API. + * + * @return bool + */ + protected function is_marketing_plugin( array $plugin_data ): bool { + $categories = $plugin_data['categories'] ?? []; + + return in_array( self::MARKETING_EXTENSION_CATEGORY_SLUG, $categories, true ); + } + + /** + * Returns whether a plugin is a marketing channel. + * + * @param array $plugin_data The plugin properties returned by the API. + * + * @return bool + */ + protected function is_marketing_channel_plugin( array $plugin_data ): bool { + if ( ! $this->is_marketing_plugin( $plugin_data ) ) { + return false; + } + + $subcategories = $plugin_data['subcategories'] ?? []; + foreach ( $subcategories as $subcategory ) { + if ( isset( $subcategory['slug'] ) && self::MARKETING_CHANNEL_SUBCATEGORY_SLUG === $subcategory['slug'] ) { + return true; + } + } + + return false; + } + /** * Load knowledge base posts from WooCommerce.com * From 677416fcf2066b7913ce58521a89ca6eceadca58 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:25:51 +0000 Subject: [PATCH 40/84] Add `marketing/recommendations` API --- plugins/woocommerce/src/Admin/API/Init.php | 1 + .../Admin/API/MarketingRecommendations.php | 235 ++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingRecommendations.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index 42b2bac82e7..45ef28bd4b9 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -64,6 +64,7 @@ class Init { 'Automattic\WooCommerce\Admin\API\Experiments', 'Automattic\WooCommerce\Admin\API\Marketing', 'Automattic\WooCommerce\Admin\API\MarketingOverview', + 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php new file mode 100644 index 00000000000..a64fcce02d7 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php @@ -0,0 +1,235 @@ +namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_items' ], + 'permission_callback' => [ $this, 'get_items_permissions_check' ], + 'args' => [ + 'category' => [ + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'sanitize_title_with_dashes', + 'enum' => [ 'channels', 'extensions' ], + 'required' => true, + ], + ], + ], + 'schema' => [ $this, 'get_public_item_schema' ], + ] + ); + } + + /** + * Check whether a given request has permission to view marketing recommendations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + return true; + } + + /** + * Retrieves a collection of recommendations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + /** + * MarketingSpecs class. + * + * @var MarketingSpecs $marketing_specs + */ + $marketing_specs = wc_get_container()->get( MarketingSpecs::class ); + + $category = $request->get_param( 'category' ); + if ( 'channels' === $category ) { + $items = $marketing_specs->get_recommended_marketing_channels(); + } elseif ( 'extensions' === $category ) { + $items = $marketing_specs->get_recommended_marketing_extensions_excluding_channels(); + } else { + return new WP_Error( 'woocommerce_rest_invalid_category', __( 'The specified category for recommendations is invalid. Allowed values: "channels", "extensions".', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $responses = []; + foreach ( $items as $item ) { + $response = $this->prepare_item_for_response( $item, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param array $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_recommendation', + 'type' => 'object', + 'properties' => [ + 'title' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'url' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'direct_install' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'product' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'plugin' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'categories' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'string', + ], + ], + 'subcategories' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + 'tags' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } +} From 086ce8f48a704740a71bea3ef3dc6fbf976f56c1 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:26:04 +0000 Subject: [PATCH 41/84] Add unit tests for `marketing/recommendations` API --- .../API/MarketingRecommendationsTest.php | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php new file mode 100644 index 00000000000..46335da52e3 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php @@ -0,0 +1,138 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + set_transient( + MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, + [ + [ + 'title' => 'Example Marketing Channel', + 'description' => 'List your products and create ads, etc.', + 'url' => 'https://woocommerce.com/products/example-channel', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example.svg', + 'product' => 'example-channel', + 'plugin' => 'example-channel/example-channel.php', + 'categories' => [ MarketingSpecs::MARKETING_EXTENSION_CATEGORY_SLUG ], + 'subcategories' => [ + [ + 'slug' => MarketingSpecs::MARKETING_CHANNEL_SUBCATEGORY_SLUG, + 'name' => 'Sales channels', + ], + ], + 'tags' => [], + ], + [ + 'title' => 'Example Marketing Extension', + 'description' => 'Automate your customer communications, etc.', + 'url' => 'https://woocommerce.com/products/example-marketing-extension', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example-marketing-extension.svg', + 'product' => 'example-marketing-extension', + 'plugin' => 'example-marketing-extension/example-marketing-extension.php', + 'categories' => [ MarketingSpecs::MARKETING_EXTENSION_CATEGORY_SLUG ], + 'subcategories' => [ + [ + 'slug' => 'email', + 'name' => 'Email', + ], + ], + 'tags' => [], + ], + [ + 'title' => 'Example NON Marketing Extension', + 'description' => 'Handle coupons, etc.', + 'url' => 'https://woocommerce.com/products/example-random-extension', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example-random-extension.svg', + 'product' => 'example-random-extension', + 'plugin' => 'example-random-extension/example-random-extension.php', + 'categories' => [ 'coupons' ], + 'subcategories' => [], + 'tags' => [], + ], + ] + ); + } + + /** + * Tests that the marketing channel recommendations are returned by the endpoint. + */ + public function test_returns_recommended_marketing_channels() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'channels' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'Example Marketing Channel', $data[0]['title'] ); + $this->assertEquals( 'example-channel', $data[0]['product'] ); + } + + /** + * Tests that the marketing extension recommendations are returned by the endpoint. + */ + public function test_returns_recommended_marketing_extensions() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'extensions' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'Example Marketing Extension', $data[0]['title'] ); + $this->assertEquals( 'example-marketing-extension', $data[0]['product'] ); + } + + /** + * Tests that the endpoint returns an error if the provided category is invalid. + */ + public function test_returns_error_if_invalid_category_provided() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'test-non-existing-invalid-category' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'rest_invalid_param', $data['code'] ); + } + +} From a1468ec73927b16cc54c567376dcf56fe9377ca2 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:26:40 +0000 Subject: [PATCH 42/84] Add `marketing/channels` API --- plugins/woocommerce/src/Admin/API/Init.php | 1 + .../src/Admin/API/MarketingChannels.php | 192 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingChannels.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index 45ef28bd4b9..bf2232185ca 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -65,6 +65,7 @@ class Init { 'Automattic\WooCommerce\Admin\API\Marketing', 'Automattic\WooCommerce\Admin\API\MarketingOverview', 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', + 'Automattic\WooCommerce\Admin\API\MarketingChannels', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingChannels.php b/plugins/woocommerce/src/Admin/API/MarketingChannels.php new file mode 100644 index 00000000000..a02039339f1 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingChannels.php @@ -0,0 +1,192 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view marketing channels. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + return true; + } + + /** + * Return installed marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + $channels = $marketing_channels_service->get_registered_channels(); + + $responses = []; + foreach ( $channels as $item ) { + $response = $this->prepare_item_for_response( $item, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingChannelInterface $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'slug' => $item->get_slug(), + 'is_setup_completed' => $item->is_setup_completed(), + 'settings_url' => $item->get_setup_url(), + 'name' => $item->get_name(), + 'description' => $item->get_description(), + 'product_listings_status' => $item->get_product_listings_status(), + 'errors_count' => $item->get_errors_count(), + 'icon' => $item->get_icon_url(), + ]; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_channel', + 'type' => 'object', + 'properties' => [ + 'slug' => [ + 'description' => __( 'Unique identifier string for the marketing channel extension, also known as the plugin slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'Name of the marketing channel.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'description' => __( 'Description of the marketing channel.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon' => [ + 'description' => __( 'Path to the channel icon.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'is_setup_completed' => [ + 'type' => 'boolean', + 'description' => __( 'Whether or not the marketing channel is set up.', 'woocommerce' ), + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'settings_url' => [ + 'description' => __( 'URL to the settings page, or the link to complete the setup/onboarding if the channel has not been set up yet.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'product_listings_status' => [ + 'description' => __( 'Status of the marketing channel\'s product listings.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'errors_count' => [ + 'description' => __( 'Number of channel issues/errors (e.g. account-related errors, product synchronization issues, etc.).', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } +} From 200156d7359960476e0df0e3477bae95f1439092 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:26:52 +0000 Subject: [PATCH 43/84] Add unit tests for `marketing/channels` API --- .../src/Admin/API/MarketingChannelsTest.php | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php new file mode 100644 index 00000000000..a043e77c753 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php @@ -0,0 +1,69 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Tests that the registered marketing channels are returned by the endpoint. + */ + public function test_returns_registered_marketing_channels() { + // Register marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + $test_channel_1->expects( $this->any() )->method( 'get_name' )->willReturn( 'Test Channel One' ); + $this->marketing_channels_service->register( $test_channel_1 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'test-channel-1', $data[0]['slug'] ); + $this->assertEquals( 'Test Channel One', $data[0]['name'] ); + } + +} From 817ca2a96fbb902ed9612c083e80201c9be51eb0 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:27:10 +0000 Subject: [PATCH 44/84] Add `marketing/campaigns` API --- plugins/woocommerce/src/Admin/API/Init.php | 1 + .../src/Admin/API/MarketingCampaigns.php | 237 ++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingCampaigns.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index bf2232185ca..8fff86a1ee1 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -66,6 +66,7 @@ class Init { 'Automattic\WooCommerce\Admin\API\MarketingOverview', 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', 'Automattic\WooCommerce\Admin\API\MarketingChannels', + 'Automattic\WooCommerce\Admin\API\MarketingCampaigns', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php new file mode 100644 index 00000000000..9c479d5e705 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -0,0 +1,237 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view marketing campaigns. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing campaigns.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + return true; + } + + + /** + * Returns an aggregated array of marketing campaigns for all active marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + // Aggregate the campaigns from all registered marketing channels. + $responses = []; + foreach ( $marketing_channels_service->get_registered_channels() as $channel ) { + foreach ( $channel->get_campaigns() as $campaign ) { + $response = $this->prepare_item_for_response( $campaign, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + } + + // Pagination. + $page = $request['page']; + $items_per_page = $request['per_page']; + $offset = ( $page - 1 ) * $items_per_page; + $paginated_results = array_slice( $responses, $offset, $items_per_page ); + + $response = rest_ensure_response( $paginated_results ); + + $total_campaigns = count( $responses ); + $max_pages = ceil( $total_campaigns / $items_per_page ); + $response->header( 'X-WP-Total', $total_campaigns ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + // Add previous and next page links to response header. + $request_params = $request->get_query_params(); + $base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingCampaign $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'id' => $item->get_id(), + 'channel' => $item->get_channel()->get_slug(), + 'title' => $item->get_title(), + 'manage_url' => $item->get_manage_url(), + ]; + + if ( $item->get_cost() instanceof Price ) { + $data['cost'] = [ + 'value' => wc_format_decimal( $item->get_cost()->get_value() ), + 'currency' => $item->get_cost()->get_currency(), + ]; + } + + $context = $request['context'] ?? 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_campaign', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'The unique identifier for the marketing campaign.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'channel' => [ + 'description' => __( 'The unique identifier for the marketing channel that this campaign belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'title' => [ + 'description' => __( 'Title of the marketing campaign.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'manage_url' => [ + 'description' => __( 'URL to the campaign management page', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'cost' => [ + 'description' => __( 'Cost of the marketing campaign.', 'woocommerce' ), + 'context' => [ 'view' ], + 'readonly' => true, + 'type' => 'object', + 'properties' => [ + 'value' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'currency' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Retrieves the query params for the collections. + * + * @return array Query parameters for the collection. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + unset( $params['search'] ); + + return $params; + } + + +} From e82feb21da36988562c67b54e85a8c099e19c69b Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:27:19 +0000 Subject: [PATCH 45/84] Add unit tests for `marketing/campaigns` API --- .../src/Admin/API/MarketingCampaignsTest.php | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php new file mode 100644 index 00000000000..27b94bc4239 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php @@ -0,0 +1,140 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. + */ + public function test_returns_aggregated_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Create a mock marketing campaign. + $test_campaign_1 = $this->createMock( MarketingCampaign::class ); + $test_campaign_1->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-1' ); + $test_campaign_1->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_1 ); + // Return the sample campaign by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_campaigns' )->willReturn( [ $test_campaign_1 ] ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + // Create a second mock marketing channel. + $test_channel_2 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_2->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-2' ); + // Create a mock marketing campaign for the second marketing channel. + $test_campaign_2 = $this->createMock( MarketingCampaign::class ); + $test_campaign_2->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-2' ); + $test_campaign_2->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_2 ); + // Return the sample campaign by the second mock marketing channel. + $test_channel_2->expects( $this->any() )->method( 'get_campaigns' )->willReturn( [ $test_campaign_2 ] ); + // Register the second marketing channel. + $this->marketing_channels_service->register( $test_channel_2 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( + [ + 'test-campaign-1', + 'test-campaign-2', + ], + array_column( $data, 'id' ) + ); + $this->assertEquals( + [ + 'test-channel-1', + 'test-channel-2', + ], + array_column( $data, 'channel' ) + ); + } + + /** + * Tests that the marketing campaigns are paginated and then returned by the endpoint. + */ + public function test_paginates_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Return mock campaigns by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_campaigns' )->willReturn( + [ + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + ] + ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( + [ + 'page' => '1', + 'per_page' => '2', + ] + ); + $response = $this->server->dispatch( $request ); + $headers = $response->get_headers(); + + $this->assertCount( 2, $response->get_data() ); + + $this->assertArrayHasKey( 'Link', $headers ); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + $this->assertEquals( 5, $headers['X-WP-Total'] ); + $this->assertEquals( 3, $headers['X-WP-TotalPages'] ); + } + +} From 44042634e6ab2a78447b22120cebbf5cdc23e5de Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:28:43 +0000 Subject: [PATCH 46/84] Translate Exception message --- plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index a5fde99eb32..f7b96516753 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -33,7 +33,7 @@ class MarketingChannels { */ public function register( MarketingChannelInterface $channel ): void { if ( isset( $this->registered_channels[ $channel->get_slug() ] ) ) { - throw new Exception( 'Marketing channel cannot be registered because there is already a channel registered with the same slug!' ); + throw new Exception( __( 'Marketing channel cannot be registered because there is already a channel registered with the same slug!', 'woocommerce' ) ); } $this->registered_channels[ $channel->get_slug() ] = $channel; From f7be32dc9b4f1107edbb3bd115e04533dd075bed Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 13:30:48 +0000 Subject: [PATCH 47/84] Remove doc references to predetermined list of marketing channels --- .../woocommerce/src/Admin/Marketing/MarketingChannels.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index f7b96516753..786aeb33b5f 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -23,8 +23,6 @@ class MarketingChannels { /** * Registers a marketing channel. * - * Note that only a predetermined list of third party extensions can be registered as a marketing channel. - * * @param MarketingChannelInterface $channel The marketing channel to register. * * @return void @@ -48,9 +46,6 @@ class MarketingChannels { /** * Filter the list of registered marketing channels. * - * Note that only a predetermined list of third party extensions can be registered as a marketing channel. - * Any new plugins added to this array will be cross-checked with that list, which is obtained from WooCommerce.com API. - * * @param MarketingChannelInterface[] $channels Array of registered marketing channels. * * @since x.x.x From 3fb90016dc2b6e11d2e86e1387c5664304993fde Mon Sep 17 00:00:00 2001 From: Nima Karimi <73110514+nima-karimi@users.noreply.github.com> Date: Wed, 28 Dec 2022 17:25:37 +0000 Subject: [PATCH 48/84] Multichannel Marketing - Changes to the marketing classes (#36012) * Rename `get_errors_no` to `get_errors_count` * Remove the validation for marketing channel slugs Do not check if the marketing channel's slug exists in the list returned by WooCommerce.com Recommendation API. This allows any third-party extension to register as a marketing channel. * Revert InstalledExtensions The InstalledExtensions class will be used by the previous generation of the Marketing dashboard (if the user has not enabled the new "Marketing" feature); therefore, it's best to restore it to the original code. * Fix code style * Translate Exception message * Remove doc references to a predetermined list of marketing channels Co-authored-by: Nima --- .../src/Admin/API/MarketingOverview.php | 9 +- .../Admin/Marketing/InstalledExtensions.php | 617 +++++++++++++++++- .../Marketing/MarketingChannelInterface.php | 2 +- .../src/Admin/Marketing/MarketingChannels.php | 84 +-- .../src/Internal/Admin/Marketing.php | 9 +- .../MarketingServiceProvider.php | 5 +- .../Admin/Marketing/MarketingChannelsTest.php | 79 +-- 7 files changed, 624 insertions(+), 181 deletions(-) diff --git a/plugins/woocommerce/src/Admin/API/MarketingOverview.php b/plugins/woocommerce/src/Admin/API/MarketingOverview.php index 883ce04c1bb..930dcb4c0fc 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingOverview.php +++ b/plugins/woocommerce/src/Admin/API/MarketingOverview.php @@ -125,14 +125,7 @@ class MarketingOverview extends \WC_REST_Data_Controller { * @return \WP_Error|\WP_REST_Response */ public function get_installed_plugins( $request ) { - /** - * InstalledExtensions - * - * @var InstalledExtensions $installed_extensions - */ - $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); - - return rest_ensure_response( $installed_extensions->get_data() ); + return rest_ensure_response( InstalledExtensions::get_data() ); } } diff --git a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php index 9669b9014c6..b1e9ad82b80 100644 --- a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php +++ b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php @@ -5,46 +5,597 @@ namespace Automattic\WooCommerce\Admin\Marketing; +use Automattic\WooCommerce\Admin\PluginsHelper; + /** * Installed Marketing Extensions class. */ class InstalledExtensions { - /** - * MarketingChannels repository - * - * @var MarketingChannels - */ - protected $marketing_channels; - - /** - * Class initialization, invoked by the DI container. - * - * @param MarketingChannels $marketing_channels The MarketingChannels repository. - * - * @internal - */ - final public function init( MarketingChannels $marketing_channels ) { - $this->marketing_channels = $marketing_channels; - } /** * Gets an array of plugin data for the "Installed marketing extensions" card. + * + * Valid extensions statuses are: installed, activated, configured */ - public function get_data(): array { - return array_map( - function ( MarketingChannelInterface $channel ) { - return [ - 'slug' => $channel->get_slug(), - 'status' => $channel->is_setup_completed() ? 'configured' : 'activated', - 'settingsUrl' => $channel->get_setup_url(), - 'name' => $channel->get_name(), - 'description' => $channel->get_description(), - 'product_listings_status' => $channel->get_product_listings_status(), - 'errors_no' => $channel->get_errors_no(), - 'icon' => $channel->get_icon_url(), - ]; - }, - $this->marketing_channels->get_registered_channels() - ); + public static function get_data() { + $data = []; + + $automatewoo = self::get_automatewoo_extension_data(); + $aw_referral = self::get_aw_referral_extension_data(); + $aw_birthdays = self::get_aw_birthdays_extension_data(); + $mailchimp = self::get_mailchimp_extension_data(); + $facebook = self::get_facebook_extension_data(); + $pinterest = self::get_pinterest_extension_data(); + $google = self::get_google_extension_data(); + $amazon_ebay = self::get_amazon_ebay_extension_data(); + $mailpoet = self::get_mailpoet_extension_data(); + $creative_mail = self::get_creative_mail_extension_data(); + $tiktok = self::get_tiktok_extension_data(); + $jetpack_crm = self::get_jetpack_crm_extension_data(); + $zapier = self::get_zapier_extension_data(); + $salesforce = self::get_salesforce_extension_data(); + $vimeo = self::get_vimeo_extension_data(); + $trustpilot = self::get_trustpilot_extension_data(); + + if ( $automatewoo ) { + $data[] = $automatewoo; + } + + if ( $aw_referral ) { + $data[] = $aw_referral; + } + + if ( $aw_birthdays ) { + $data[] = $aw_birthdays; + } + + if ( $mailchimp ) { + $data[] = $mailchimp; + } + + if ( $facebook ) { + $data[] = $facebook; + } + + if ( $pinterest ) { + $data[] = $pinterest; + } + + if ( $google ) { + $data[] = $google; + } + + if ( $amazon_ebay ) { + $data[] = $amazon_ebay; + } + + if ( $mailpoet ) { + $data[] = $mailpoet; + } + + if ( $creative_mail ) { + $data[] = $creative_mail; + } + + if ( $tiktok ) { + $data[] = $tiktok; + } + + if ( $jetpack_crm ) { + $data[] = $jetpack_crm; + } + + if ( $zapier ) { + $data[] = $zapier; + } + + if ( $salesforce ) { + $data[] = $salesforce; + } + + if ( $vimeo ) { + $data[] = $vimeo; + } + + if ( $trustpilot ) { + $data[] = $trustpilot; + } + + return $data; } + + /** + * Get allowed plugins. + * + * @return array + */ + public static function get_allowed_plugins() { + return [ + 'automatewoo', + 'mailchimp-for-woocommerce', + 'creative-mail-by-constant-contact', + 'facebook-for-woocommerce', + 'pinterest-for-woocommerce', + 'google-listings-and-ads', + 'hubspot-for-woocommerce', + 'woocommerce-amazon-ebay-integration', + 'mailpoet', + ]; + } + + /** + * Get AutomateWoo extension data. + * + * @return array|bool + */ + protected static function get_automatewoo_extension_data() { + $slug = 'automatewoo'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; + + if ( 'activated' === $data['status'] && function_exists( 'AW' ) ) { + $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings' ); + $data['docsUrl'] = 'https://automatewoo.com/docs/'; + $data['status'] = 'configured'; // Currently no configuration step. + } + + return $data; + } + + /** + * Get AutomateWoo Refer a Friend extension data. + * + * @return array|bool + */ + protected static function get_aw_referral_extension_data() { + $slug = 'automatewoo-referrals'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; + + if ( 'activated' === $data['status'] ) { + $data['docsUrl'] = 'https://automatewoo.com/docs/refer-a-friend/'; + $data['status'] = 'configured'; + if ( function_exists( 'AW_Referrals' ) ) { + $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings&tab=referrals' ); + } + } + + return $data; + } + + /** + * Get AutomateWoo Birthdays extension data. + * + * @return array|bool + */ + protected static function get_aw_birthdays_extension_data() { + $slug = 'automatewoo-birthdays'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; + + if ( 'activated' === $data['status'] ) { + $data['docsUrl'] = 'https://automatewoo.com/docs/getting-started-with-birthdays/'; + $data['status'] = 'configured'; + if ( function_exists( 'AW_Birthdays' ) ) { + $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings&tab=birthdays' ); + } + } + + return $data; + } + + /** + * Get MailChimp extension data. + * + * @return array|bool + */ + protected static function get_mailchimp_extension_data() { + $slug = 'mailchimp-for-woocommerce'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/mailchimp.svg'; + + if ( 'activated' === $data['status'] && function_exists( 'mailchimp_is_configured' ) ) { + $data['docsUrl'] = 'https://mailchimp.com/help/connect-or-disconnect-mailchimp-for-woocommerce/'; + $data['settingsUrl'] = admin_url( 'admin.php?page=mailchimp-woocommerce' ); + + if ( mailchimp_is_configured() ) { + $data['status'] = 'configured'; + } + } + + return $data; + } + + /** + * Get Facebook extension data. + * + * @return array|bool + */ + protected static function get_facebook_extension_data() { + $slug = 'facebook-for-woocommerce'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/facebook-icon.svg'; + + if ( 'activated' === $data['status'] && function_exists( 'facebook_for_woocommerce' ) ) { + $integration = facebook_for_woocommerce()->get_integration(); + + if ( $integration->is_configured() ) { + $data['status'] = 'configured'; + } + + $data['settingsUrl'] = facebook_for_woocommerce()->get_settings_url(); + $data['docsUrl'] = facebook_for_woocommerce()->get_documentation_url(); + } + + return $data; + } + + /** + * Get Pinterest extension data. + * + * @return array|bool + */ + protected static function get_pinterest_extension_data() { + $slug = 'pinterest-for-woocommerce'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/pinterest.svg'; + + $data['docsUrl'] = 'https://woocommerce.com/document/pinterest-for-woocommerce/?utm_medium=product'; + + if ( 'activated' === $data['status'] && class_exists( 'Pinterest_For_Woocommerce' ) ) { + $pinterest_onboarding_completed = Pinterest_For_Woocommerce()::is_setup_complete(); + if ( $pinterest_onboarding_completed ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/pinterest/settings' ); + } else { + $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/pinterest/landing' ); + } + } + + return $data; + } + + /** + * Get Google extension data. + * + * @return array|bool + */ + protected static function get_google_extension_data() { + $slug = 'google-listings-and-ads'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/google.svg'; + + if ( 'activated' === $data['status'] && function_exists( 'woogle_get_container' ) && class_exists( '\Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService' ) ) { + + $merchant_center = woogle_get_container()->get( \Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService::class ); + + if ( $merchant_center->is_setup_complete() ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/google/settings' ); + } else { + $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/google/start' ); + } + + $data['docsUrl'] = 'https://woocommerce.com/document/google-listings-and-ads/?utm_medium=product'; + } + + return $data; + } + + /** + * Get Amazon / Ebay extension data. + * + * @return array|bool + */ + protected static function get_amazon_ebay_extension_data() { + $slug = 'woocommerce-amazon-ebay-integration'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/amazon-ebay.svg'; + + if ( 'activated' === $data['status'] && class_exists( '\CodistoConnect' ) ) { + + $codisto_merchantid = get_option( 'codisto_merchantid' ); + + // Use same check as codisto admin tabs. + if ( is_numeric( $codisto_merchantid ) ) { + $data['status'] = 'configured'; + } + + $data['settingsUrl'] = admin_url( 'admin.php?page=codisto-settings' ); + $data['docsUrl'] = 'https://woocommerce.com/document/multichannel-for-woocommerce-google-amazon-ebay-walmart-integration/?utm_medium=product'; + } + + return $data; + } + + /** + * Get MailPoet extension data. + * + * @return array|bool + */ + protected static function get_mailpoet_extension_data() { + $slug = 'mailpoet'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/mailpoet.svg'; + + if ( 'activated' === $data['status'] && class_exists( '\MailPoet\API\API' ) ) { + $mailpoet_api = \MailPoet\API\API::MP( 'v1' ); + + if ( ! method_exists( $mailpoet_api, 'isSetupComplete' ) || $mailpoet_api->isSetupComplete() ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=mailpoet-settings' ); + } else { + $data['settingsUrl'] = admin_url( 'admin.php?page=mailpoet-newsletters' ); + } + + $data['docsUrl'] = 'https://kb.mailpoet.com/'; + $data['supportUrl'] = 'https://www.mailpoet.com/support/'; + } + + return $data; + } + + /** + * Get Creative Mail for WooCommerce extension data. + * + * @return array|bool + */ + protected static function get_creative_mail_extension_data() { + $slug = 'creative-mail-by-constant-contact'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/creative-mail-by-constant-contact.png'; + + if ( 'activated' === $data['status'] && class_exists( '\CreativeMail\Helpers\OptionsHelper' ) ) { + if ( ! method_exists( '\CreativeMail\Helpers\OptionsHelper', 'get_instance_id' ) || \CreativeMail\Helpers\OptionsHelper::get_instance_id() !== null ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=creativemail_settings' ); + } else { + $data['settingsUrl'] = admin_url( 'admin.php?page=creativemail' ); + } + + $data['docsUrl'] = 'https://app.creativemail.com/kb/help/WooCommerce'; + $data['supportUrl'] = 'https://app.creativemail.com/kb/help/'; + } + + return $data; + } + + /** + * Get TikTok for WooCommerce extension data. + * + * @return array|bool + */ + protected static function get_tiktok_extension_data() { + $slug = 'tiktok-for-business'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/tiktok.jpg'; + + if ( 'activated' === $data['status'] ) { + if ( false !== get_option( 'tt4b_access_token' ) ) { + $data['status'] = 'configured'; + } + + $data['settingsUrl'] = admin_url( 'admin.php?page=tiktok' ); + $data['docsUrl'] = 'https://woocommerce.com/document/tiktok-for-woocommerce/'; + $data['supportUrl'] = 'https://ads.tiktok.com/athena/user-feedback/?identify_key=6a1e079024806640c5e1e695d13db80949525168a052299b4970f9c99cb5ac78'; + } + + return $data; + } + + /** + * Get Jetpack CRM for WooCommerce extension data. + * + * @return array|bool + */ + protected static function get_jetpack_crm_extension_data() { + $slug = 'zero-bs-crm'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/jetpack-crm.png'; + + if ( 'activated' === $data['status'] ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=zerobscrm-plugin-settings' ); + $data['docsUrl'] = 'https://kb.jetpackcrm.com/'; + $data['supportUrl'] = 'https://kb.jetpackcrm.com/crm-support/'; + } + + return $data; + } + + /** + * Get WooCommerce Zapier extension data. + * + * @return array|bool + */ + protected static function get_zapier_extension_data() { + $slug = 'woocommerce-zapier'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/zapier.png'; + + if ( 'activated' === $data['status'] ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=wc-settings&tab=wc_zapier' ); + $data['docsUrl'] = 'https://docs.om4.io/woocommerce-zapier/'; + } + + return $data; + } + + /** + * Get Salesforce extension data. + * + * @return array|bool + */ + protected static function get_salesforce_extension_data() { + $slug = 'integration-with-salesforce'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/salesforce.jpg'; + + if ( 'activated' === $data['status'] && class_exists( '\Integration_With_Salesforce_Admin' ) ) { + if ( ! method_exists( '\Integration_With_Salesforce_Admin', 'get_connection_status' ) || \Integration_With_Salesforce_Admin::get_connection_status() ) { + $data['status'] = 'configured'; + } + + $data['settingsUrl'] = admin_url( 'admin.php?page=integration-with-salesforce' ); + $data['docsUrl'] = 'https://woocommerce.com/document/salesforce-integration/'; + $data['supportUrl'] = 'https://wpswings.com/submit-query/'; + } + + return $data; + } + + /** + * Get Vimeo extension data. + * + * @return array|bool + */ + protected static function get_vimeo_extension_data() { + $slug = 'vimeo'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/vimeo.png'; + + if ( 'activated' === $data['status'] && class_exists( '\Tribe\Vimeo_WP\Vimeo\Vimeo_Auth' ) ) { + if ( method_exists( '\Tribe\Vimeo_WP\Vimeo\Vimeo_Auth', 'has_access_token' ) ) { + $vimeo_auth = new \Tribe\Vimeo_WP\Vimeo\Vimeo_Auth(); + if ( $vimeo_auth->has_access_token() ) { + $data['status'] = 'configured'; + } + } else { + $data['status'] = 'configured'; + } + + $data['settingsUrl'] = admin_url( 'options-general.php?page=vimeo_settings' ); + $data['docsUrl'] = 'https://woocommerce.com/document/vimeo/'; + $data['supportUrl'] = 'https://vimeo.com/help/contact'; + } + + return $data; + } + + /** + * Get Trustpilot extension data. + * + * @return array|bool + */ + protected static function get_trustpilot_extension_data() { + $slug = 'trustpilot-reviews'; + + if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { + return false; + } + + $data = self::get_extension_base_data( $slug ); + $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/trustpilot.png'; + + if ( 'activated' === $data['status'] ) { + $data['status'] = 'configured'; + $data['settingsUrl'] = admin_url( 'admin.php?page=woocommerce-trustpilot-settings-page' ); + $data['docsUrl'] = 'https://woocommerce.com/document/trustpilot-reviews/'; + $data['supportUrl'] = 'https://support.trustpilot.com/hc/en-us/requests/new'; + } + + return $data; + } + + + /** + * Get an array of basic data for a given extension. + * + * @param string $slug Plugin slug. + * + * @return array|false + */ + protected static function get_extension_base_data( $slug ) { + $status = PluginsHelper::is_plugin_active( $slug ) ? 'activated' : 'installed'; + $plugin_data = PluginsHelper::get_plugin_data( $slug ); + + if ( ! $plugin_data ) { + return false; + } + + return [ + 'slug' => $slug, + 'status' => $status, + 'name' => $plugin_data['Name'], + 'description' => html_entity_decode( wp_trim_words( $plugin_data['Description'], 20 ) ), + 'supportUrl' => 'https://woocommerce.com/my-account/create-a-ticket/?utm_medium=product', + ]; + } + } diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php index 3a7233f073b..b2dbc3819a2 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -71,7 +71,7 @@ interface MarketingChannelInterface { * * @return int The number of issues to resolve, or 0 if there are no issues with the channel. */ - public function get_errors_no(): int; + public function get_errors_count(): int; /** * Returns an array of the channel's marketing campaigns. diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index 0fcb6fd1e5e..786aeb33b5f 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; +use Exception; /** * MarketingChannels repository class @@ -20,49 +20,18 @@ class MarketingChannels { */ private $registered_channels = []; - /** - * Array of plugin slugs for allowed marketing channels. - * - * @var string[] - */ - private $allowed_channels; - - /** - * MarketingSpecs repository - * - * @var MarketingSpecs - */ - protected $marketing_specs; - - /** - * Class initialization, invoked by the DI container. - * - * @param MarketingSpecs $marketing_specs The MarketingSpecs class. - * - * @internal - */ - final public function init( MarketingSpecs $marketing_specs ) { - $this->marketing_specs = $marketing_specs; - $this->allowed_channels = $this->get_allowed_channels(); - } - /** * Registers a marketing channel. * - * Note that only a predetermined list of third party extensions can be registered as a marketing channel. - * * @param MarketingChannelInterface $channel The marketing channel to register. * * @return void * - * @see MarketingChannels::is_channel_allowed() Checks if the marketing channel is allowed to be registered or not. + * @throws Exception If the given marketing channel is already registered. */ public function register( MarketingChannelInterface $channel ): void { - if ( ! $this->is_channel_allowed( $channel ) ) { - // Silently log an error and bail. - wc_get_logger()->error( sprintf( 'Marketing channel %s (%s) cannot be registered!', $channel->get_name(), $channel->get_slug() ) ); - - return; + if ( isset( $this->registered_channels[ $channel->get_slug() ] ) ) { + throw new Exception( __( 'Marketing channel cannot be registered because there is already a channel registered with the same slug!', 'woocommerce' ) ); } $this->registered_channels[ $channel->get_slug() ] = $channel; @@ -77,55 +46,12 @@ class MarketingChannels { /** * Filter the list of registered marketing channels. * - * Note that only a predetermined list of third party extensions can be registered as a marketing channel. - * Any new plugins added to this array will be cross-checked with that list, which is obtained from WooCommerce.com API. - * * @param MarketingChannelInterface[] $channels Array of registered marketing channels. * * @since x.x.x */ $channels = apply_filters( 'woocommerce_marketing_channels', $this->registered_channels ); - // Only return allowed channels. - $allowed_channels = array_filter( - $channels, - function ( MarketingChannelInterface $channel ) { - if ( ! $this->is_channel_allowed( $channel ) ) { - // Silently log an error and bail. - wc_get_logger()->error( sprintf( 'Marketing channel %s (%s) cannot be registered!', $channel->get_name(), $channel->get_slug() ) ); - - return false; - } - - return true; - } - ); - - return array_values( $allowed_channels ); - } - - /** - * Returns an array of plugin slugs for the marketing channels that are allowed to be registered. - * - * @return array - */ - protected function get_allowed_channels(): array { - $recommended_channels = $this->marketing_specs->get_recommended_plugins(); - if ( empty( $recommended_channels ) ) { - return []; - } - - return array_column( $recommended_channels, 'product', 'product' ); - } - - /** - * Determines whether the given marketing channel is allowed to be registered. - * - * @param MarketingChannelInterface $channel The marketing channel object. - * - * @return bool - */ - protected function is_channel_allowed( MarketingChannelInterface $channel ): bool { - return isset( $this->allowed_channels[ $channel->get_slug() ] ); + return array_values( $channels ); } } diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing.php b/plugins/woocommerce/src/Internal/Admin/Marketing.php index f11cd2d31d9..47d4ab241bb 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing.php @@ -165,14 +165,7 @@ class Marketing { return $settings; } - /** - * InstalledExtensions helper class. - * - * @var InstalledExtensions $installed_extensions - */ - $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); - - $settings['marketing']['installedExtensions'] = $installed_extensions->get_data(); + $settings['marketing']['installedExtensions'] = InstalledExtensions::get_data(); return $settings; } diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php index 8e27386ae86..c00e484fe89 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php @@ -5,7 +5,6 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; -use Automattic\WooCommerce\Admin\Marketing\InstalledExtensions; use Automattic\WooCommerce\Admin\Marketing\MarketingChannels; use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; @@ -30,7 +29,6 @@ class MarketingServiceProvider extends AbstractServiceProvider { protected $provides = array( MarketingSpecs::class, MarketingChannels::class, - InstalledExtensions::class, ); /** @@ -38,7 +36,6 @@ class MarketingServiceProvider extends AbstractServiceProvider { */ public function register() { $this->share( MarketingSpecs::class ); - $this->share( MarketingChannels::class )->addArgument( MarketingSpecs::class ); - $this->share( InstalledExtensions::class )->addArgument( MarketingChannels::class ); + $this->share( MarketingChannels::class ); } } diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php index cf5885a0557..fdc112ae370 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php @@ -4,7 +4,6 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingChannelInterface; use Automattic\WooCommerce\Admin\Marketing\MarketingChannels; -use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; use WC_Unit_Test_Case; /** @@ -16,29 +15,17 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { * Runs before each test. */ public function setUp(): void { - delete_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT ); + remove_all_filters( 'woocommerce_marketing_channels' ); } /** - * @testdox A marketing channel can be registered using the `register` method if it is in the allowed list. + * @testdox A marketing channel can be registered using the `register` method if the same channel slug is NOT previously registered. */ - public function test_registers_allowed_channels() { + public function test_registers_channel() { $test_channel = $this->createMock( MarketingChannelInterface::class ); $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); - $marketing_specs = $this->createMock( MarketingSpecs::class ); - $marketing_specs->expects( $this->once() ) - ->method( 'get_recommended_plugins' ) - ->willReturn( - [ - [ - 'product' => 'test-channel-1', - ], - ] - ); - $marketing_channels = new MarketingChannels(); - $marketing_channels->init( $marketing_specs ); $marketing_channels->register( $test_channel ); $this->assertNotEmpty( $marketing_channels->get_registered_channels() ); @@ -46,42 +33,33 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { } /** - * @testdox A marketing channel can NOT be registered using the `register` method if it is NOT in the allowed list. + * @testdox A marketing channel can NOT be registered using the `register` method if it is previously registered. */ - public function test_does_not_register_disallowed_channels() { - $test_channel = $this->createMock( MarketingChannelInterface::class ); - $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + public function test_throws_exception_if_registering_existing_channels() { + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); - $marketing_specs = $this->createMock( MarketingSpecs::class ); - $marketing_specs->expects( $this->once() )->method( 'get_recommended_plugins' )->willReturn( [] ); + $test_channel_1_duplicate = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1_duplicate->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); $marketing_channels = new MarketingChannels(); - $marketing_channels->init( $marketing_specs ); - $marketing_channels->register( $test_channel ); + $marketing_channels->register( $test_channel_1 ); - $this->assertEmpty( $marketing_channels->get_registered_channels() ); + $this->expectException( \Exception::class ); + $marketing_channels->register( $test_channel_1_duplicate ); + + $this->assertCount( 1, $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel_1, $marketing_channels->get_registered_channels()[0] ); } /** - * @testdox A marketing channel can be registered using the `woocommerce_marketing_channels` WordPress filter if it is in the allowed list. + * @testdox A marketing channel can be registered using the `woocommerce_marketing_channels` WordPress filter if the same channel slug is NOT previously registered. */ - public function test_registers_allowed_channels_using_wp_filter() { + public function test_registers_channel_using_wp_filter() { $test_channel = $this->createMock( MarketingChannelInterface::class ); $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); - $marketing_specs = $this->createMock( MarketingSpecs::class ); - $marketing_specs->expects( $this->once() ) - ->method( 'get_recommended_plugins' ) - ->willReturn( - [ - [ - 'product' => 'test-channel-1', - ], - ] - ); - $marketing_channels = new MarketingChannels(); - $marketing_channels->init( $marketing_specs ); add_filter( 'woocommerce_marketing_channels', @@ -97,24 +75,29 @@ class MarketingChannelsTest extends WC_Unit_Test_Case { } /** - * @testdox A marketing channel can NOT be registered using the `woocommerce_marketing_channels` WordPress filter if it NOT is in the allowed list. + * @testdox A marketing channel can NOT be registered using the `woocommerce_marketing_channels` WordPress filter if it is previously registered. */ - public function test_does_not_register_disallowed_channels_using_wp_filter() { - $test_channel = $this->createMock( MarketingChannelInterface::class ); - $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + public function test_overrides_existing_channel_if_registered_using_wp_filter() { + $marketing_channels = new MarketingChannels(); - set_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, [] ); + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_channels->register( $test_channel_1 ); + + $test_channel_1_duplicate = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1_duplicate->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); add_filter( 'woocommerce_marketing_channels', - function ( array $channels ) use ( $test_channel ) { - $channels[ $test_channel->get_slug() ] = $test_channel; + function ( array $channels ) use ( $test_channel_1_duplicate ) { + $channels[ $test_channel_1_duplicate->get_slug() ] = $test_channel_1_duplicate; return $channels; } ); - $marketing_channels = new MarketingChannels(); - $this->assertEmpty( $marketing_channels->get_registered_channels() ); + $this->assertCount( 1, $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel_1_duplicate, $marketing_channels->get_registered_channels()[0] ); } } From 130b2a94c605295bf2988472b1d8421c4da5e792 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 21:05:09 +0000 Subject: [PATCH 49/84] Add `unregister_all` method To allow unregistering all marketing channels. --- .../src/Admin/Marketing/MarketingChannels.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index 786aeb33b5f..61cce159181 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -37,6 +37,15 @@ class MarketingChannels { $this->registered_channels[ $channel->get_slug() ] = $channel; } + /** + * Unregisters all marketing channels. + * + * @return void + */ + public function unregister_all(): void { + unset( $this->registered_channels ); + } + /** * Returns an array of all registered marketing channels. * From 4d6ef8ece34218539f88f232352a4f3c4f9a4bb1 Mon Sep 17 00:00:00 2001 From: Nima Date: Wed, 28 Dec 2022 21:05:27 +0000 Subject: [PATCH 50/84] Unregister all channels on test tear down --- .../src/Admin/API/MarketingCampaignsTest.php | 18 +++++++++++++----- .../src/Admin/API/MarketingChannelsTest.php | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php index 27b94bc4239..313fca64b66 100644 --- a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php @@ -26,6 +26,11 @@ class MarketingCampaignsTest extends WC_REST_Unit_Test_Case { */ const ENDPOINT = '/wc-admin/marketing/campaigns'; + /** + * @var MarketingChannelsService + */ + private $marketing_channels_service; + /** * Set up. */ @@ -40,14 +45,17 @@ class MarketingCampaignsTest extends WC_REST_Unit_Test_Case { ); wp_set_current_user( $this->user ); - /** - * MarketingChannels class. - * - * @var MarketingChannelsService $marketing_channels_service - */ $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); } + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + /** * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. */ diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php index a043e77c753..9159b977ba4 100644 --- a/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php @@ -25,6 +25,11 @@ class MarketingChannelsTest extends WC_REST_Unit_Test_Case { */ const ENDPOINT = '/wc-admin/marketing/channels'; + /** + * @var MarketingChannelsService + */ + private $marketing_channels_service; + /** * Set up. */ @@ -39,14 +44,17 @@ class MarketingChannelsTest extends WC_REST_Unit_Test_Case { ); wp_set_current_user( $this->user ); - /** - * MarketingChannels class. - * - * @var MarketingChannelsService $marketing_channels_service - */ $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); } + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + /** * Tests that the registered marketing channels are returned by the endpoint. */ From 264c92a52fef791bbd6ed43c7ede5ac1aeb1687e Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 11:27:31 +0000 Subject: [PATCH 51/84] Change API access denied authorization code --- plugins/woocommerce/src/Admin/API/MarketingCampaigns.php | 4 ++-- plugins/woocommerce/src/Admin/API/MarketingChannels.php | 2 +- .../woocommerce/src/Admin/API/MarketingRecommendations.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php index 9c479d5e705..e59ede8740d 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -68,7 +68,7 @@ class MarketingCampaigns extends WC_REST_Controller { */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'install_plugins' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing campaigns.', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing campaigns.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -192,7 +192,7 @@ class MarketingCampaigns extends WC_REST_Controller { 'readonly' => true, ], 'manage_url' => [ - 'description' => __( 'URL to the campaign management page', 'woocommerce' ), + 'description' => __( 'URL to the campaign management page.', 'woocommerce' ), 'type' => 'string', 'context' => [ 'view' ], 'readonly' => true, diff --git a/plugins/woocommerce/src/Admin/API/MarketingChannels.php b/plugins/woocommerce/src/Admin/API/MarketingChannels.php index a02039339f1..6772cfbcc42 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/API/MarketingChannels.php @@ -66,7 +66,7 @@ class MarketingChannels extends WC_REST_Controller { */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'install_plugins' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php index a64fcce02d7..7e9cbf398b8 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php +++ b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php @@ -74,7 +74,7 @@ class MarketingRecommendations extends WC_REST_Controller { */ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'install_plugins' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => 400 ) ); + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; From 20efcfb8042f725e57a6b8c868d5496549302d3d Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 14:05:27 +0000 Subject: [PATCH 52/84] Change API access permission --- plugins/woocommerce/src/Admin/API/MarketingCampaigns.php | 4 ++-- plugins/woocommerce/src/Admin/API/MarketingChannels.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php index e59ede8740d..8316d9ef11f 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -67,8 +67,8 @@ class MarketingCampaigns extends WC_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'install_plugins' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing campaigns.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; diff --git a/plugins/woocommerce/src/Admin/API/MarketingChannels.php b/plugins/woocommerce/src/Admin/API/MarketingChannels.php index 6772cfbcc42..bb22cd01c74 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/API/MarketingChannels.php @@ -65,8 +65,8 @@ class MarketingChannels extends WC_REST_Controller { * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { - if ( ! current_user_can( 'install_plugins' ) ) { - return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; From 16874164138e6332af0960b59faafc04ec60ffd3 Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 14:54:03 +0000 Subject: [PATCH 53/84] Add MarketingCampaignType class This allows defining campaign types for each marketing channel. --- .../Admin/Marketing/MarketingCampaignType.php | 130 ++++++++++++++++++ .../Marketing/MarketingChannelInterface.php | 7 + 2 files changed, 137 insertions(+) create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php new file mode 100644 index 00000000000..032dc6f249d --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php @@ -0,0 +1,130 @@ +id = $id; + $this->channel = $channel; + $this->name = $name; + $this->description = $description; + $this->create_url = $create_url; + $this->icon_url = $icon_url; + } + + /** + * Returns the marketing campaign's unique identifier. + * + * @return string + */ + public function get_id(): string { + return $this->id; + } + + /** + * Returns the marketing channel that this campaign type belongs to. + * + * @return MarketingChannelInterface + */ + public function get_channel(): MarketingChannelInterface { + return $this->channel; + } + + /** + * Returns the name of the marketing campaign type. + * + * @return string + */ + public function get_name(): string { + return $this->name; + } + + /** + * Returns the description of the marketing campaign type. + * + * @return string + */ + public function get_description(): string { + return $this->description; + } + + /** + * Returns the URL to the create campaign page. + * + * @return string + */ + public function get_create_url(): string { + return $this->create_url; + } + + /** + * Returns the URL to an image/icon for the campaign type. + * + * @return string + */ + public function get_icon_url(): string { + return $this->icon_url; + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php index b2dbc3819a2..6794c4b4259 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -73,6 +73,13 @@ interface MarketingChannelInterface { */ public function get_errors_count(): int; + /** + * Returns an array of marketing campaign types that the channel supports. + * + * @return MarketingCampaignType[] Array of marketing campaign type objects. + */ + public function get_supported_campaign_types(): array; + /** * Returns an array of the channel's marketing campaigns. * From 8ef6532a078153e3bdb196f52cfa8eeb095a7b2b Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 14:55:06 +0000 Subject: [PATCH 54/84] Add campaign type property to campaign class --- .../src/Admin/API/MarketingCampaigns.php | 2 +- .../src/Admin/Marketing/MarketingCampaign.php | 47 ++++++------------- .../Admin/Marketing/MarketingCampaignTest.php | 40 +++------------- 3 files changed, 23 insertions(+), 66 deletions(-) diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php index 8316d9ef11f..d8f9ac79378 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -143,7 +143,7 @@ class MarketingCampaigns extends WC_REST_Controller { public function prepare_item_for_response( $item, $request ) { $data = [ 'id' => $item->get_id(), - 'channel' => $item->get_channel()->get_slug(), + 'channel' => $item->get_type()->get_channel()->get_slug(), 'title' => $item->get_title(), 'manage_url' => $item->get_manage_url(), ]; diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php index 45037fea367..c85ced83f21 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php @@ -7,14 +7,12 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use JsonSerializable; - /** * MarketingCampaign class * * @since x.x.x */ -class MarketingCampaign implements JsonSerializable { +class MarketingCampaign { /** * The unique identifier. * @@ -23,11 +21,11 @@ class MarketingCampaign implements JsonSerializable { protected $id; /** - * The marketing channel that this campaign belongs to. + * The marketing campaign type. * - * @var MarketingChannelInterface + * @var MarketingCampaignType */ - protected $channel; + protected $type; /** * Title of the marketing campaign. @@ -53,15 +51,15 @@ class MarketingCampaign implements JsonSerializable { /** * MarketingCampaign constructor. * - * @param string $id The marketing campaign's unique identifier. - * @param MarketingChannelInterface $channel The marketing channel that this campaign belongs to. - * @param string $title The title of the marketing campaign. - * @param string $manage_url The URL to the channel's campaign management page. - * @param Price|null $cost The cost of the marketing campaign with the currency. + * @param string $id The marketing campaign's unique identifier. + * @param MarketingCampaignType $type The marketing campaign type. + * @param string $title The title of the marketing campaign. + * @param string $manage_url The URL to the channel's campaign management page. + * @param Price|null $cost The cost of the marketing campaign with the currency. */ - public function __construct( string $id, MarketingChannelInterface $channel, string $title, string $manage_url, Price $cost = null ) { + public function __construct( string $id, MarketingCampaignType $type, string $title, string $manage_url, Price $cost = null ) { $this->id = $id; - $this->channel = $channel; + $this->type = $type; $this->title = $title; $this->manage_url = $manage_url; $this->cost = $cost; @@ -77,12 +75,12 @@ class MarketingCampaign implements JsonSerializable { } /** - * Returns the marketing channel that this campaign belongs to. + * Returns the marketing campaign type. * - * @return MarketingChannelInterface + * @return MarketingCampaignType */ - public function get_channel(): MarketingChannelInterface { - return $this->channel; + public function get_type(): MarketingCampaignType { + return $this->type; } /** @@ -111,19 +109,4 @@ class MarketingCampaign implements JsonSerializable { public function get_cost(): ?Price { return $this->cost; } - - /** - * Serialize the marketing campaign data. - * - * @return array - */ - public function jsonSerialize() { - return [ - 'id' => $this->get_id(), - 'channel' => $this->get_channel()->get_slug(), - 'title' => $this->get_title(), - 'manage_url' => $this->get_manage_url(), - 'cost' => $this->get_cost(), - ]; - } } diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php index 30d8c243ba5..4277cff9437 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingCampaign; -use Automattic\WooCommerce\Admin\Marketing\MarketingChannelInterface; +use Automattic\WooCommerce\Admin\Marketing\MarketingCampaignType; use Automattic\WooCommerce\Admin\Marketing\Price; use WC_Unit_Test_Case; @@ -13,15 +13,15 @@ use WC_Unit_Test_Case; class MarketingCampaignTest extends WC_Unit_Test_Case { /** - * @testdox `get_id`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. + * @testdox `get_id`, `get_type`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. */ public function test_get_methods_return_properties() { - $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); - $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); - $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); + $marketing_campaign = new MarketingCampaign( '1234', $test_campaign_type_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); $this->assertEquals( '1234', $marketing_campaign->get_id() ); + $this->assertEquals( $test_campaign_type_1, $marketing_campaign->get_type() ); $this->assertEquals( 'Ad #1234', $marketing_campaign->get_title() ); $this->assertEquals( 'https://example.com/manage-campaigns', $marketing_campaign->get_manage_url() ); $this->assertNotNull( $marketing_campaign->get_cost() ); @@ -33,36 +33,10 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @testdox `cost` property can be null. */ public function test_cost_can_be_null() { - $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); - $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, 'Ad #1234', 'https://example.com/manage-campaigns' ); + $marketing_campaign = new MarketingCampaign( '1234', $test_campaign_type_1, 'Ad #1234', 'https://example.com/manage-campaigns' ); $this->assertNull( $marketing_campaign->get_cost() ); } - - /** - * @testdox It can be serialized to JSON including all its properties. - */ - public function test_can_be_serialized_to_json() { - $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); - $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); - - $marketing_campaign = new MarketingCampaign( '1234', $test_channel_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); - - $json = wp_json_encode( $marketing_campaign ); - $this->assertNotEmpty( $json ); - $this->assertEqualSets( - [ - 'id' => $marketing_campaign->get_id(), - 'channel' => 'test-channel-1', - 'title' => $marketing_campaign->get_title(), - 'manage_url' => $marketing_campaign->get_manage_url(), - 'cost' => [ - 'value' => $marketing_campaign->get_cost()->get_value(), - 'currency' => $marketing_campaign->get_cost()->get_currency(), - ], - ], - json_decode( $json, true ) - ); - } } From 1b668d94f3d490f27ac8342ba2fa9b54bfd49415 Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 14:55:52 +0000 Subject: [PATCH 55/84] Add `marketing/campaign-types` API This API returns the aggregated list of supported marketing campaign types for all registered marketing channels. --- plugins/woocommerce/src/Admin/API/Init.php | 1 + .../src/Admin/API/MarketingCampaignTypes.php | 211 ++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index 8fff86a1ee1..625374b2683 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -67,6 +67,7 @@ class Init { 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', 'Automattic\WooCommerce\Admin\API\MarketingChannels', 'Automattic\WooCommerce\Admin\API\MarketingCampaigns', + 'Automattic\WooCommerce\Admin\API\MarketingCampaignTypes', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php b/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php new file mode 100644 index 00000000000..f01dba7a649 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php @@ -0,0 +1,211 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves the query params for the collections. + * + * @return array Query parameters for the collection. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + unset( $params['search'] ); + + return $params; + } + + /** + * Check whether a given request has permission to view marketing campaigns. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Returns an aggregated array of marketing campaigns for all active marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + // Aggregate the supported campaign types from all registered marketing channels. + $responses = []; + foreach ( $marketing_channels_service->get_registered_channels() as $channel ) { + foreach ( $channel->get_supported_campaign_types() as $campaign_type ) { + $response = $this->prepare_item_for_response( $campaign_type, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingCampaignType $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'id' => $item->get_id(), + 'name' => $item->get_name(), + 'description' => $item->get_description(), + 'channel' => [ + 'slug' => $item->get_channel()->get_slug(), + 'name' => $item->get_channel()->get_name(), + ], + 'create_url' => $item->get_create_url(), + 'icon_url' => $item->get_icon_url(), + ]; + + $context = $request['context'] ?? 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_campaign_type', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'The unique identifier for the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'Name of the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'description' => __( 'Description of the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'channel' => [ + 'description' => __( 'The marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'description' => __( 'The unique identifier of the marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'The name of the marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + 'create_url' => [ + 'description' => __( 'URL to the create campaign page for this campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon_url' => [ + 'description' => __( 'URL to an image/icon for the campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + + +} From 02cd258b44352bdfc2775ab6e17bd4c2b187a231 Mon Sep 17 00:00:00 2001 From: Nima Date: Tue, 3 Jan 2023 15:00:10 +0000 Subject: [PATCH 56/84] Add unit tests for `marketing/campaign-types` API --- .../Admin/API/MarketingCampaignTypesTest.php | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php new file mode 100644 index 00000000000..f38b49e1bb5 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php @@ -0,0 +1,101 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. + */ + public function test_returns_aggregated_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Create a mock marketing campaign type. + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-type-1' ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_1 ); + // Return the sample campaign type by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_supported_campaign_types' )->willReturn( [ $test_campaign_type_1 ] ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + // Create a second mock marketing channel. + $test_channel_2 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_2->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-2' ); + // Create a mock marketing campaign type for the second marketing channel. + $test_campaign_type_2 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-type-2' ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_2 ); + // Return the sample campaign by the second mock marketing channel. + $test_channel_2->expects( $this->any() )->method( 'get_supported_campaign_types' )->willReturn( [ $test_campaign_type_2 ] ); + // Register the second marketing channel. + $this->marketing_channels_service->register( $test_channel_2 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( + [ + 'test-campaign-type-1', + 'test-campaign-type-2', + ], + array_column( $data, 'id' ) + ); + } + +} From b2ff0ba1a2b8012b5664ffea8f8d73b3114c236b Mon Sep 17 00:00:00 2001 From: Nima Karimi <73110514+nima-karimi@users.noreply.github.com> Date: Fri, 13 Jan 2023 16:54:48 +0000 Subject: [PATCH 57/84] Multichannel Marketing - API (#36222) * Rename `get_errors_no` to `get_errors_count` * Remove the validation for marketing channel slugs Do not check if the marketing channel's slug exists in the list returned by WooCommerce.com Recommendation API. This essentially allows any third-party extension to register as a marketing channel. * Revert InstalledExtensions The InstalledExtensions class will be used by the previous generation of Marketing dashboard (if the user has not enabled the new "Marketing" feature); therefore, it's best to restore it to the original code. * Fix code style * Add channel property to MarketingCampaign * Add methods to filter the recommended marketing channels and extensions * Add `marketing/recommendations` API * Add unit tests for `marketing/recommendations` API * Add `marketing/channels` API * Add unit tests for `marketing/channels` API * Add `marketing/campaigns` API * Add unit tests for `marketing/campaigns` API * Translate Exception message * Remove doc references to predetermined list of marketing channels * Add `unregister_all` method To allow unregistering all marketing channels. * Unregister all channels on test tear down * Change API access denied authorization code * Change API access permission * Add MarketingCampaignType class This allows defining campaign types for each marketing channel. * Add campaign type property to campaign class * Add `marketing/campaign-types` API This API returns the aggregated list of supported marketing campaign types for all registered marketing channels. * Add unit tests for `marketing/campaign-types` API * Remove unused jsonSerialize method * Fix unit tests Co-authored-by: Nima --- plugins/woocommerce/src/Admin/API/Init.php | 4 + .../src/Admin/API/MarketingCampaignTypes.php | 211 ++++++++++++++++ .../src/Admin/API/MarketingCampaigns.php | 237 ++++++++++++++++++ .../src/Admin/API/MarketingChannels.php | 192 ++++++++++++++ .../Admin/API/MarketingRecommendations.php | 235 +++++++++++++++++ .../src/Admin/Marketing/MarketingCampaign.php | 46 ++-- .../Admin/Marketing/MarketingCampaignType.php | 130 ++++++++++ .../Marketing/MarketingChannelInterface.php | 7 + .../src/Admin/Marketing/MarketingChannels.php | 9 + .../woocommerce/src/Admin/Marketing/Price.php | 16 +- .../Admin/Marketing/MarketingSpecs.php | 72 ++++++ .../Admin/API/MarketingCampaignTypesTest.php | 101 ++++++++ .../src/Admin/API/MarketingCampaignsTest.php | 155 ++++++++++++ .../src/Admin/API/MarketingChannelsTest.php | 77 ++++++ .../API/MarketingRecommendationsTest.php | 138 ++++++++++ .../Admin/Marketing/MarketingCampaignTest.php | 34 +-- 16 files changed, 1602 insertions(+), 62 deletions(-) create mode 100644 plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php create mode 100644 plugins/woocommerce/src/Admin/API/MarketingCampaigns.php create mode 100644 plugins/woocommerce/src/Admin/API/MarketingChannels.php create mode 100644 plugins/woocommerce/src/Admin/API/MarketingRecommendations.php create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index 42b2bac82e7..625374b2683 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -64,6 +64,10 @@ class Init { 'Automattic\WooCommerce\Admin\API\Experiments', 'Automattic\WooCommerce\Admin\API\Marketing', 'Automattic\WooCommerce\Admin\API\MarketingOverview', + 'Automattic\WooCommerce\Admin\API\MarketingRecommendations', + 'Automattic\WooCommerce\Admin\API\MarketingChannels', + 'Automattic\WooCommerce\Admin\API\MarketingCampaigns', + 'Automattic\WooCommerce\Admin\API\MarketingCampaignTypes', 'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Orders', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php b/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php new file mode 100644 index 00000000000..f01dba7a649 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaignTypes.php @@ -0,0 +1,211 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves the query params for the collections. + * + * @return array Query parameters for the collection. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + unset( $params['search'] ); + + return $params; + } + + /** + * Check whether a given request has permission to view marketing campaigns. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Returns an aggregated array of marketing campaigns for all active marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + // Aggregate the supported campaign types from all registered marketing channels. + $responses = []; + foreach ( $marketing_channels_service->get_registered_channels() as $channel ) { + foreach ( $channel->get_supported_campaign_types() as $campaign_type ) { + $response = $this->prepare_item_for_response( $campaign_type, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingCampaignType $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'id' => $item->get_id(), + 'name' => $item->get_name(), + 'description' => $item->get_description(), + 'channel' => [ + 'slug' => $item->get_channel()->get_slug(), + 'name' => $item->get_channel()->get_name(), + ], + 'create_url' => $item->get_create_url(), + 'icon_url' => $item->get_icon_url(), + ]; + + $context = $request['context'] ?? 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_campaign_type', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'The unique identifier for the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'Name of the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'description' => __( 'Description of the marketing campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'channel' => [ + 'description' => __( 'The marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'description' => __( 'The unique identifier of the marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'The name of the marketing channel that this campaign type belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + 'create_url' => [ + 'description' => __( 'URL to the create campaign page for this campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon_url' => [ + 'description' => __( 'URL to an image/icon for the campaign type.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + + +} diff --git a/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php new file mode 100644 index 00000000000..d8f9ac79378 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingCampaigns.php @@ -0,0 +1,237 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view marketing campaigns. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + + /** + * Returns an aggregated array of marketing campaigns for all active marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + // Aggregate the campaigns from all registered marketing channels. + $responses = []; + foreach ( $marketing_channels_service->get_registered_channels() as $channel ) { + foreach ( $channel->get_campaigns() as $campaign ) { + $response = $this->prepare_item_for_response( $campaign, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + } + + // Pagination. + $page = $request['page']; + $items_per_page = $request['per_page']; + $offset = ( $page - 1 ) * $items_per_page; + $paginated_results = array_slice( $responses, $offset, $items_per_page ); + + $response = rest_ensure_response( $paginated_results ); + + $total_campaigns = count( $responses ); + $max_pages = ceil( $total_campaigns / $items_per_page ); + $response->header( 'X-WP-Total', $total_campaigns ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + // Add previous and next page links to response header. + $request_params = $request->get_query_params(); + $base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingCampaign $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'id' => $item->get_id(), + 'channel' => $item->get_type()->get_channel()->get_slug(), + 'title' => $item->get_title(), + 'manage_url' => $item->get_manage_url(), + ]; + + if ( $item->get_cost() instanceof Price ) { + $data['cost'] = [ + 'value' => wc_format_decimal( $item->get_cost()->get_value() ), + 'currency' => $item->get_cost()->get_currency(), + ]; + } + + $context = $request['context'] ?? 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_campaign', + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'description' => __( 'The unique identifier for the marketing campaign.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'channel' => [ + 'description' => __( 'The unique identifier for the marketing channel that this campaign belongs to.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'title' => [ + 'description' => __( 'Title of the marketing campaign.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'manage_url' => [ + 'description' => __( 'URL to the campaign management page.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'cost' => [ + 'description' => __( 'Cost of the marketing campaign.', 'woocommerce' ), + 'context' => [ 'view' ], + 'readonly' => true, + 'type' => 'object', + 'properties' => [ + 'value' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'currency' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Retrieves the query params for the collections. + * + * @return array Query parameters for the collection. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + unset( $params['search'] ); + + return $params; + } + + +} diff --git a/plugins/woocommerce/src/Admin/API/MarketingChannels.php b/plugins/woocommerce/src/Admin/API/MarketingChannels.php new file mode 100644 index 00000000000..bb22cd01c74 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingChannels.php @@ -0,0 +1,192 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view marketing channels. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Return installed marketing channels. + * + * @param WP_REST_Request $request Request data. + * + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + /** + * MarketingChannels class. + * + * @var MarketingChannelsService $marketing_channels_service + */ + $marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + + $channels = $marketing_channels_service->get_registered_channels(); + + $responses = []; + foreach ( $channels as $item ) { + $response = $this->prepare_item_for_response( $item, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param MarketingChannelInterface $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $data = [ + 'slug' => $item->get_slug(), + 'is_setup_completed' => $item->is_setup_completed(), + 'settings_url' => $item->get_setup_url(), + 'name' => $item->get_name(), + 'description' => $item->get_description(), + 'product_listings_status' => $item->get_product_listings_status(), + 'errors_count' => $item->get_errors_count(), + 'icon' => $item->get_icon_url(), + ]; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_channel', + 'type' => 'object', + 'properties' => [ + 'slug' => [ + 'description' => __( 'Unique identifier string for the marketing channel extension, also known as the plugin slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'description' => __( 'Name of the marketing channel.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'description' => __( 'Description of the marketing channel.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon' => [ + 'description' => __( 'Path to the channel icon.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'is_setup_completed' => [ + 'type' => 'boolean', + 'description' => __( 'Whether or not the marketing channel is set up.', 'woocommerce' ), + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'settings_url' => [ + 'description' => __( 'URL to the settings page, or the link to complete the setup/onboarding if the channel has not been set up yet.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'product_listings_status' => [ + 'description' => __( 'Status of the marketing channel\'s product listings.', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'errors_count' => [ + 'description' => __( 'Number of channel issues/errors (e.g. account-related errors, product synchronization issues, etc.).', 'woocommerce' ), + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php new file mode 100644 index 00000000000..7e9cbf398b8 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/MarketingRecommendations.php @@ -0,0 +1,235 @@ +namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_items' ], + 'permission_callback' => [ $this, 'get_items_permissions_check' ], + 'args' => [ + 'category' => [ + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'sanitize_title_with_dashes', + 'enum' => [ 'channels', 'extensions' ], + 'required' => true, + ], + ], + ], + 'schema' => [ $this, 'get_public_item_schema' ], + ] + ); + } + + /** + * Check whether a given request has permission to view marketing recommendations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view marketing channels.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Retrieves a collection of recommendations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + /** + * MarketingSpecs class. + * + * @var MarketingSpecs $marketing_specs + */ + $marketing_specs = wc_get_container()->get( MarketingSpecs::class ); + + $category = $request->get_param( 'category' ); + if ( 'channels' === $category ) { + $items = $marketing_specs->get_recommended_marketing_channels(); + } elseif ( 'extensions' === $category ) { + $items = $marketing_specs->get_recommended_marketing_extensions_excluding_channels(); + } else { + return new WP_Error( 'woocommerce_rest_invalid_category', __( 'The specified category for recommendations is invalid. Allowed values: "channels", "extensions".', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $responses = []; + foreach ( $items as $item ) { + $response = $this->prepare_item_for_response( $item, $request ); + $responses[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $responses ); + } + + /** + * Prepares the item for the REST response. + * + * @param array $item WordPress representation of the item. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'marketing_recommendation', + 'type' => 'object', + 'properties' => [ + 'title' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'description' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'url' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'direct_install' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'icon' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'product' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'plugin' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'categories' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'string', + ], + ], + 'subcategories' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + 'tags' => [ + 'type' => 'array', + 'context' => [ 'view' ], + 'readonly' => true, + 'items' => [ + 'type' => 'object', + 'context' => [ 'view' ], + 'readonly' => true, + 'properties' => [ + 'slug' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'name' => [ + 'type' => 'string', + 'context' => [ 'view' ], + 'readonly' => true, + ], + ], + ], + ], + ], + ]; + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php index 7b3f99a4b3a..c85ced83f21 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php @@ -7,14 +7,12 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use JsonSerializable; - /** * MarketingCampaign class * * @since x.x.x */ -class MarketingCampaign implements JsonSerializable { +class MarketingCampaign { /** * The unique identifier. * @@ -22,6 +20,13 @@ class MarketingCampaign implements JsonSerializable { */ protected $id; + /** + * The marketing campaign type. + * + * @var MarketingCampaignType + */ + protected $type; + /** * Title of the marketing campaign. * @@ -46,13 +51,15 @@ class MarketingCampaign implements JsonSerializable { /** * MarketingCampaign constructor. * - * @param string $id The marketing campaign's unique identifier. - * @param string $title The title of the marketing campaign. - * @param string $manage_url The URL to the channel's campaign management page. - * @param Price|null $cost The cost of the marketing campaign with the currency. + * @param string $id The marketing campaign's unique identifier. + * @param MarketingCampaignType $type The marketing campaign type. + * @param string $title The title of the marketing campaign. + * @param string $manage_url The URL to the channel's campaign management page. + * @param Price|null $cost The cost of the marketing campaign with the currency. */ - public function __construct( string $id, string $title, string $manage_url, Price $cost = null ) { + public function __construct( string $id, MarketingCampaignType $type, string $title, string $manage_url, Price $cost = null ) { $this->id = $id; + $this->type = $type; $this->title = $title; $this->manage_url = $manage_url; $this->cost = $cost; @@ -67,6 +74,15 @@ class MarketingCampaign implements JsonSerializable { return $this->id; } + /** + * Returns the marketing campaign type. + * + * @return MarketingCampaignType + */ + public function get_type(): MarketingCampaignType { + return $this->type; + } + /** * Returns the title of the marketing campaign. * @@ -93,18 +109,4 @@ class MarketingCampaign implements JsonSerializable { public function get_cost(): ?Price { return $this->cost; } - - /** - * Serialize the marketing campaign data. - * - * @return array - */ - public function jsonSerialize() { - return [ - 'id' => $this->get_id(), - 'title' => $this->get_title(), - 'manage_url' => $this->get_manage_url(), - 'cost' => $this->get_cost(), - ]; - } } diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php new file mode 100644 index 00000000000..032dc6f249d --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaignType.php @@ -0,0 +1,130 @@ +id = $id; + $this->channel = $channel; + $this->name = $name; + $this->description = $description; + $this->create_url = $create_url; + $this->icon_url = $icon_url; + } + + /** + * Returns the marketing campaign's unique identifier. + * + * @return string + */ + public function get_id(): string { + return $this->id; + } + + /** + * Returns the marketing channel that this campaign type belongs to. + * + * @return MarketingChannelInterface + */ + public function get_channel(): MarketingChannelInterface { + return $this->channel; + } + + /** + * Returns the name of the marketing campaign type. + * + * @return string + */ + public function get_name(): string { + return $this->name; + } + + /** + * Returns the description of the marketing campaign type. + * + * @return string + */ + public function get_description(): string { + return $this->description; + } + + /** + * Returns the URL to the create campaign page. + * + * @return string + */ + public function get_create_url(): string { + return $this->create_url; + } + + /** + * Returns the URL to an image/icon for the campaign type. + * + * @return string + */ + public function get_icon_url(): string { + return $this->icon_url; + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php index b2dbc3819a2..6794c4b4259 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -73,6 +73,13 @@ interface MarketingChannelInterface { */ public function get_errors_count(): int; + /** + * Returns an array of marketing campaign types that the channel supports. + * + * @return MarketingCampaignType[] Array of marketing campaign type objects. + */ + public function get_supported_campaign_types(): array; + /** * Returns an array of the channel's marketing campaigns. * diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php index 786aeb33b5f..61cce159181 100644 --- a/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php @@ -37,6 +37,15 @@ class MarketingChannels { $this->registered_channels[ $channel->get_slug() ] = $channel; } + /** + * Unregisters all marketing channels. + * + * @return void + */ + public function unregister_all(): void { + unset( $this->registered_channels ); + } + /** * Returns an array of all registered marketing channels. * diff --git a/plugins/woocommerce/src/Admin/Marketing/Price.php b/plugins/woocommerce/src/Admin/Marketing/Price.php index 9dbb00837ae..961228d5730 100644 --- a/plugins/woocommerce/src/Admin/Marketing/Price.php +++ b/plugins/woocommerce/src/Admin/Marketing/Price.php @@ -5,14 +5,12 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use JsonSerializable; - /** * Price class * * @since x.x.x */ -class Price implements JsonSerializable { +class Price { /** * The price. * @@ -55,16 +53,4 @@ class Price implements JsonSerializable { public function get_currency(): string { return $this->currency; } - - /** - * Serialize the price data. - * - * @return array - */ - public function jsonSerialize() { - return [ - 'value' => $this->get_value(), - 'currency' => $this->get_currency(), - ]; - } } diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php index ee36ba1375c..e6ed4e875d9 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php @@ -28,6 +28,20 @@ class MarketingSpecs { */ const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base'; + /** + * Slug of the category specifying marketing extensions on the WooCommerce.com store. + * + * @var string + */ + const MARKETING_EXTENSION_CATEGORY_SLUG = 'marketing'; + + /** + * Slug of the subcategory specifying marketing channels on the WooCommerce.com store. + * + * @var string + */ + const MARKETING_CHANNEL_SUBCATEGORY_SLUG = 'sales-channels'; + /** * Load recommended plugins from WooCommerce.com * @@ -61,6 +75,64 @@ class MarketingSpecs { return array_values( $plugins ); } + /** + * Return only the recommended marketing channels from WooCommerce.com. + * + * @return array + */ + public function get_recommended_marketing_channels(): array { + return array_filter( $this->get_recommended_plugins(), [ $this, 'is_marketing_channel_plugin' ] ); + } + + /** + * Return all recommended marketing extensions EXCEPT the marketing channels from WooCommerce.com. + * + * @return array + */ + public function get_recommended_marketing_extensions_excluding_channels(): array { + return array_filter( + $this->get_recommended_plugins(), + function ( array $plugin_data ) { + return $this->is_marketing_plugin( $plugin_data ) && ! $this->is_marketing_channel_plugin( $plugin_data ); + } + ); + } + + /** + * Returns whether a plugin is a marketing extension. + * + * @param array $plugin_data The plugin properties returned by the API. + * + * @return bool + */ + protected function is_marketing_plugin( array $plugin_data ): bool { + $categories = $plugin_data['categories'] ?? []; + + return in_array( self::MARKETING_EXTENSION_CATEGORY_SLUG, $categories, true ); + } + + /** + * Returns whether a plugin is a marketing channel. + * + * @param array $plugin_data The plugin properties returned by the API. + * + * @return bool + */ + protected function is_marketing_channel_plugin( array $plugin_data ): bool { + if ( ! $this->is_marketing_plugin( $plugin_data ) ) { + return false; + } + + $subcategories = $plugin_data['subcategories'] ?? []; + foreach ( $subcategories as $subcategory ) { + if ( isset( $subcategory['slug'] ) && self::MARKETING_CHANNEL_SUBCATEGORY_SLUG === $subcategory['slug'] ) { + return true; + } + } + + return false; + } + /** * Load knowledge base posts from WooCommerce.com * diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php new file mode 100644 index 00000000000..f38b49e1bb5 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignTypesTest.php @@ -0,0 +1,101 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. + */ + public function test_returns_aggregated_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Create a mock marketing campaign type. + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-type-1' ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_1 ); + // Return the sample campaign type by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_supported_campaign_types' )->willReturn( [ $test_campaign_type_1 ] ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + // Create a second mock marketing channel. + $test_channel_2 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_2->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-2' ); + // Create a mock marketing campaign type for the second marketing channel. + $test_campaign_type_2 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-type-2' ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_2 ); + // Return the sample campaign by the second mock marketing channel. + $test_channel_2->expects( $this->any() )->method( 'get_supported_campaign_types' )->willReturn( [ $test_campaign_type_2 ] ); + // Register the second marketing channel. + $this->marketing_channels_service->register( $test_channel_2 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( + [ + 'test-campaign-type-1', + 'test-campaign-type-2', + ], + array_column( $data, 'id' ) + ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php new file mode 100644 index 00000000000..341799b6127 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingCampaignsTest.php @@ -0,0 +1,155 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the marketing campaigns for all registered channels are aggregated and returned by the endpoint. + */ + public function test_returns_aggregated_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Create a mock marketing campaign type. + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_1->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_1 ); + // Create a mock marketing campaign. + $test_campaign_1 = $this->createMock( MarketingCampaign::class ); + $test_campaign_1->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-1' ); + $test_campaign_1->expects( $this->any() )->method( 'get_type' )->willReturn( $test_campaign_type_1 ); + // Return the sample campaign by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_campaigns' )->willReturn( [ $test_campaign_1 ] ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + // Create a second mock marketing channel. + $test_channel_2 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_2->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-2' ); + // Create a mock marketing campaign type for the second marketing channel. + $test_campaign_type_2 = $this->createMock( MarketingCampaignType::class ); + $test_campaign_type_2->expects( $this->any() )->method( 'get_channel' )->willReturn( $test_channel_2 ); + // Create a mock marketing campaign for the second marketing channel. + $test_campaign_2 = $this->createMock( MarketingCampaign::class ); + $test_campaign_2->expects( $this->any() )->method( 'get_id' )->willReturn( 'test-campaign-2' ); + $test_campaign_2->expects( $this->any() )->method( 'get_type' )->willReturn( $test_campaign_type_2 ); + // Return the sample campaign by the second mock marketing channel. + $test_channel_2->expects( $this->any() )->method( 'get_campaigns' )->willReturn( [ $test_campaign_2 ] ); + // Register the second marketing channel. + $this->marketing_channels_service->register( $test_channel_2 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( + [ + 'test-campaign-1', + 'test-campaign-2', + ], + array_column( $data, 'id' ) + ); + $this->assertEquals( + [ + 'test-channel-1', + 'test-channel-2', + ], + array_column( $data, 'channel' ) + ); + } + + /** + * Tests that the marketing campaigns are paginated and then returned by the endpoint. + */ + public function test_paginates_marketing_campaigns() { + // Create a mock marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + // Return mock campaigns by the mock marketing channel. + $test_channel_1->expects( $this->any() )->method( 'get_campaigns' )->willReturn( + [ + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + $this->createMock( MarketingCampaign::class ), + ] + ); + // Register the marketing channel. + $this->marketing_channels_service->register( $test_channel_1 ); + + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( + [ + 'page' => '1', + 'per_page' => '2', + ] + ); + $response = $this->server->dispatch( $request ); + $headers = $response->get_headers(); + + $this->assertCount( 2, $response->get_data() ); + + $this->assertArrayHasKey( 'Link', $headers ); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + $this->assertEquals( 5, $headers['X-WP-Total'] ); + $this->assertEquals( 3, $headers['X-WP-TotalPages'] ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php new file mode 100644 index 00000000000..9159b977ba4 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingChannelsTest.php @@ -0,0 +1,77 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + $this->marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class ); + } + + /** + * Test teardown. + */ + public function tearDown(): void { + $this->marketing_channels_service->unregister_all(); + parent::tearDown(); + } + + /** + * Tests that the registered marketing channels are returned by the endpoint. + */ + public function test_returns_registered_marketing_channels() { + // Register marketing channel. + $test_channel_1 = $this->createMock( MarketingChannelInterface::class ); + $test_channel_1->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + $test_channel_1->expects( $this->any() )->method( 'get_name' )->willReturn( 'Test Channel One' ); + $this->marketing_channels_service->register( $test_channel_1 ); + + $request = new WP_REST_Request( 'GET', self::ENDPOINT ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'test-channel-1', $data[0]['slug'] ); + $this->assertEquals( 'Test Channel One', $data[0]['name'] ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php new file mode 100644 index 00000000000..46335da52e3 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/API/MarketingRecommendationsTest.php @@ -0,0 +1,138 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + + set_transient( + MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, + [ + [ + 'title' => 'Example Marketing Channel', + 'description' => 'List your products and create ads, etc.', + 'url' => 'https://woocommerce.com/products/example-channel', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example.svg', + 'product' => 'example-channel', + 'plugin' => 'example-channel/example-channel.php', + 'categories' => [ MarketingSpecs::MARKETING_EXTENSION_CATEGORY_SLUG ], + 'subcategories' => [ + [ + 'slug' => MarketingSpecs::MARKETING_CHANNEL_SUBCATEGORY_SLUG, + 'name' => 'Sales channels', + ], + ], + 'tags' => [], + ], + [ + 'title' => 'Example Marketing Extension', + 'description' => 'Automate your customer communications, etc.', + 'url' => 'https://woocommerce.com/products/example-marketing-extension', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example-marketing-extension.svg', + 'product' => 'example-marketing-extension', + 'plugin' => 'example-marketing-extension/example-marketing-extension.php', + 'categories' => [ MarketingSpecs::MARKETING_EXTENSION_CATEGORY_SLUG ], + 'subcategories' => [ + [ + 'slug' => 'email', + 'name' => 'Email', + ], + ], + 'tags' => [], + ], + [ + 'title' => 'Example NON Marketing Extension', + 'description' => 'Handle coupons, etc.', + 'url' => 'https://woocommerce.com/products/example-random-extension', + 'direct_install' => true, + 'icon' => 'https://woocommerce.com/example-random-extension.svg', + 'product' => 'example-random-extension', + 'plugin' => 'example-random-extension/example-random-extension.php', + 'categories' => [ 'coupons' ], + 'subcategories' => [], + 'tags' => [], + ], + ] + ); + } + + /** + * Tests that the marketing channel recommendations are returned by the endpoint. + */ + public function test_returns_recommended_marketing_channels() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'channels' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'Example Marketing Channel', $data[0]['title'] ); + $this->assertEquals( 'example-channel', $data[0]['product'] ); + } + + /** + * Tests that the marketing extension recommendations are returned by the endpoint. + */ + public function test_returns_recommended_marketing_extensions() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'extensions' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( 1, $data ); + $this->assertEquals( 'Example Marketing Extension', $data[0]['title'] ); + $this->assertEquals( 'example-marketing-extension', $data[0]['product'] ); + } + + /** + * Tests that the endpoint returns an error if the provided category is invalid. + */ + public function test_returns_error_if_invalid_category_provided() { + $endpoint = self::ENDPOINT; + $request = new WP_REST_Request( 'GET', $endpoint ); + $request->set_query_params( [ 'category' => 'test-non-existing-invalid-category' ] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'rest_invalid_param', $data['code'] ); + } + +} diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php index 401e1630294..4277cff9437 100644 --- a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php @@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Tests\Admin\Marketing; use Automattic\WooCommerce\Admin\Marketing\MarketingCampaign; +use Automattic\WooCommerce\Admin\Marketing\MarketingCampaignType; use Automattic\WooCommerce\Admin\Marketing\Price; use WC_Unit_Test_Case; @@ -12,12 +13,15 @@ use WC_Unit_Test_Case; class MarketingCampaignTest extends WC_Unit_Test_Case { /** - * @testdox `get_id`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. + * @testdox `get_id`, `get_type`, `get_title`, `get_manage_url`, and `get_cost` return the class properties set by the constructor. */ public function test_get_methods_return_properties() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_campaign_type_1, 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); $this->assertEquals( '1234', $marketing_campaign->get_id() ); + $this->assertEquals( $test_campaign_type_1, $marketing_campaign->get_type() ); $this->assertEquals( 'Ad #1234', $marketing_campaign->get_title() ); $this->assertEquals( 'https://example.com/manage-campaigns', $marketing_campaign->get_manage_url() ); $this->assertNotNull( $marketing_campaign->get_cost() ); @@ -29,30 +33,10 @@ class MarketingCampaignTest extends WC_Unit_Test_Case { * @testdox `cost` property can be null. */ public function test_cost_can_be_null() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns' ); + $test_campaign_type_1 = $this->createMock( MarketingCampaignType::class ); + + $marketing_campaign = new MarketingCampaign( '1234', $test_campaign_type_1, 'Ad #1234', 'https://example.com/manage-campaigns' ); $this->assertNull( $marketing_campaign->get_cost() ); } - - /** - * @testdox It can be serialized to JSON including all its properties. - */ - public function test_can_be_serialized_to_json() { - $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); - - $json = wp_json_encode( $marketing_campaign ); - $this->assertNotEmpty( $json ); - $this->assertEqualSets( - [ - 'id' => $marketing_campaign->get_id(), - 'title' => $marketing_campaign->get_title(), - 'manage_url' => $marketing_campaign->get_manage_url(), - 'cost' => [ - 'value' => $marketing_campaign->get_cost()->get_value(), - 'currency' => $marketing_campaign->get_cost()->get_currency(), - ], - ], - json_decode( $json, true ) - ); - } } From 7e27c0d67ac1aab6b2d19969a2f20e1acca24df8 Mon Sep 17 00:00:00 2001 From: Nima Date: Mon, 16 Jan 2023 17:12:16 +0000 Subject: [PATCH 58/84] Add changelog --- .../changelog/feature-34548-multichannel-marketing-backend | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/feature-34548-multichannel-marketing-backend diff --git a/plugins/woocommerce/changelog/feature-34548-multichannel-marketing-backend b/plugins/woocommerce/changelog/feature-34548-multichannel-marketing-backend new file mode 100644 index 00000000000..c23bfc56f5a --- /dev/null +++ b/plugins/woocommerce/changelog/feature-34548-multichannel-marketing-backend @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add multichannel marketing API From f616d688d87a21bd36b64b5edcbac89f9d4f110c Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 18 Jan 2023 02:04:42 +0800 Subject: [PATCH 59/84] Call marketing channels API with wp.data. --- .../data-multichannel/action-types.ts | 4 + .../marketing/data-multichannel/actions.ts | 24 ++++++ .../marketing/data-multichannel/constants.ts | 2 + .../marketing/data-multichannel/guards.ts | 14 ++++ .../marketing/data-multichannel/index.ts | 26 +++++++ .../marketing/data-multichannel/reducer.ts | 43 +++++++++++ .../marketing/data-multichannel/resolvers.ts | 28 +++++++ .../marketing/data-multichannel/selectors.ts | 8 ++ .../marketing/data-multichannel/types.ts | 27 +++++++ .../marketing/hooks/useRegisteredChannels.ts | 77 ++++++++++++++----- .../MarketingOverviewMultichannel.tsx | 1 + 11 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts create mode 100644 plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts new file mode 100644 index 00000000000..06c9e42a1db --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts @@ -0,0 +1,4 @@ +export const TYPES = { + RECEIVE_CHANNELS_SUCCESS: 'RECEIVE_CHANNELS_SUCCESS' as const, + RECEIVE_CHANNELS_ERROR: 'RECEIVE_CHANNELS_ERROR' as const, +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts new file mode 100644 index 00000000000..2e0986b0f3d --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts @@ -0,0 +1,24 @@ +/** + * Internal dependencies + */ +import { TYPES } from './action-types'; +import { ApiFetchError, Channel } from './types'; + +export const receiveChannelsSuccess = ( channels: Array< Channel > ) => { + return { + type: TYPES.RECEIVE_CHANNELS_SUCCESS, + payload: channels, + }; +}; + +export const receiveChannelsError = ( error: ApiFetchError ) => { + return { + type: TYPES.RECEIVE_CHANNELS_ERROR, + payload: error, + error: true, + }; +}; + +export type Action = ReturnType< + typeof receiveChannelsSuccess | typeof receiveChannelsError +>; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts new file mode 100644 index 00000000000..e57d9c8406e --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts @@ -0,0 +1,2 @@ +export const STORE_KEY = 'wc/marketing-multichannel'; +export const API_NAMESPACE = '/wc-admin/marketing'; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts new file mode 100644 index 00000000000..b8906cf2ed9 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import { ApiFetchError } from './types'; + +export const isObject = ( obj: unknown ): obj is Record< string, unknown > => { + return !! obj && typeof obj === 'object'; +}; + +export const isApiFetchError = ( obj: unknown ): obj is ApiFetchError => { + return ( + isObject( obj ) && 'code' in obj && 'data' in obj && 'message' in obj + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts new file mode 100644 index 00000000000..1c213637e2f --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { createReduxStore, register } from '@wordpress/data'; +import { Reducer, AnyAction } from 'redux'; +import { controls } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import { State } from './types'; +import { STORE_KEY } from './constants'; +import { reducer } from './reducer'; +import * as actions from './actions'; +import * as selectors from './selectors'; +import * as resolvers from './resolvers'; + +const store = createReduxStore( STORE_KEY, { + reducer: reducer as Reducer< State, AnyAction >, + actions, + selectors, + resolvers, + controls, +} ); + +register( store ); diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts new file mode 100644 index 00000000000..b6a643402a8 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ + +import type { Reducer } from 'redux'; + +/** + * Internal dependencies + */ +import { State } from './types'; +import { Action } from './actions'; +import { TYPES } from './action-types'; + +const initialState = { + channels: { + data: undefined, + error: undefined, + }, +}; + +export const reducer: Reducer< State, Action > = ( + state = initialState, + action +) => { + switch ( action.type ) { + case TYPES.RECEIVE_CHANNELS_SUCCESS: + return { + ...state, + channels: { + data: action.payload, + }, + }; + case TYPES.RECEIVE_CHANNELS_ERROR: + return { + ...state, + channels: { + error: action.payload, + }, + }; + default: + return state; + } +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts new file mode 100644 index 00000000000..cf55b4b1790 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts @@ -0,0 +1,28 @@ +/** + * External dependencies + */ +import { apiFetch } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import { receiveChannelsSuccess, receiveChannelsError } from './actions'; +import { Channel } from './types'; +import { API_NAMESPACE } from './constants'; +import { isApiFetchError } from './guards'; + +export function* getChannels() { + try { + const data: Channel[] = yield apiFetch( { + path: `${ API_NAMESPACE }/channels`, + } ); + + yield receiveChannelsSuccess( data ); + } catch ( error ) { + if ( isApiFetchError( error ) ) { + yield receiveChannelsError( error ); + } + + throw error; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts new file mode 100644 index 00000000000..6626e622a1b --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { State } from './types'; + +export const getChannels = ( state: State ) => { + return state.channels; +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts new file mode 100644 index 00000000000..1e213b31f54 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts @@ -0,0 +1,27 @@ +export type ApiFetchError = { + code: string; + data: { + status: number; + }; + message: string; +}; + +export type Channel = { + slug: string; + is_setup_completed: boolean; + settings_url: string; + name: string; + description: string; + product_listings_status: string; + errors_count: number; + icon: string; +}; + +export type Channels = { + data?: Array< Channel >; + error?: ApiFetchError; +}; + +export type State = { + channels: Channels; +}; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts index d512f1d4d32..73b77ebb187 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -1,11 +1,19 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + /** * Internal dependencies */ import { InstalledChannel } from '~/marketing/types'; +import { STORE_KEY } from '~/marketing/data-multichannel/constants'; +import { ApiFetchError, Channel, Channels } from '../data-multichannel/types'; type UseRegisteredChannels = { loading: boolean; data: Array< InstalledChannel >; + error?: ApiFetchError; }; // // TODO: To be removed. This is for testing loading state. @@ -66,26 +74,55 @@ type UseRegisteredChannels = { // }; // }; -// TODO: To be removed. This is for testing everything works okay. -export const useRegisteredChannels = (): UseRegisteredChannels => { - // TODO: call API here to get data. - // The following are just dummy data for testing now. +// // TODO: To be removed. This is for testing everything works okay. +// export const useRegisteredChannels = (): UseRegisteredChannels => { +// // TODO: call API here to get data. +// // The following are just dummy data for testing now. +// return { +// loading: false, +// data: [ +// { +// slug: 'google-listings-and-ads', +// title: 'Google Listings and Ads', +// description: +// 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', +// icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', +// isSetupCompleted: true, +// setupUrl: 'https://www.example.com/setup', +// manageUrl: 'https://www.example.com/manage', +// syncStatus: 'synced' as const, +// issueType: 'none' as const, +// issueText: 'No issues to resolve', +// }, +// ], +// }; +// }; + +const convert = ( data: Channel ): InstalledChannel => { + // TODO: map all the fields correctly from API to UI. return { - loading: false, - data: [ - { - slug: 'google-listings-and-ads', - title: 'Google Listings and Ads', - description: - 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', - isSetupCompleted: true, - setupUrl: 'https://www.example.com/setup', - manageUrl: 'https://www.example.com/manage', - syncStatus: 'synced' as const, - issueType: 'none' as const, - issueText: 'No issues to resolve', - }, - ], + slug: data.slug, + title: data.name, + description: data.description, + icon: data.icon, + isSetupCompleted: data.is_setup_completed, + setupUrl: data.settings_url, + manageUrl: data.settings_url, + syncStatus: 'synced', + issueType: 'none', + issueText: '', }; }; + +export const useRegisteredChannels = (): UseRegisteredChannels => { + return useSelect( ( select ) => { + const { hasFinishedResolution, getChannels } = select( STORE_KEY ); + const channels = getChannels< Channels >(); + + return { + loading: ! hasFinishedResolution( 'getChannels' ), + data: channels.data?.map( convert ) || [], + error: channels.error, + }; + } ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index 7cbad05d6a2..a34e1a84f67 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -12,6 +12,7 @@ import { InstalledExtensions } from './InstalledExtensions'; import { DiscoverTools } from './DiscoverTools'; import { LearnMarketing } from './LearnMarketing'; import '~/marketing/data'; +import '~/marketing/data-multichannel'; import { useRegisteredChannels, useRecommendedChannels, From 0a36d7a4fe621ecc1904d70070fa8a0c9ab465c1 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 01:12:50 +0800 Subject: [PATCH 60/84] Map errors_count to issueType and issueText. --- .../marketing/hooks/useRegisteredChannels.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts index 73b77ebb187..8399a7a04f1 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -2,6 +2,7 @@ * External dependencies */ import { useSelect } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -100,6 +101,17 @@ type UseRegisteredChannels = { const convert = ( data: Channel ): InstalledChannel => { // TODO: map all the fields correctly from API to UI. + + const issueType = data.errors_count >= 1 ? 'error' : 'none'; + const issueText = + data.errors_count >= 1 + ? sprintf( + // translators: %d: The number of issues to resolve. + __( '%d issues to resolve', 'woocommerce' ), + data.errors_count + ) + : __( 'No issues to resolve', 'woocommerce' ); + return { slug: data.slug, title: data.name, @@ -109,8 +121,8 @@ const convert = ( data: Channel ): InstalledChannel => { setupUrl: data.settings_url, manageUrl: data.settings_url, syncStatus: 'synced', - issueType: 'none', - issueText: '', + issueType, + issueText, }; }; From 2d7c8db2e5b49c2d268e3a2d43605c777ec89d35 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 01:32:38 +0800 Subject: [PATCH 61/84] Map product_listings_status to syncStatus. --- .../marketing/hooks/useRegisteredChannels.ts | 17 ++++++++++++----- .../Channels/InstalledChannelCardBody.tsx | 8 ++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts index 8399a7a04f1..d23fc705ca8 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -7,9 +7,13 @@ import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ -import { InstalledChannel } from '~/marketing/types'; +import { InstalledChannel, SyncStatusType } from '~/marketing/types'; import { STORE_KEY } from '~/marketing/data-multichannel/constants'; -import { ApiFetchError, Channel, Channels } from '../data-multichannel/types'; +import { + ApiFetchError, + Channel, + Channels, +} from '~/marketing/data-multichannel/types'; type UseRegisteredChannels = { loading: boolean; @@ -99,9 +103,12 @@ type UseRegisteredChannels = { // }; // }; -const convert = ( data: Channel ): InstalledChannel => { - // TODO: map all the fields correctly from API to UI. +const statusMap: Record< string, SyncStatusType > = { + synced: 'synced', + 'sync-in-progress': 'syncing', +}; +const convert = ( data: Channel ): InstalledChannel => { const issueType = data.errors_count >= 1 ? 'error' : 'none'; const issueText = data.errors_count >= 1 @@ -120,7 +127,7 @@ const convert = ( data: Channel ): InstalledChannel => { isSetupCompleted: data.is_setup_completed, setupUrl: data.settings_url, manageUrl: data.settings_url, - syncStatus: 'synced', + syncStatus: statusMap[ data.product_listings_status ], issueType, issueText, }; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx index abd7f85dc5f..9be8d93c417 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/InstalledChannelCardBody.tsx @@ -115,8 +115,12 @@ export const InstalledChannelCardBody: React.FC< installedChannel.description ) : (
- -
+ { installedChannel.syncStatus && ( + <> + +
+ + ) }
); From b708cc18ff0a7cdc046853822ab240b500fc7649 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 19:33:51 +0800 Subject: [PATCH 62/84] Get recommended channels from API. --- .../data-multichannel/action-types.ts | 4 + .../marketing/data-multichannel/actions.ts | 24 ++- .../marketing/data-multichannel/reducer.ts | 18 +++ .../marketing/data-multichannel/resolvers.ts | 25 ++- .../marketing/data-multichannel/selectors.ts | 4 + .../marketing/data-multichannel/types.ts | 32 ++++ .../marketing/hooks/useRecommendedChannels.ts | 153 +++--------------- 7 files changed, 124 insertions(+), 136 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts index 06c9e42a1db..c53be7bbd49 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts @@ -1,4 +1,8 @@ export const TYPES = { RECEIVE_CHANNELS_SUCCESS: 'RECEIVE_CHANNELS_SUCCESS' as const, RECEIVE_CHANNELS_ERROR: 'RECEIVE_CHANNELS_ERROR' as const, + RECEIVE_RECOMMENDED_CHANNELS_SUCCESS: + 'RECEIVE_RECOMMENDED_CHANNELS_SUCCESS' as const, + RECEIVE_RECOMMENDED_CHANNELS_ERROR: + 'RECEIVE_RECOMMENDED_CHANNELS_ERROR' as const, }; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts index 2e0986b0f3d..74785dbc5ce 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts @@ -2,7 +2,7 @@ * Internal dependencies */ import { TYPES } from './action-types'; -import { ApiFetchError, Channel } from './types'; +import { ApiFetchError, Channel, RecommendedPlugin } from './types'; export const receiveChannelsSuccess = ( channels: Array< Channel > ) => { return { @@ -19,6 +19,26 @@ export const receiveChannelsError = ( error: ApiFetchError ) => { }; }; +export const receiveRecommendedChannelsSuccess = ( + channels: Array< RecommendedPlugin > +) => { + return { + type: TYPES.RECEIVE_RECOMMENDED_CHANNELS_SUCCESS, + payload: channels, + }; +}; + +export const receiveRecommendedChannelsError = ( error: ApiFetchError ) => { + return { + type: TYPES.RECEIVE_RECOMMENDED_CHANNELS_ERROR, + payload: error, + error: true, + }; +}; + export type Action = ReturnType< - typeof receiveChannelsSuccess | typeof receiveChannelsError + | typeof receiveChannelsSuccess + | typeof receiveChannelsError + | typeof receiveRecommendedChannelsSuccess + | typeof receiveRecommendedChannelsError >; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts index b6a643402a8..d9e95181bf6 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts @@ -16,6 +16,10 @@ const initialState = { data: undefined, error: undefined, }, + recommendedChannels: { + data: undefined, + error: undefined, + }, }; export const reducer: Reducer< State, Action > = ( @@ -37,6 +41,20 @@ export const reducer: Reducer< State, Action > = ( error: action.payload, }, }; + case TYPES.RECEIVE_RECOMMENDED_CHANNELS_SUCCESS: + return { + ...state, + recommendedChannels: { + data: action.payload, + }, + }; + case TYPES.RECEIVE_RECOMMENDED_CHANNELS_ERROR: + return { + ...state, + recommendedChannels: { + error: action.payload, + }, + }; default: return state; } diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts index cf55b4b1790..deb68f3b9cf 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts @@ -6,8 +6,13 @@ import { apiFetch } from '@wordpress/data-controls'; /** * Internal dependencies */ -import { receiveChannelsSuccess, receiveChannelsError } from './actions'; -import { Channel } from './types'; +import { + receiveChannelsSuccess, + receiveChannelsError, + receiveRecommendedChannelsSuccess, + receiveRecommendedChannelsError, +} from './actions'; +import { Channel, RecommendedPlugin } from './types'; import { API_NAMESPACE } from './constants'; import { isApiFetchError } from './guards'; @@ -26,3 +31,19 @@ export function* getChannels() { throw error; } } + +export function* getRecommendedChannels() { + try { + const data: RecommendedPlugin[] = yield apiFetch( { + path: `${ API_NAMESPACE }/recommendations?category=channels`, + } ); + + yield receiveRecommendedChannelsSuccess( data ); + } catch ( error ) { + if ( isApiFetchError( error ) ) { + yield receiveRecommendedChannelsError( error ); + } + + throw error; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts index 6626e622a1b..c37bb8599ad 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts @@ -6,3 +6,7 @@ import { State } from './types'; export const getChannels = ( state: State ) => { return state.channels; }; + +export const getRecommendedChannels = ( state: State ) => { + return state.recommendedChannels; +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts index 1e213b31f54..9f412d0b445 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts @@ -22,6 +22,38 @@ export type Channels = { error?: ApiFetchError; }; +// TODO: The following types are copied from plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/types.ts. +// They are may be changed later, depending on the outcome of API development. + +type Subcategory = { + slug: string; + name: string; +}; + +type Tag = { + slug: string; + name: string; +}; + +export type RecommendedPlugin = { + title: string; + description: string; + url: string; + direct_install: boolean; + icon: string; + product: string; + plugin: string; + categories: Array< string >; + subcategories: Array< Subcategory >; + tags: Array< Tag >; +}; + +export type RecommendedChannels = { + data?: Array< RecommendedPlugin >; + error?: ApiFetchError; +}; + export type State = { channels: Channels; + recommendedChannels: RecommendedChannels; }; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts index 88982ec5bed..f8c03b163da 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts @@ -1,144 +1,33 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + /** * Internal dependencies */ +import { STORE_KEY } from '~/marketing/data-multichannel/constants'; import { RecommendedChannel } from '~/marketing/types'; +import { RecommendedChannels } from '~/marketing/data-multichannel/types'; type UseRecommendedChannels = { loading: boolean; data: Array< RecommendedChannel >; }; -// // TODO: to be removed. This is to test for loading state. -// export const useRecommendedChannels = (): UseRecommendedChannels => { -// // TODO: call API here to get data. -// // The following are just dummy data for testing now. -// return { -// loading: true, -// data: [], -// }; -// }; - -// // TODO: to be removed. This is to test for empty data. -// export const useRecommendedChannels = (): UseRecommendedChannels => { -// TODO: call API here to get data. -// The following are just dummy data for testing now. -// return { -// loading: false, -// data: [], -// }; -// }; - export const useRecommendedChannels = (): UseRecommendedChannels => { - // TODO: call API here to get data. - // The following are just dummy data for testing now. - return { - loading: false, - data: [ - { - title: 'Facebook for WooCommerce', - description: - 'List your products and create ads on Facebook and Instagram.', - url: 'https://woocommerce.com/products/facebook/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', - direct_install: true, - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/facebook.svg', - product: 'facebook-for-woocommerce', - plugin: 'facebook-for-woocommerce/facebook-for-woocommerce.php', - categories: [ 'marketing' ], - subcategories: [ - { - slug: 'sales-channels', - name: 'Sales channels', - }, - ], - tags: [ - { - slug: 'built-by-woocommerce', - name: 'Built by WooCommerce', - }, - ], - }, - { - title: 'Google Listings and Ads', - description: - 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', - url: 'https://woocommerce.com/products/google-listings-and-ads/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', - direct_install: true, - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/google.svg', - product: 'google-listings-and-ads', - plugin: 'google-listings-and-ads/google-listings-and-ads.php', - categories: [ 'marketing' ], - subcategories: [ - { - slug: 'sales-channels', - name: 'Sales channels', - }, - ], - tags: [ - { - slug: 'built-by-woocommerce', - name: 'Built by WooCommerce', - }, - ], - }, - { - title: 'Pinterest for WooCommerce', - description: - 'Grow your business on Pinterest! Use this official plugin to allow shoppers to Pin products while browsing your store, track conversions, and advertise on Pinterest.', - url: 'https://woocommerce.com/products/pinterest-for-woocommerce/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', - direct_install: true, - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/pinterest.svg', - product: 'pinterest-for-woocommerce', - plugin: 'pinterest-for-woocommerce/pinterest-for-woocommerce.php', - categories: [ 'marketing' ], - subcategories: [ - { - slug: 'sales-channels', - name: 'Sales channels', - }, - ], - tags: [ - { - slug: 'built-by-woocommerce', - name: 'Built by WooCommerce', - }, - ], - }, - { - title: 'TikTok for WooCommerce', - description: - 'Create advertising campaigns and reach one billion global users with TikTok for WooCommerce.', - url: 'https://woocommerce.com/products/tiktok-for-woocommerce/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', - direct_install: true, - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/tiktok.jpg', - product: 'tiktok-for-business', - plugin: 'tiktok-for-business/tiktok-for-woocommerce.php', - categories: [ 'marketing' ], - subcategories: [ - { - slug: 'sales-channels', - name: 'Sales channels', - }, - ], - tags: [], - }, - { - title: 'Amazon, eBay & Walmart Integration for WooCommerce', - description: - 'Get the official Amazon, eBay and Walmart extension and create, sync and manage multichannel listings directly from WooCommerce.', - url: 'https://woocommerce.com/products/amazon-ebay-integration/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', - direct_install: false, - icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/amazon-ebay.svg', - product: 'amazon-ebay-integration', - plugin: 'woocommerce-amazon-ebay-integration/woocommerce-amazon-ebay-integration.php', - categories: [ 'marketing' ], - subcategories: [ - { - slug: 'sales-channels', - name: 'Sales channels', - }, - ], - tags: [], - }, - ], - }; + return useSelect( ( select ) => { + const { hasFinishedResolution, getRecommendedChannels } = + select( STORE_KEY ); + const channels = getRecommendedChannels< RecommendedChannels >(); + + // TODO: filter recommended channels against installed plugins, + // using @woocommerce/data/plugins. + + return { + loading: ! hasFinishedResolution( 'getChannels' ), + data: channels.data || [], + error: channels.error, + }; + } ); }; From cf4c62e7076f91f84fd0e43e45fba9ce1d9b1bc8 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 19:50:56 +0800 Subject: [PATCH 63/84] Fix loading bug in useRecommendedChannels. --- .../client/marketing/hooks/useRecommendedChannels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts index f8c03b163da..1058ae364a0 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts @@ -25,7 +25,7 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { // using @woocommerce/data/plugins. return { - loading: ! hasFinishedResolution( 'getChannels' ), + loading: ! hasFinishedResolution( 'getRecommendedChannels' ), data: channels.data || [], error: channels.error, }; From 223ea4d7127d0592d1e754339725f0109fe4688b Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 19 Jan 2023 22:23:00 +0800 Subject: [PATCH 64/84] Filter recommended channels to get "not installed" or "not activated" channels. --- .../marketing/hooks/useRecommendedChannels.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts index 1058ae364a0..7111f776d59 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts @@ -2,6 +2,8 @@ * External dependencies */ import { useSelect } from '@wordpress/data'; +import { PLUGINS_STORE_NAME } from '@woocommerce/data'; +import { differenceWith } from 'lodash'; /** * Internal dependencies @@ -19,15 +21,27 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { return useSelect( ( select ) => { const { hasFinishedResolution, getRecommendedChannels } = select( STORE_KEY ); - const channels = getRecommendedChannels< RecommendedChannels >(); + const { data, error } = getRecommendedChannels< RecommendedChannels >(); - // TODO: filter recommended channels against installed plugins, - // using @woocommerce/data/plugins. + const { getActivePlugins } = select( PLUGINS_STORE_NAME ); + const activePlugins = getActivePlugins(); + + /** + * Recommended channels that are not in "active" state, + * i.e. channels that are not installed or not activated yet. + */ + const nonActiveRecommendedChannels = differenceWith( + data, + activePlugins, + ( a, b ) => { + return a.product === b; + } + ); return { loading: ! hasFinishedResolution( 'getRecommendedChannels' ), - data: channels.data || [], - error: channels.error, + data: nonActiveRecommendedChannels, + error, }; } ); }; From 9fa6ea4a25ae393a033d7d469ce84f365eafb0ed Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 20 Jan 2023 00:36:34 +0800 Subject: [PATCH 65/84] Display activate button for channels that are not yet activated. --- .../PluginCardBody/SmartPluginCardBody.tsx | 15 +++++++++++ .../PluginCardBody/useIsPluginInstalled.ts | 25 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 plugins/woocommerce-admin/client/marketing/components/PluginCardBody/useIsPluginInstalled.ts diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx index f3272e48f78..b6375d82109 100644 --- a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx @@ -16,6 +16,7 @@ import { PluginCardBody } from '~/marketing/components'; import { RecommendedPlugin } from '~/marketing/types'; import { getInAppPurchaseUrl } from '~/lib/in-app-purchase'; import { createNoticesFromResponse } from '~/lib/notices'; +import { useIsPluginInstalled } from './useIsPluginInstalled'; import './PluginCardBody.scss'; type SmartPluginCardBodyProps = { @@ -38,6 +39,7 @@ export const SmartPluginCardBody = ( { null ); const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME ); + const { isPluginInstalled } = useIsPluginInstalled(); /** * Install and activate a plugin. @@ -71,6 +73,19 @@ export const SmartPluginCardBody = ( { const renderButton = () => { const buttonDisabled = !! currentPlugin; + if ( isPluginInstalled( plugin.product ) ) { + return ( + + ); + } + if ( plugin.direct_install ) { return (
diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx index d88bef12f29..9aeac78e5ec 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx @@ -14,17 +14,21 @@ import './Channels.scss'; type RecommendedChannelListPropsType = { recommendedChannels: Array< RecommendedChannel >; + onInstalledAndActivated?: () => void; }; export const RecommendedChannelsList: React.FC< RecommendedChannelListPropsType -> = ( { recommendedChannels } ) => { +> = ( { recommendedChannels, onInstalledAndActivated } ) => { return ( <> { recommendedChannels.map( ( el, idx ) => { return ( - + { idx < recommendedChannels.length - 1 && ( ) } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index a34e1a84f67..838a45ecbf0 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -21,8 +21,11 @@ import './MarketingOverviewMultichannel.scss'; import { CenteredSpinner } from '../components'; export const MarketingOverviewMultichannel: React.FC = () => { - const { loading: loadingRegistered, data: dataRegistered } = - useRegisteredChannels(); + const { + loading: loadingRegistered, + data: dataRegistered, + refetch, + } = useRegisteredChannels(); const { loading: loadingRecommended, data: dataRecommended } = useRecommendedChannels(); const { currentUserCan } = useUser(); @@ -31,18 +34,25 @@ export const MarketingOverviewMultichannel: React.FC = () => { getAdminSetting( 'allowMarketplaceSuggestions', false ) && currentUserCan( 'install_plugins' ); - if ( loadingRegistered || loadingRecommended ) { + if ( + ( loadingRegistered && ! dataRegistered ) || + ( loadingRecommended && ! dataRecommended ) + ) { return ; } return (
- { ( dataRegistered.length >= 1 || dataRecommended.length >= 1 ) && ( - - ) } + { dataRegistered && + dataRecommended && + ( dataRegistered.length >= 1 || + dataRecommended.length >= 1 ) && ( + + ) } { shouldShowExtensions && } From d69f078fd551aee5f44781169a619e9495dbe12f Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 20 Jan 2023 01:36:39 +0800 Subject: [PATCH 67/84] Replace useIsPluginInstalled with useIsPluginInstalledNotActivated. --- .../PluginCardBody/SmartPluginCardBody.tsx | 7 +++--- .../PluginCardBody/useIsPluginInstalled.ts | 25 ------------------- .../useIsPluginInstalledNotActivated.ts | 24 ++++++++++++++++++ 3 files changed, 28 insertions(+), 28 deletions(-) delete mode 100644 plugins/woocommerce-admin/client/marketing/components/PluginCardBody/useIsPluginInstalled.ts create mode 100644 plugins/woocommerce-admin/client/marketing/components/PluginCardBody/useIsPluginInstalledNotActivated.ts diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx index b6375d82109..32262e7139c 100644 --- a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx @@ -16,7 +16,7 @@ import { PluginCardBody } from '~/marketing/components'; import { RecommendedPlugin } from '~/marketing/types'; import { getInAppPurchaseUrl } from '~/lib/in-app-purchase'; import { createNoticesFromResponse } from '~/lib/notices'; -import { useIsPluginInstalled } from './useIsPluginInstalled'; +import { useIsPluginInstalledNotActivated } from './useIsPluginInstalledNotActivated'; import './PluginCardBody.scss'; type SmartPluginCardBodyProps = { @@ -39,7 +39,8 @@ export const SmartPluginCardBody = ( { null ); const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME ); - const { isPluginInstalled } = useIsPluginInstalled(); + const { isPluginInstalledNotActivated } = + useIsPluginInstalledNotActivated(); /** * Install and activate a plugin. @@ -73,7 +74,7 @@ export const SmartPluginCardBody = ( { const renderButton = () => { const buttonDisabled = !! currentPlugin; - if ( isPluginInstalled( plugin.product ) ) { + if ( isPluginInstalledNotActivated( plugin.product ) ) { return ( ) : ( - ); @@ -147,11 +147,11 @@ export const InstalledChannelCardBody: React.FC< className="woocommerce-marketing-installed-channel-card-body" icon={ { } - name={ installedChannel.title } + name={ registeredChannel.title } description={ description } button={ button } /> diff --git a/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts b/plugins/woocommerce-admin/client/marketing/types/RegisteredChannel.ts similarity index 90% rename from plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts rename to plugins/woocommerce-admin/client/marketing/types/RegisteredChannel.ts index c4e735089ac..24ddd4c6f54 100644 --- a/plugins/woocommerce-admin/client/marketing/types/InstalledChannel.ts +++ b/plugins/woocommerce-admin/client/marketing/types/RegisteredChannel.ts @@ -1,7 +1,7 @@ export type SyncStatusType = 'synced' | 'syncing' | 'failed'; export type IssueTypeType = 'error' | 'warning' | 'none'; -export type InstalledChannel = { +export type RegisteredChannel = { slug: string; title: string; description: string; diff --git a/plugins/woocommerce-admin/client/marketing/types/index.ts b/plugins/woocommerce-admin/client/marketing/types/index.ts index 24fbe739fd0..38327962d5d 100644 --- a/plugins/woocommerce-admin/client/marketing/types/index.ts +++ b/plugins/woocommerce-admin/client/marketing/types/index.ts @@ -3,5 +3,5 @@ export { RecommendedPlugin } from './RecommendedPlugin'; export { SyncStatusType, IssueTypeType, - InstalledChannel, -} from './InstalledChannel'; + RegisteredChannel, +} from './RegisteredChannel'; From 1af1524b83870a50cc1066c8a4e8a82e90ea53c9 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 21 Jan 2023 02:38:00 +0800 Subject: [PATCH 73/84] Rename CSS installed-channel to registered-channel. --- .../Channels/RegisteredChannelCardBody.scss | 4 ++-- .../Channels/RegisteredChannelCardBody.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss index 3a80fa7ef64..db8b73f9a24 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss @@ -1,5 +1,5 @@ -.woocommerce-marketing-installed-channel-card-body { - .woocommerce-marketing-installed-channel-description { +.woocommerce-marketing-registered-channel-card-body { + .woocommerce-marketing-registered-channel-description { display: flex; gap: $gap-smaller; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx index 9bdbfb871a3..f25b539a893 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx @@ -114,11 +114,11 @@ export const RegisteredChannelCardBody: React.FC< const description = ! registeredChannel.isSetupCompleted ? ( registeredChannel.description ) : ( -
+
{ registeredChannel.syncStatus && ( <> -
+
) } @@ -144,7 +144,7 @@ export const RegisteredChannelCardBody: React.FC< return ( Date: Fri, 27 Jan 2023 21:15:40 +0800 Subject: [PATCH 74/84] Simplify RegisteredChannelCardBody CSS. --- .../Channels/RegisteredChannelCardBody.scss | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss index db8b73f9a24..79766418107 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss @@ -18,14 +18,10 @@ fill: $alert-red; } - &__syncing { - color: #008a20; - fill: #008a20; - } - + &__syncing, &__synced { - color: #008a20; - fill: #008a20; + color: $studio-green-50; + fill: $studio-green-50; } } From 49032b98dbe182a70bb8453062fa75b19f279017 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Fri, 27 Jan 2023 21:38:38 +0800 Subject: [PATCH 75/84] Make data undefined on first load in useRecommendedChannels. --- .../client/marketing/hooks/useRecommendedChannels.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts index 8ebeed82534..0e69e63e2eb 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts @@ -16,7 +16,7 @@ import { type UseRecommendedChannels = { loading: boolean; - data: Array< RecommendedChannel >; + data?: Array< RecommendedChannel >; }; export const useRecommendedChannels = (): UseRecommendedChannels => { @@ -33,13 +33,11 @@ export const useRecommendedChannels = (): UseRecommendedChannels => { * Recommended channels that are not in "active" state, * i.e. channels that are not installed or not activated yet. */ - const nonActiveRecommendedChannels = differenceWith( - data, - activePlugins, - ( a, b ) => { + const nonActiveRecommendedChannels = + data && + differenceWith( data, activePlugins, ( a, b ) => { return a.product === b; - } - ); + } ); return { loading: ! hasFinishedResolution( 'getRecommendedChannels' ), From 14511ae4d6a7ebbce7d9a32d066a658691b4f410 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 28 Jan 2023 00:14:50 +0800 Subject: [PATCH 76/84] Code refactor with SyncStatus and IssueStatus components. --- .../Channels/IssueStatus.scss | 15 ++++ .../Channels/IssueStatus.tsx | 56 ++++++++++++ .../Channels/RegisteredChannelCardBody.scss | 33 ------- .../Channels/RegisteredChannelCardBody.tsx | 90 +------------------ .../Channels/SyncStatus.scss | 16 ++++ .../Channels/SyncStatus.tsx | 52 +++++++++++ .../Channels/iconSize.ts | 1 + 7 files changed, 143 insertions(+), 120 deletions(-) create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.scss create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.tsx create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.scss create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.tsx create mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/iconSize.ts diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.scss new file mode 100644 index 00000000000..df5c9b02811 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.scss @@ -0,0 +1,15 @@ +.woocommerce-marketing-issue-status { + display: flex; + align-items: center; + gap: $gap-smallest; + + &__error { + color: $alert-red; + fill: $alert-red; + } + + &__warning { + color: $alert-yellow; + fill: $alert-yellow; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.tsx new file mode 100644 index 00000000000..d0aee1202e3 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.tsx @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import GridiconNotice from 'gridicons/dist/notice'; +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import { RegisteredChannel } from '~/marketing/types'; +import { iconSize } from './iconSize'; +import './IssueStatus.scss'; + +type IssueStatusPropsType = { + registeredChannel: RegisteredChannel; +}; + +const issueStatusClassName = 'woocommerce-marketing-issue-status'; + +export const IssueStatus: React.FC< IssueStatusPropsType > = ( { + registeredChannel, +} ) => { + if ( registeredChannel.issueType === 'error' ) { + return ( +
+ + { registeredChannel.issueText } +
+ ); + } + + if ( registeredChannel.issueType === 'warning' ) { + return ( +
+ + { registeredChannel.issueText } +
+ ); + } + + return ( +
+ { registeredChannel.issueText } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss index 79766418107..50c4615fb25 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss @@ -7,37 +7,4 @@ content: '•'; } } - - .woocommerce-marketing-sync-status { - display: flex; - align-items: center; - gap: $gap-smallest; - - &__failed { - color: $alert-red; - fill: $alert-red; - } - - &__syncing, - &__synced { - color: $studio-green-50; - fill: $studio-green-50; - } - } - - .woocommerce-marketing-issue-status { - display: flex; - align-items: center; - gap: $gap-smallest; - - &__error { - color: $alert-red; - fill: $alert-red; - } - - &__warning { - color: $alert-yellow; - fill: $alert-yellow; - } - } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx index f25b539a893..2d4d9c007fa 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx @@ -3,104 +3,20 @@ */ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import GridiconCheckmarkCircle from 'gridicons/dist/checkmark-circle'; -import GridiconSync from 'gridicons/dist/sync'; -import GridiconNotice from 'gridicons/dist/notice'; -import classnames from 'classnames'; /** * Internal dependencies */ import { PluginCardBody } from '~/marketing/components'; -import { RegisteredChannel, SyncStatusType } from '~/marketing/types'; +import { RegisteredChannel } from '~/marketing/types'; +import { SyncStatus } from './SyncStatus'; +import { IssueStatus } from './IssueStatus'; import './RegisteredChannelCardBody.scss'; type RegisteredChannelCardBodyProps = { registeredChannel: RegisteredChannel; }; -type SyncStatusPropsType = { - status: SyncStatusType; -}; - -const iconSize = 18; -const className = 'woocommerce-marketing-sync-status'; - -const SyncStatus: React.FC< SyncStatusPropsType > = ( { status } ) => { - if ( status === 'failed' ) { - return ( -
- - { __( 'Sync failed', 'woocommerce' ) } -
- ); - } - - if ( status === 'syncing' ) { - return ( -
- - { __( 'Syncing', 'woocommerce' ) } -
- ); - } - - return ( -
- - { __( 'Synced', 'woocommerce' ) } -
- ); -}; - -type IssueStatusPropsType = { - registeredChannel: RegisteredChannel; -}; - -const issueStatusClassName = 'woocommerce-marketing-issue-status'; - -const IssueStatus: React.FC< IssueStatusPropsType > = ( { - registeredChannel, -} ) => { - if ( registeredChannel.issueType === 'error' ) { - return ( -
- - { registeredChannel.issueText } -
- ); - } - - if ( registeredChannel.issueType === 'warning' ) { - return ( -
- - { registeredChannel.issueText } -
- ); - } - - return ( -
- { registeredChannel.issueText } -
- ); -}; - export const RegisteredChannelCardBody: React.FC< RegisteredChannelCardBodyProps > = ( { registeredChannel } ) => { diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.scss new file mode 100644 index 00000000000..402b0163f80 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.scss @@ -0,0 +1,16 @@ +.woocommerce-marketing-sync-status { + display: flex; + align-items: center; + gap: $gap-smallest; + + &__failed { + color: $alert-red; + fill: $alert-red; + } + + &__syncing, + &__synced { + color: $studio-green-50; + fill: $studio-green-50; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.tsx new file mode 100644 index 00000000000..772dc8fcf75 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.tsx @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import GridiconCheckmarkCircle from 'gridicons/dist/checkmark-circle'; +import GridiconSync from 'gridicons/dist/sync'; +import GridiconNotice from 'gridicons/dist/notice'; +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import { SyncStatusType } from '~/marketing/types'; +import { iconSize } from './iconSize'; +import './SyncStatus.scss'; + +type SyncStatusPropsType = { + status: SyncStatusType; +}; + +const className = 'woocommerce-marketing-sync-status'; + +export const SyncStatus: React.FC< SyncStatusPropsType > = ( { status } ) => { + if ( status === 'failed' ) { + return ( +
+ + { __( 'Sync failed', 'woocommerce' ) } +
+ ); + } + + if ( status === 'syncing' ) { + return ( +
+ + { __( 'Syncing', 'woocommerce' ) } +
+ ); + } + + return ( +
+ + { __( 'Synced', 'woocommerce' ) } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/iconSize.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/iconSize.ts new file mode 100644 index 00000000000..ef79ab572ac --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/iconSize.ts @@ -0,0 +1 @@ +export const iconSize = 18; From 0eb318c1f5153844354d82515e57385fcd1a35fc Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 28 Jan 2023 01:12:36 +0800 Subject: [PATCH 77/84] Simplify and remove RecommendedChannels and RecommendedChannelsList components. --- .../Channels/Channels.scss | 7 ++ .../Channels/Channels.tsx | 81 ++++++++++++++++--- .../Channels/RecommendedChannels.scss | 8 -- .../Channels/RecommendedChannels.tsx | 50 ------------ .../Channels/RecommendedChannelsList.tsx | 40 --------- 5 files changed, 75 insertions(+), 111 deletions(-) delete mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss delete mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx delete mode 100644 plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss index aff331de94f..bfa5c4e55b6 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss @@ -4,4 +4,11 @@ align-items: flex-start; gap: $gap-smallest; } + + .components-button.is-link { + font-size: 14px; + font-weight: 600; + line-height: 17px; + text-decoration: none; + } } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index 7fae3ff2721..e07280348ee 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -1,19 +1,29 @@ /** * External dependencies */ -import { Fragment } from '@wordpress/element'; +import { Fragment, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Card, CardHeader, CardDivider } from '@wordpress/components'; +import { + Card, + CardHeader, + CardBody, + CardDivider, + Button, + Icon, +} from '@wordpress/components'; +import { chevronUp, chevronDown } from '@wordpress/icons'; /** * Internal dependencies */ import { RecommendedChannel } from '~/marketing/data-multichannel/types'; -import { CardHeaderTitle, CardHeaderDescription } from '~/marketing/components'; +import { + CardHeaderTitle, + CardHeaderDescription, + SmartPluginCardBody, +} from '~/marketing/components'; import { RegisteredChannel } from '~/marketing/types'; import { RegisteredChannelCardBody } from './RegisteredChannelCardBody'; -import { RecommendedChannels } from './RecommendedChannels'; -import { RecommendedChannelsList } from './RecommendedChannelsList'; import './Channels.scss'; type ChannelsProps = { @@ -27,6 +37,13 @@ export const Channels: React.FC< ChannelsProps > = ( { recommendedChannels, onInstalledAndActivated, } ) => { + /** + * State to collapse / expand the recommended channels. + */ + const [ expanded, setExpanded ] = useState( + registeredChannels.length === 0 + ); + /* * If users have no registered channels, * we should display recommended channels without collapsible list @@ -46,10 +63,21 @@ export const Channels: React.FC< ChannelsProps > = ( { ) } - + { recommendedChannels.map( ( el, idx ) => { + return ( + + + { idx < recommendedChannels.length - 1 && ( + + ) } + + ); + } ) } ); } @@ -82,10 +110,37 @@ export const Channels: React.FC< ChannelsProps > = ( { { /* Recommended channels section. */ } { recommendedChannels.length >= 1 && ( - +
+ + + + + { expanded && + recommendedChannels.map( ( el, idx ) => { + return ( + + + { idx < recommendedChannels.length - 1 && ( + + ) } + + ); + } ) } +
) } ); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss deleted file mode 100644 index 9a02c3861dc..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.scss +++ /dev/null @@ -1,8 +0,0 @@ -.woocommerce-marketing-recommended-channels { - .components-button.is-link { - font-size: 14px; - font-weight: 600; - line-height: 17px; - text-decoration: none; - } -} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx deleted file mode 100644 index b359af6ecb5..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannels.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/** - * External dependencies - */ -import { useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { CardBody, CardDivider, Button, Icon } from '@wordpress/components'; -import { chevronUp, chevronDown } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import { RecommendedChannel } from '~/marketing/data-multichannel/types'; -import { RecommendedChannelsList } from './RecommendedChannelsList'; -import './RecommendedChannels.scss'; - -type RecommendedChannelsType = { - recommendedChannels: Array< RecommendedChannel >; - onInstalledAndActivated?: () => void; -}; - -export const RecommendedChannels: React.FC< RecommendedChannelsType > = ( { - recommendedChannels, - onInstalledAndActivated, -} ) => { - const [ collapsed, setCollapsed ] = useState( true ); - - return ( -
- - - - - { ! collapsed && ( - - ) } -
- ); -}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx deleted file mode 100644 index dac8d07d386..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RecommendedChannelsList.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * External dependencies - */ -import { Fragment } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { CardDivider } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { SmartPluginCardBody } from '~/marketing/components'; -import { RecommendedChannel } from '~/marketing/data-multichannel/types'; -import './Channels.scss'; - -type RecommendedChannelListPropsType = { - recommendedChannels: Array< RecommendedChannel >; - onInstalledAndActivated?: () => void; -}; - -export const RecommendedChannelsList: React.FC< - RecommendedChannelListPropsType -> = ( { recommendedChannels, onInstalledAndActivated } ) => { - return ( - <> - { recommendedChannels.map( ( el, idx ) => { - return ( - - - { idx < recommendedChannels.length - 1 && ( - - ) } - - ); - } ) } - - ); -}; From 15200103ccb5fd936fd50d178b289f880a62e14f Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sat, 28 Jan 2023 01:16:31 +0800 Subject: [PATCH 78/84] Simplify Channels CSS. --- .../marketing/overview-multichannel/Channels/Channels.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss index bfa5c4e55b6..276caba3969 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss @@ -6,9 +6,8 @@ } .components-button.is-link { - font-size: 14px; + @include font-size( 14 ); font-weight: 600; - line-height: 17px; text-decoration: none; } } From e700232415a6163dcc89d731d1bf617a49ee1def Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Sun, 29 Jan 2023 01:28:14 +0800 Subject: [PATCH 79/84] Simplify Channels component. --- .../Channels/Channels.tsx | 98 +++++++------------ 1 file changed, 35 insertions(+), 63 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx index e07280348ee..8e3b469634b 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -37,63 +37,28 @@ export const Channels: React.FC< ChannelsProps > = ( { recommendedChannels, onInstalledAndActivated, } ) => { + const hasRegisteredChannels = registeredChannels.length >= 1; + /** * State to collapse / expand the recommended channels. + * Initial state is expanded if there are no registered channels in first page load. */ - const [ expanded, setExpanded ] = useState( - registeredChannels.length === 0 - ); + const [ expanded, setExpanded ] = useState( ! hasRegisteredChannels ); - /* - * If users have no registered channels, - * we should display recommended channels without collapsible list - * and with a description in the card header. - */ - if ( registeredChannels.length === 0 ) { - return ( - - - - { __( 'Channels', 'woocommerce' ) } - - - { __( - 'Start by adding a channel to your store', - 'woocommerce' - ) } - - - { recommendedChannels.map( ( el, idx ) => { - return ( - - - { idx < recommendedChannels.length - 1 && ( - - ) } - - ); - } ) } - - ); - } - - /* - * Users have registered channels, - * so here we display the registered channels first. - * If there are recommended channels, - * we display them next in a collapsible list. - */ return ( { __( 'Channels', 'woocommerce' ) } + { ! hasRegisteredChannels && ( + + { __( + 'Start by adding a channel to your store', + 'woocommerce' + ) } + + ) } { /* Registered channels section. */ } @@ -101,7 +66,7 @@ export const Channels: React.FC< ChannelsProps > = ( { return ( - { idx < registeredChannels.length - 1 && ( + { idx !== registeredChannels.length - 1 && ( ) } @@ -110,20 +75,26 @@ export const Channels: React.FC< ChannelsProps > = ( { { /* Recommended channels section. */ } { recommendedChannels.length >= 1 && ( -
- - - - +
+ { hasRegisteredChannels && ( + <> + + + + + + ) } { expanded && recommendedChannels.map( ( el, idx ) => { return ( @@ -134,7 +105,8 @@ export const Channels: React.FC< ChannelsProps > = ( { onInstalledAndActivated } /> - { idx < recommendedChannels.length - 1 && ( + { idx !== + recommendedChannels.length - 1 && ( ) } From d59e7e57210abaf93b30cda8d3c0788f1315378d Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Tue, 31 Jan 2023 23:39:02 +0800 Subject: [PATCH 80/84] Edit and arrange imports in MarketingOverviewMultichannel. --- .../MarketingOverviewMultichannel.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index 838a45ecbf0..2e899bf5774 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -6,19 +6,19 @@ import { useUser } from '@woocommerce/data'; /** * Internal dependencies */ +import '~/marketing/data'; +import '~/marketing/data-multichannel'; +import { CenteredSpinner } from '~/marketing/components'; +import { + useRegisteredChannels, + useRecommendedChannels, +} from '~/marketing/hooks'; import { getAdminSetting } from '~/utils/admin-settings'; import { Channels } from './Channels'; import { InstalledExtensions } from './InstalledExtensions'; import { DiscoverTools } from './DiscoverTools'; import { LearnMarketing } from './LearnMarketing'; -import '~/marketing/data'; -import '~/marketing/data-multichannel'; -import { - useRegisteredChannels, - useRecommendedChannels, -} from '~/marketing/hooks'; import './MarketingOverviewMultichannel.scss'; -import { CenteredSpinner } from '../components'; export const MarketingOverviewMultichannel: React.FC = () => { const { From 177fc59deac5f5277ee4c8469cc5424bd2320912 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 1 Feb 2023 00:29:38 +0800 Subject: [PATCH 81/84] Simplify rendering code in MarketingOverviewMultichannel. --- .../overview-multichannel/MarketingOverviewMultichannel.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx index 2e899bf5774..54c85682800 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/MarketingOverviewMultichannel.tsx @@ -45,8 +45,7 @@ export const MarketingOverviewMultichannel: React.FC = () => {
{ dataRegistered && dataRecommended && - ( dataRegistered.length >= 1 || - dataRecommended.length >= 1 ) && ( + ( dataRegistered.length || dataRecommended.length ) && ( Date: Wed, 1 Feb 2023 00:52:35 +0800 Subject: [PATCH 82/84] Rename Channel to RegisteredChannel. This is for better clarity and consistency. --- .../marketing/data-multichannel/action-types.ts | 6 ++++-- .../marketing/data-multichannel/actions.ts | 16 +++++++++------- .../marketing/data-multichannel/reducer.ts | 10 +++++----- .../marketing/data-multichannel/resolvers.ts | 14 +++++++------- .../marketing/data-multichannel/selectors.ts | 4 ++-- .../client/marketing/data-multichannel/types.ts | 6 +++--- .../marketing/hooks/useRegisteredChannels.ts | 13 +++++++------ 7 files changed, 37 insertions(+), 32 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts index c53be7bbd49..ab0e40f15ae 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts @@ -1,6 +1,8 @@ export const TYPES = { - RECEIVE_CHANNELS_SUCCESS: 'RECEIVE_CHANNELS_SUCCESS' as const, - RECEIVE_CHANNELS_ERROR: 'RECEIVE_CHANNELS_ERROR' as const, + RECEIVE_REGISTERED_CHANNELS_SUCCESS: + 'RECEIVE_REGISTERED_CHANNELS_SUCCESS' as const, + RECEIVE_REGISTERED_CHANNELS_ERROR: + 'RECEIVE_REGISTERED_CHANNELS_ERROR' as const, RECEIVE_RECOMMENDED_CHANNELS_SUCCESS: 'RECEIVE_RECOMMENDED_CHANNELS_SUCCESS' as const, RECEIVE_RECOMMENDED_CHANNELS_ERROR: diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts index b07417e420e..685aa867c02 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts @@ -2,18 +2,20 @@ * Internal dependencies */ import { TYPES } from './action-types'; -import { ApiFetchError, Channel, RecommendedChannel } from './types'; +import { ApiFetchError, RegisteredChannel, RecommendedChannel } from './types'; -export const receiveChannelsSuccess = ( channels: Array< Channel > ) => { +export const receiveRegisteredChannelsSuccess = ( + channels: Array< RegisteredChannel > +) => { return { - type: TYPES.RECEIVE_CHANNELS_SUCCESS, + type: TYPES.RECEIVE_REGISTERED_CHANNELS_SUCCESS, payload: channels, }; }; -export const receiveChannelsError = ( error: ApiFetchError ) => { +export const receiveRegisteredChannelsError = ( error: ApiFetchError ) => { return { - type: TYPES.RECEIVE_CHANNELS_ERROR, + type: TYPES.RECEIVE_REGISTERED_CHANNELS_ERROR, payload: error, error: true, }; @@ -37,8 +39,8 @@ export const receiveRecommendedChannelsError = ( error: ApiFetchError ) => { }; export type Action = ReturnType< - | typeof receiveChannelsSuccess - | typeof receiveChannelsError + | typeof receiveRegisteredChannelsSuccess + | typeof receiveRegisteredChannelsError | typeof receiveRecommendedChannelsSuccess | typeof receiveRecommendedChannelsError >; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts index d9e95181bf6..82749ee49f6 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts @@ -12,7 +12,7 @@ import { Action } from './actions'; import { TYPES } from './action-types'; const initialState = { - channels: { + registeredChannels: { data: undefined, error: undefined, }, @@ -27,17 +27,17 @@ export const reducer: Reducer< State, Action > = ( action ) => { switch ( action.type ) { - case TYPES.RECEIVE_CHANNELS_SUCCESS: + case TYPES.RECEIVE_REGISTERED_CHANNELS_SUCCESS: return { ...state, - channels: { + registeredChannels: { data: action.payload, }, }; - case TYPES.RECEIVE_CHANNELS_ERROR: + case TYPES.RECEIVE_REGISTERED_CHANNELS_ERROR: return { ...state, - channels: { + registeredChannels: { error: action.payload, }, }; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts index c0feb6b2985..771a3b084c3 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts @@ -7,25 +7,25 @@ import { apiFetch } from '@wordpress/data-controls'; * Internal dependencies */ import { - receiveChannelsSuccess, - receiveChannelsError, + receiveRegisteredChannelsSuccess, + receiveRegisteredChannelsError, receiveRecommendedChannelsSuccess, receiveRecommendedChannelsError, } from './actions'; -import { Channel, RecommendedChannel } from './types'; +import { RegisteredChannel, RecommendedChannel } from './types'; import { API_NAMESPACE } from './constants'; import { isApiFetchError } from './guards'; -export function* getChannels() { +export function* getRegisteredChannels() { try { - const data: Channel[] = yield apiFetch( { + const data: RegisteredChannel[] = yield apiFetch( { path: `${ API_NAMESPACE }/channels`, } ); - yield receiveChannelsSuccess( data ); + yield receiveRegisteredChannelsSuccess( data ); } catch ( error ) { if ( isApiFetchError( error ) ) { - yield receiveChannelsError( error ); + yield receiveRegisteredChannelsError( error ); } throw error; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts index c37bb8599ad..48df4625c01 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts @@ -3,8 +3,8 @@ */ import { State } from './types'; -export const getChannels = ( state: State ) => { - return state.channels; +export const getRegisteredChannels = ( state: State ) => { + return state.registeredChannels; }; export const getRecommendedChannels = ( state: State ) => { diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts index 518cdb03df7..f9e4dc44ee7 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts @@ -6,7 +6,7 @@ export type ApiFetchError = { message: string; }; -export type Channel = { +export type RegisteredChannel = { slug: string; is_setup_completed: boolean; settings_url: string; @@ -18,7 +18,7 @@ export type Channel = { }; export type ChannelsState = { - data?: Array< Channel >; + data?: Array< RegisteredChannel >; error?: ApiFetchError; }; @@ -51,6 +51,6 @@ export type RecommendedChannelsState = { }; export type State = { - channels: ChannelsState; + registeredChannels: ChannelsState; recommendedChannels: RecommendedChannelsState; }; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts index 97337bc8d3d..1b4341d0694 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -12,7 +12,7 @@ import { RegisteredChannel, SyncStatusType } from '~/marketing/types'; import { STORE_KEY } from '~/marketing/data-multichannel/constants'; import { ApiFetchError, - Channel, + RegisteredChannel as APIRegisteredChannel, ChannelsState, } from '~/marketing/data-multichannel/types'; @@ -34,7 +34,7 @@ const statusMap: Record< string, SyncStatusType > = { synced: 'synced', }; -const convert = ( data: Channel ): RegisteredChannel => { +const convert = ( data: APIRegisteredChannel ): RegisteredChannel => { const issueType = data.errors_count >= 1 ? 'error' : 'none'; const issueText = data.errors_count >= 1 @@ -63,15 +63,16 @@ export const useRegisteredChannels = (): UseRegisteredChannels => { const { invalidateResolution } = useDispatch( STORE_KEY ); const refetch = useCallback( () => { - invalidateResolution( 'getChannels' ); + invalidateResolution( 'getRegisteredChannels' ); }, [ invalidateResolution ] ); return useSelect( ( select ) => { - const { hasFinishedResolution, getChannels } = select( STORE_KEY ); - const channels = getChannels< ChannelsState >(); + const { hasFinishedResolution, getRegisteredChannels } = + select( STORE_KEY ); + const channels = getRegisteredChannels< ChannelsState >(); return { - loading: ! hasFinishedResolution( 'getChannels' ), + loading: ! hasFinishedResolution( 'getRegisteredChannels' ), data: channels.data?.map( convert ), error: channels.error, refetch, From 46adb0dc25e329ad3e1f7fb5d108c2f1c0131cd6 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Wed, 1 Feb 2023 02:58:49 +0800 Subject: [PATCH 83/84] Rename Channels to RegisteredChannels. --- .../client/marketing/data-multichannel/types.ts | 4 ++-- .../client/marketing/hooks/useRegisteredChannels.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts index f9e4dc44ee7..31cee8017c0 100644 --- a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts @@ -17,7 +17,7 @@ export type RegisteredChannel = { icon: string; }; -export type ChannelsState = { +export type RegisteredChannelsState = { data?: Array< RegisteredChannel >; error?: ApiFetchError; }; @@ -51,6 +51,6 @@ export type RecommendedChannelsState = { }; export type State = { - registeredChannels: ChannelsState; + registeredChannels: RegisteredChannelsState; recommendedChannels: RecommendedChannelsState; }; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts index 1b4341d0694..bafbd5b6f2e 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -13,7 +13,7 @@ import { STORE_KEY } from '~/marketing/data-multichannel/constants'; import { ApiFetchError, RegisteredChannel as APIRegisteredChannel, - ChannelsState, + RegisteredChannelsState, } from '~/marketing/data-multichannel/types'; type UseRegisteredChannels = { @@ -69,12 +69,12 @@ export const useRegisteredChannels = (): UseRegisteredChannels => { return useSelect( ( select ) => { const { hasFinishedResolution, getRegisteredChannels } = select( STORE_KEY ); - const channels = getRegisteredChannels< ChannelsState >(); + const state = getRegisteredChannels< RegisteredChannelsState >(); return { loading: ! hasFinishedResolution( 'getRegisteredChannels' ), - data: channels.data?.map( convert ), - error: channels.error, + data: state.data?.map( convert ), + error: state.error, refetch, }; } ); From 7bb042fcd22affc206acfb51575b8c12dcf99a12 Mon Sep 17 00:00:00 2001 From: Gan Eng Chin Date: Thu, 2 Feb 2023 21:50:36 +0800 Subject: [PATCH 84/84] Simplify CSS for CardHeaderDescription. --- .../CardHeaderDescription/CardHeaderDescription.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketing/components/CardHeaderDescription/CardHeaderDescription.scss b/plugins/woocommerce-admin/client/marketing/components/CardHeaderDescription/CardHeaderDescription.scss index be1ad094c6e..f6cedefdb70 100644 --- a/plugins/woocommerce-admin/client/marketing/components/CardHeaderDescription/CardHeaderDescription.scss +++ b/plugins/woocommerce-admin/client/marketing/components/CardHeaderDescription/CardHeaderDescription.scss @@ -1,6 +1,4 @@ .woocommerce-marketing-card-header-description { - font-size: 14px; - font-weight: 400; - line-height: 18px; + @include font-size( 14 ); color: $gray-700; }