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 <nima.karimi@automattic.com>
This commit is contained in:
Nima Karimi 2022-11-09 10:41:18 +00:00 committed by GitHub
parent c55c91d7e0
commit 6acd69e404
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 830 additions and 723 deletions

View File

@ -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 );
}

View File

@ -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 ) );
}
}

View File

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

View File

@ -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;
}
/**
* Get allowed plugins.
*
* @return array
*/
public static function get_allowed_plugins() {
public function get_data(): array {
return array_map(
function ( MarketingChannelInterface $channel ) {
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',
'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 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',
];
}
}

View File

@ -0,0 +1,110 @@
<?php
/**
* Represents a marketing/ads campaign for marketing channels.
*
* Marketing channels (implementing MarketingChannelInterface) can use this class to map their campaign data and present it to WooCommerce core.
*/
namespace Automattic\WooCommerce\Admin\Marketing;
use JsonSerializable;
/**
* MarketingCampaign class
*
* @since x.x.x
*/
class MarketingCampaign implements JsonSerializable {
/**
* The unique identifier.
*
* @var string
*/
protected $id;
/**
* Title of the marketing campaign.
*
* @var string
*/
protected $title;
/**
* The URL to the channel's campaign management page.
*
* @var string
*/
protected $manage_url;
/**
* The cost of the marketing campaign with the currency.
*
* @var Price
*/
protected $cost;
/**
* 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.
*/
public function __construct( string $id, string $title, string $manage_url, Price $cost = null ) {
$this->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(),
];
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* Represents a marketing channel for the multichannel-marketing feature.
*
* This interface will be implemented by third-party extensions to register themselves as marketing channels.
*/
namespace Automattic\WooCommerce\Admin\Marketing;
/**
* MarketingChannelInterface interface
*
* @since x.x.x
*/
interface MarketingChannelInterface {
public const PRODUCT_LISTINGS_NOT_APPLICABLE = 'not-applicable';
public const PRODUCT_LISTINGS_SYNC_IN_PROGRESS = 'sync-in-progress';
public const PRODUCT_LISTINGS_SYNCED = 'synced';
/**
* Returns the unique identifier string for the marketing channel extension, also known as the plugin slug.
*
* @return string
*/
public function get_slug(): string;
/**
* Returns the name of the marketing channel.
*
* @return string
*/
public function get_name(): string;
/**
* Returns the description of the marketing channel.
*
* @return string
*/
public function get_description(): string;
/**
* Returns the path to the channel icon.
*
* @return string
*/
public function get_icon_url(): string;
/**
* Returns the setup status of the marketing channel.
*
* @return bool
*/
public function is_setup_completed(): bool;
/**
* Returns the URL to the settings page, or the link to complete the setup/onboarding if the channel has not been set up yet.
*
* @return string
*/
public function get_setup_url(): string;
/**
* Returns the status of the marketing channel's product listings.
*
* @return string
*/
public function get_product_listings_status(): string;
/**
* Returns the number of channel issues/errors (e.g. account-related errors, product synchronization issues, etc.).
*
* @return int The number of issues to resolve, or 0 if there are no issues with the channel.
*/
public function get_errors_no(): int;
/**
* Returns an array of the channel's marketing campaigns.
*
* @return MarketingCampaign[]
*/
public function get_campaigns(): array;
}

View File

@ -0,0 +1,131 @@
<?php
/**
* Handles the registration of marketing channels and acts as their repository.
*/
namespace Automattic\WooCommerce\Admin\Marketing;
use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs;
/**
* MarketingChannels repository class
*
* @since x.x.x
*/
class MarketingChannels {
/**
* The registered marketing channels.
*
* @var MarketingChannelInterface[]
*/
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.
*/
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() ] );
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* Represents a price with a currency.
*/
namespace Automattic\WooCommerce\Admin\Marketing;
use JsonSerializable;
/**
* Price class
*
* @since x.x.x
*/
class Price implements JsonSerializable {
/**
* The price.
*
* @var string
*/
protected $value;
/**
* The currency of the price.
*
* @var string
*/
protected $currency;
/**
* Price constructor.
*
* @param string $value The value of the price.
* @param string $currency The currency of the price.
*/
public function __construct( string $value, string $currency ) {
$this->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(),
];
}
}

View File

@ -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,
);
/**

View File

@ -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;
}
}

View File

@ -0,0 +1,145 @@
<?php
/**
* Marketing Specs Handler
*
* Fetches the specifications for the marketing feature from WC.com API.
*/
namespace Automattic\WooCommerce\Internal\Admin\Marketing;
/**
* Marketing Specifications Class.
*
* @internal
* @since x.x.x
*/
class MarketingSpecs {
/**
* 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';
/**
* Load recommended plugins from WooCommerce.com
*
* @return array
*/
public function get_recommended_plugins(): array {
$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|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;
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* MarketingServiceProvider class file.
*/
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;
// Indicates that the multichannel marketing classes exist.
// This constant will be checked by third-party extensions before utilizing any of the classes defined for this feature.
if ( ! defined( 'WC_MCM_EXISTS' ) ) {
define( 'WC_MCM_EXISTS', true );
}
/**
* Service provider for the non-static utils classes in the Automattic\WooCommerce\src namespace.
*
* @since x.x.x
*/
class MarketingServiceProvider extends AbstractServiceProvider {
/**
* The classes/interfaces that are serviced by this service provider.
*
* @var array
*/
protected $provides = array(
MarketingSpecs::class,
MarketingChannels::class,
InstalledExtensions::class,
);
/**
* Register the classes.
*/
public function register() {
$this->share( MarketingSpecs::class );
$this->share( MarketingChannels::class )->addArgument( MarketingSpecs::class );
$this->share( InstalledExtensions::class )->addArgument( MarketingChannels::class );
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Automattic\WooCommerce\Tests\Admin\Marketing;
use Automattic\WooCommerce\Admin\Marketing\MarketingCampaign;
use Automattic\WooCommerce\Admin\Marketing\Price;
use WC_Unit_Test_Case;
/**
* Tests for the MarketingCampaign class.
*/
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' ) );
$this->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 )
);
}
}

View File

@ -0,0 +1,120 @@
<?php
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;
/**
* Tests for the MarketingChannels class.
*/
class MarketingChannelsTest extends WC_Unit_Test_Case {
/**
* Runs before each test.
*/
public function setUp(): void {
delete_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT );
}
/**
* @testdox A marketing channel can be registered using the `register` method if it is in the allowed list.
*/
public function test_registers_allowed_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(
[
[
'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() );
}
}