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] 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() ); + } +}