This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,185 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Renderer\Blocks\Coupon;
use MailPoet\NewsletterProcessingException;
use MailPoet\WP\DateTime;
class CouponPreProcessor {
/** @var bool */
private $generated = false;
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var Helper */
private $wcHelper;
public function __construct(
Helper $wcHelper,
NewslettersRepository $newslettersRepository
) {
$this->wcHelper = $wcHelper;
$this->newslettersRepository = $newslettersRepository;
}
/**
* @throws NewsletterProcessingException
*/
public function processCoupons(NewsletterEntity $newsletter, array $blocks, bool $preview = false): array {
if ($preview) {
return $blocks;
}
$generated = $this->ensureCouponForBlocks($blocks, $newsletter);
$body = $newsletter->getBody();
if ($generated && $body && $this->shouldPersist($newsletter)) {
$updatedBody = array_merge(
$body,
[
'content' => array_merge(
$body['content'],
['blocks' => $blocks]
),
]
);
$newsletter->setBody($updatedBody);
$this->newslettersRepository->flush();
}
return $blocks;
}
private function ensureCouponForBlocks(array &$blocks, NewsletterEntity $newsletter): bool {
foreach ($blocks as &$innerBlock) {
if (isset($innerBlock['blocks']) && !empty($innerBlock['blocks'])) {
$this->ensureCouponForBlocks($innerBlock['blocks'], $newsletter);
}
if (isset($innerBlock['type']) && $innerBlock['type'] === Coupon::TYPE) {
if (!$this->wcHelper->isWooCommerceActive()) {
throw NewsletterProcessingException::create()->withMessage(__('WooCommerce is not active', 'mailpoet'));
}
if ($this->shouldGenerateCoupon($innerBlock)) {
try {
$innerBlock['couponId'] = $this->addOrUpdateCoupon($innerBlock, $newsletter);
$this->generated = true;
} catch (\Exception $e) {
throw NewsletterProcessingException::create()->withMessage($e->getMessage())->withCode($e->getCode());
}
}
}
}
return $this->generated;
}
/**
* @param array $couponBlock
* @param NewsletterEntity $newsletter
* @return int
* @throws \WC_Data_Exception|\Exception
*/
private function addOrUpdateCoupon(array $couponBlock, NewsletterEntity $newsletter) {
$coupon = $this->wcHelper->createWcCoupon($couponBlock['couponId'] ?? '');
if ($this->shouldGenerateCoupon($couponBlock)) {
$code = isset($couponBlock['code']) && $couponBlock['code'] !== Coupon::CODE_PLACEHOLDER ? $couponBlock['code'] : $this->generateRandomCode();
$coupon->set_code($code);
}
$coupon->set_description(
sprintf(
// translators: %1$s is newsletter id and %2$s is the subject.
_x('Auto Generated coupon by MailPoet for email: %1$s: %2$s', 'Coupon block code generation', 'mailpoet'),
$newsletter->getId(),
$newsletter->getSubject()
)
);
// general
$coupon->set_discount_type($couponBlock['discountType']);
if (isset($couponBlock['amount'])) {
$coupon->set_amount($couponBlock['amount']);
}
if (isset($couponBlock['expiryDay'])) {
$expiration = (new DateTime())->getCurrentDateTime()
->modify("+{$couponBlock['expiryDay']} day")
->getTimestamp();
$coupon->set_date_expires($expiration);
}
$coupon->set_free_shipping($couponBlock['freeShipping'] ?? false);
// usage restriction
$coupon->set_minimum_amount($couponBlock['minimumAmount'] ?? '');
$coupon->set_maximum_amount($couponBlock['maximumAmount'] ?? '');
$coupon->set_individual_use($couponBlock['individualUse'] ?? false);
$coupon->set_exclude_sale_items($couponBlock['excludeSaleItems'] ?? false);
$coupon->set_product_ids($this->getItemIds($couponBlock['productIds'] ?? []));
$coupon->set_excluded_product_ids($this->getItemIds($couponBlock['excludedProductIds'] ?? []));
$coupon->set_product_categories($this->getItemIds($couponBlock['productCategoryIds'] ?? []));
$coupon->set_excluded_product_categories($this->getItemIds($couponBlock['excludedProductCategoryIds'] ?? []));
$coupon->set_email_restrictions(explode(',', $couponBlock['emailRestrictions'] ?? ''));
// usage limit
$coupon->set_usage_limit($couponBlock['usageLimit'] ?? 0);
$coupon->set_usage_limit_per_user($couponBlock['usageLimitPerUser'] ?? 0);
return $coupon->save();
}
private function getItemIds(array $items): array {
if (empty($items)) {
return [];
}
return array_map(function ($item) {
return $item['id'];
}, $items);
}
private function generateRandomSegment($length) {
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$segment = '';
for ($i = 0; $i < $length; $i++) {
$randomIndex = rand(0, strlen($characters) - 1);
$segment .= $characters[$randomIndex];
}
return $segment;
}
/**
* Generates Coupon code for XXXX-XXXXXX-XXXX pattern
*/
private function generateRandomCode(): string {
$part1 = $this->generateRandomSegment(4);
$part2 = $this->generateRandomSegment(6);
$part3 = $this->generateRandomSegment(4);
return $part1 . '-' . $part2 . '-' . $part3;
}
private function shouldGenerateCoupon(array $block): bool {
return empty($block['couponId']);
}
/**
* Only emails that can have their body-HTML re-generated should persist the generated couponId
*/
private function shouldPersist(NewsletterEntity $newsletter): bool {
return in_array($newsletter->getType(), NewsletterEntity::TYPES_WITH_RESETTABLE_BODY);
}
}
@@ -0,0 +1,346 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\DataStore;
use MailPoet\DI\ContainerWrapper;
use MailPoet\RuntimeException;
use MailPoet\WP\Functions as WPFunctions;
class Helper {
/** @var WPFunctions */
private $wp;
public function __construct(
WPFunctions $wp
) {
$this->wp = $wp;
}
public function isWooCommerceActive() {
return class_exists('WooCommerce') && $this->wp->isPluginActive('woocommerce/woocommerce.php');
}
public function getWooCommerceVersion() {
return $this->isWooCommerceActive() ? get_plugin_data(WP_PLUGIN_DIR . '/woocommerce/woocommerce.php', false, false)['Version'] : null;
}
public function getPurchaseStates(): array {
return (array)$this->wp->applyFilters(
'mailpoet_purchase_order_states',
['completed']
);
}
public function isWooCommerceBlocksActive($min_version = '') {
if (!class_exists('\Automattic\WooCommerce\Blocks\Package')) {
return false;
}
if ($min_version) {
return version_compare(\Automattic\WooCommerce\Blocks\Package::get_version(), $min_version, '>=');
}
return true;
}
public function isWooCommerceCustomOrdersTableEnabled(): bool {
if (
$this->isWooCommerceActive()
&& method_exists('\Automattic\WooCommerce\Utilities\OrderUtil', 'custom_orders_table_usage_is_enabled')
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled()
) {
return true;
}
return false;
}
public function WC() {
return WC();
}
public function wcGetCustomerOrderCount($userId) {
return wc_get_customer_order_count($userId);
}
public function wcGetOrder($order = false) {
return wc_get_order($order);
}
public function wcGetOrders(array $args) {
return wc_get_orders($args);
}
public function wcCreateOrder(array $args) {
return wc_create_order($args);
}
public function wcPrice($price, array $args = []) {
return wc_price($price, $args);
}
public function wcGetProduct($theProduct = false) {
return wc_get_product($theProduct);
}
public function wcGetPageId(string $page): ?int {
if ($this->isWooCommerceActive()) {
return (int)wc_get_page_id($page);
}
return null;
}
public function wcGetPriceDecimals(): int {
return wc_get_price_decimals();
}
public function wcGetPriceDecimalSeperator(): string {
return wc_get_price_decimal_separator();
}
public function wcGetPriceThousandSeparator(): string {
return wc_get_price_thousand_separator();
}
public function getWoocommercePriceFormat(): string {
return get_woocommerce_price_format();
}
public function getWoocommerceCurrency() {
return get_woocommerce_currency();
}
public function getWoocommerceCurrencySymbol() {
return get_woocommerce_currency_symbol();
}
public function woocommerceFormField($key, $args, $value) {
return woocommerce_form_field($key, $args, $value);
}
public function wcLightOrDark($color, $dark, $light) {
return wc_light_or_dark($color, $dark, $light);
}
public function wcHexIsLight($color) {
return wc_hex_is_light($color);
}
public function getOrdersCountCreatedBefore(string $dateTime): int {
$ordersCount = $this->wcGetOrders([
'status' => 'all',
'type' => 'shop_order',
'date_created' => '<' . $dateTime,
'limit' => 1,
'paginate' => true,
])->total;
return intval($ordersCount);
}
public function getRawPrice($price, array $args = []) {
$htmlPrice = $this->wcPrice($price, $args);
return html_entity_decode(strip_tags($htmlPrice));
}
public function getAllowedCountries(): array {
return (new \WC_Countries)->get_allowed_countries() ?? [];
}
public function getCustomersCount(): int {
if (!$this->isWooCommerceActive() || !class_exists(DataStore::class)) {
return 0;
}
$dataStore = new DataStore();
$result = (array)$dataStore->get_data([
'fields' => ['customers_count'],
]);
return isset($result['customers_count']) ? intval($result['customers_count']) : 0;
}
public function wasMailPoetInstalledViaWooCommerceOnboardingWizard(): bool {
$wp = ContainerWrapper::getInstance()->get(WPFunctions::class);
$installedViaWooCommerce = false;
$wooCommerceOnboardingProfile = $wp->getOption('woocommerce_onboarding_profile');
if (
is_array($wooCommerceOnboardingProfile)
&& isset($wooCommerceOnboardingProfile['business_extensions'])
&& is_array($wooCommerceOnboardingProfile['business_extensions'])
&& in_array('mailpoet', $wooCommerceOnboardingProfile['business_extensions'])
) {
$installedViaWooCommerce = true;
}
return $installedViaWooCommerce;
}
public function getOrdersTableName() {
if (!method_exists('\Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore', 'get_orders_table_name')) {
throw new RuntimeException('Cannot get orders table name when running a WooCommerce version that doesn\'t support custom order tables.');
}
return \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::get_orders_table_name();
}
public function getAddressesTableName() {
if (!method_exists('\Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore', 'get_addresses_table_name')) {
throw new RuntimeException('Cannot get addresses table name when running a WooCommerce version that doesn\'t support custom order tables.');
}
return \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::get_addresses_table_name();
}
public function wcGetCouponTypes(): array {
return wc_get_coupon_types();
}
public function wcGetCouponCodeById(int $id): string {
return wc_get_coupon_code_by_id($id);
}
/**
* @param mixed $data Coupon data, object, ID or code.
*/
public function createWcCoupon($data) {
return new \WC_Coupon($data);
}
public function getOrderStatuses(): array {
return wc_get_order_statuses();
}
/**
* @return array|\WP_Post[]
*/
public function getCouponList(
int $pageSize = 10,
int $pageNumber = 1,
?string $discountType = null,
?string $search = null,
array $includeCouponIds = []
): array {
$args = [
'posts_per_page' => $pageSize,
'orderby' => 'name',
'order' => 'asc',
'post_type' => 'shop_coupon',
'post_status' => 'publish',
'paged' => $pageNumber,
];
// Filtering coupons by discount type
if ($discountType) {
$args['meta_key'] = 'discount_type';
$args['meta_value'] = $discountType;
}
// Search coupon by a query string
if ($search) {
$args['s'] = $search;
}
$includeCoupons = [];
// We need to include the coupon with the given ID in the first page
if ($includeCouponIds && $pageNumber === 1) {
$includeArgs = $args;
$includeArgs['include'] = $includeCouponIds;
$includeCoupons = $this->wp->getPosts($includeArgs);
}
// We remove duplicates because one of the remaining pages might contain the coupon with the given ID
$result = array_merge($includeCoupons, $this->wp->getPosts($args));
$result = array_unique($result, SORT_REGULAR);
return array_values($result);
}
public function wcGetPriceDecimalSeparator() {
return wc_get_price_decimal_separator();
}
public function getLatestCoupon(): ?string {
$coupons = $this->wp->getPosts([
'numberposts' => 1,
'orderby' => 'date_created',
'order' => 'desc',
'post_type' => 'shop_coupon',
'post_status' => 'publish',
]);
$coupon = reset($coupons);
return $coupon ? $coupon->post_title : null; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
}
public function getPaymentGateways() {
return $this->WC()->payment_gateways();
}
/**
* Returns a list of all available shipping methods formatted
* in a way to be used in the 'used shipping method' segment.
*/
public function getShippingMethodInstancesData(): array {
$shippingZones = \WC_Shipping_Zones::get_zones();
$formattedShippingMethodData = [];
foreach ($shippingZones as $shippingZone) {
$formattedShippingMethodData = array_merge(
$formattedShippingMethodData,
$this->formatShippingMethods($shippingZone['shipping_methods'], $shippingZone['zone_name'])
);
}
// special shipping zone that includes locations not covered by the configured shipping zones
$outOfCoverageShippingZone = new \WC_Shipping_Zone(0);
$formattedShippingMethodData = array_merge(
$formattedShippingMethodData,
$this->formatShippingMethods($outOfCoverageShippingZone->get_shipping_methods(), $outOfCoverageShippingZone->get_zone_name())
);
$keyedZones = [];
foreach ($formattedShippingMethodData as $shippingMethodArray) {
$keyedZones[$shippingMethodArray['instanceId']] = $shippingMethodArray;
}
return $keyedZones;
}
public function wcGetAttributeTaxonomies(): array {
return wc_get_attribute_taxonomies();
}
protected function formatShippingMethods(array $shippingMethods, string $shippingZoneName): array {
$formattedShippingMethods = [];
foreach ($shippingMethods as $shippingMethod) {
$formattedShippingMethods[] = [
'instanceId' => (string)$shippingMethod->instance_id, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
'name' => "{$shippingMethod->title} ({$shippingZoneName})",
];
}
return $formattedShippingMethods;
}
/**
* Check whether the current request is processing a WooCommerce checkout.
* Works for both the normal checkout and the block checkout.
*
* This solution is not ideal, but I checked with a few WooCommerce developers,
* and it is what they suggested. There is no helper function provided by Woo
* for this.
*
* @return bool
*/
public function isCheckoutRequest(): bool {
$requestUri = !empty($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
$isRegularCheckout = is_checkout();
$isBlockCheckout = WC()->is_rest_api_request()
&& (strpos($requestUri, 'wc/store/checkout') !== false || strpos($requestUri, 'wc/store/v1/checkout') !== false);
return $isRegularCheckout || $isBlockCheckout;
}
}
@@ -0,0 +1,113 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce\Integrations;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\WP\Functions as WPFunctions;
class AutomateWooHooks {
const AUTOMATE_WOO_PLUGIN_SLUG = 'automatewoo/automatewoo.php';
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var WPFunctions */
private $wp;
public function __construct(
SubscribersRepository $subscribersRepository,
WPFunctions $wp
) {
$this->subscribersRepository = $subscribersRepository;
$this->wp = $wp;
}
public function isAutomateWooActive(): bool {
return $this->wp->isPluginActive(self::AUTOMATE_WOO_PLUGIN_SLUG);
}
public function areMethodsAvailable(): bool {
return class_exists('AutomateWoo\Customer_Factory') && method_exists('AutomateWoo\Customer_Factory', 'get_by_email') &&
class_exists('AutomateWoo\Customer') && method_exists('AutomateWoo\Customer', 'opt_out');
}
public function isAutomateWooReady(): bool {
return $this->isAutomateWooActive() && $this->areMethodsAvailable();
}
/**
* @return \AutomateWoo\Customer|false
*/
public function getAutomateWooCustomer(string $email) {
// AutomateWoo\Customer_Factory::get_by_email() returns false if customer is not found
// Second parameter is set to false to prevent creating new customer if not found
return \AutomateWoo\Customer_Factory::get_by_email($email, false);
}
public function setup(): void {
if (!$this->isAutomateWooReady()) {
return;
}
$this->wp->addAction(SubscriberEntity::HOOK_SUBSCRIBER_STATUS_CHANGED, [$this, 'syncSubscriber'], 10, 1);
$this->wp->addAction('mailpoet_segment_subscribed', [$this, 'maybeOptInSubscriber'], 10, 1);
}
public function optOutSubscriber($subscriber): void {
if (!$this->isAutomateWooReady() || !$subscriber) {
return;
}
$automateWooCustomer = $this->getAutomateWooCustomer($subscriber->getEmail());
if (!$automateWooCustomer) {
return;
}
$automateWooCustomer->opt_out();
}
public function optInSubscriber($subscriber): void {
if (!$this->isAutomateWooReady() || !$subscriber) {
return;
}
$automateWooCustomer = $this->getAutomateWooCustomer($subscriber->getEmail());
if (!$automateWooCustomer) {
return;
}
$automateWooCustomer->opt_in();
}
public function syncSubscriber(int $subscriberId): void {
$subscriber = $this->subscribersRepository->findOneById($subscriberId);
if (!$subscriber || !$subscriber->getEmail()) {
return;
}
if ($this->isWooCommerceSubscribed($subscriber)) {
$this->optInSubscriber($subscriber);
} else {
$this->optOutSubscriber($subscriber);
}
}
/**
* Opt-In the subscriber in AW only if the subscriber belongs to WooCommerce list.
*/
public function maybeOptInSubscriber(SubscriberSegmentEntity $subscriberSegment) {
if ($subscriberSegment->getSegment() && $subscriberSegment->getSegment()->getType() === SegmentEntity::TYPE_WC_USERS) {
$this->optInSubscriber($subscriberSegment->getSubscriber());
}
}
private function isWooCommerceSubscribed(SubscriberEntity $subscriber) {
return $subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED
&& $this->subscribersRepository->getWooCommerceSegmentSubscriber($subscriber->getEmail());
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,62 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use MailPoet\Config\Menu;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Settings\SettingsController;
/**
* MailPoet task that is added to the WooCommerce homepage.
*/
class MailPoetTask extends Task {
public function get_id(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return 'mailpoet_task';
}
public function get_title(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
if ($this->is_complete()) {
return esc_html__('MailPoet is ready to send marketing emails from your store', 'mailpoet');
}
return esc_html__('Set up email marketing with MailPoet', 'mailpoet');
}
public function get_content(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return '';
}
/**
* String that is displayed below the title of the task indicating the estimated completion time.
*/
public function get_time(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return '';
}
/**
* Link used when the user clicks on the title of the task.
*/
public function get_action_url(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
if ($this->is_complete()) {
return admin_url('admin.php?page=' . Menu::MAIN_PAGE_SLUG);
}
return admin_url('admin.php?page=' . Menu::WELCOME_WIZARD_PAGE_SLUG . '&mailpoet_wizard_loaded_via_woocommerce');
}
/**
* Whether the task is completed.
* If the setting 'version' is not null it means the welcome wizard
* was already completed so we mark this task as completed as well.
*/
public function is_complete(): bool { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
$settings = ContainerWrapper::getInstance()->get(SettingsController::class);
$version = $settings->get('version');
return $version !== null;
}
}
@@ -0,0 +1,212 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce\MultichannelMarketing;
if (!defined('ABSPATH')) exit;
use Automattic\WooCommerce\Admin\Marketing\MarketingCampaign;
use Automattic\WooCommerce\Admin\Marketing\MarketingCampaignType;
use Automattic\WooCommerce\Admin\Marketing\MarketingChannelInterface;
use Automattic\WooCommerce\Admin\Marketing\Price;
use MailPoet\Config\Menu;
class MPMarketingChannel implements MarketingChannelInterface {
/**
* @var MarketingCampaignType[]
*/
private $campaignTypes;
/**
* @var MPMarketingChannelDataController
*/
private $channelDataController;
const CAMPAIGN_TYPE_NEWSLETTERS = 'mailpoet-newsletters';
const CAMPAIGN_TYPE_POST_NOTIFICATIONS = 'mailpoet-post-notifications';
const CAMPAIGN_TYPE_AUTOMATIONS = 'mailpoet-automations';
public function __construct(
MPMarketingChannelDataController $channelDataController
) {
$this->channelDataController = $channelDataController;
$this->campaignTypes = $this->generateCampaignTypes();
}
/**
* Returns the unique identifier string for the marketing channel extension, also known as the plugin slug.
*
* @return string
*/
public function get_slug(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return 'mailpoet';
}
/**
* Returns the name of the marketing channel.
*
* @return string
*/
public function get_name(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return __('MailPoet', 'mailpoet');
}
/**
* Returns the description of the marketing channel.
*
* @return string
*/
public function get_description(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return __('Create and send newsletters, post notifications and welcome emails from your WordPress.', 'mailpoet');
}
/**
* Returns the path to the channel icon.
*
* @return string
*/
public function get_icon_url(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return $this->channelDataController->getIconUrl();
}
/**
* Returns the setup status of the marketing channel.
*
* @return bool
*/
public function is_setup_completed(): bool { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return $this->channelDataController->isMPSetupComplete();
}
/**
* 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 { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
if ($this->channelDataController->isMPSetupComplete()) {
return admin_url('admin.php?page=' . Menu::MAIN_PAGE_SLUG);
}
return admin_url('admin.php?page=' . Menu::WELCOME_WIZARD_PAGE_SLUG . '&mailpoet_wizard_loaded_via_woocommerce_marketing_dashboard');
}
/**
* Returns the status of the marketing channel's product listings.
*
* @return string
*/
public function get_product_listings_status(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
if (!$this->channelDataController->isMailPoetSendingServiceEnabled()) {
return self::PRODUCT_LISTINGS_NOT_APPLICABLE;
}
// Check for error status. It's null by default when there isn't an error
$sendingStatus = $this->channelDataController->getMailPoetSendingStatus();
if ($sendingStatus) {
return self::PRODUCT_LISTINGS_SYNC_FAILED;
}
return self::PRODUCT_LISTINGS_SYNCED;
}
/**
* 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_count(): int { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return $this->channelDataController->getErrorCount();
}
/**
* 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 { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
return $this->campaignTypes;
}
/**
* Returns an array of the channel's marketing campaigns.
*
* @return MarketingCampaign[]
*/
public function get_campaigns(): array { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
$allCampaigns = $this->generateCampaigns();
if (empty($allCampaigns)) {
return [];
}
return $allCampaigns;
}
/**
* Generate the marketing channel campaign types
*
* @return MarketingCampaignType[]
*/
protected function generateCampaignTypes(): array {
return [
self::CAMPAIGN_TYPE_NEWSLETTERS => new MarketingCampaignType(
'mailpoet-newsletters',
$this,
__('MailPoet Newsletters', 'mailpoet'),
__(
'Send a newsletter with images, buttons, dividers, and social bookmarks. Or, just send a basic text email.',
'mailpoet',
),
admin_url('admin.php?page=' . Menu::EMAILS_PAGE_SLUG . '&loadedvia=woo_multichannel_dashboard#/new/standard'),
$this->get_icon_url()
),
self::CAMPAIGN_TYPE_POST_NOTIFICATIONS => new MarketingCampaignType(
'mailpoet-post-notifications',
$this,
__('MailPoet Post Notifications', 'mailpoet'),
__(
'Let MailPoet email your subscribers with your latest content. You can send daily, weekly, monthly, or even immediately after publication.',
'mailpoet',
),
admin_url('admin.php?page=' . Menu::EMAILS_PAGE_SLUG . '&loadedvia=woo_multichannel_dashboard#/new/notification'),
$this->get_icon_url()
),
self::CAMPAIGN_TYPE_AUTOMATIONS => new MarketingCampaignType(
'mailpoet-automations',
$this,
__('MailPoet Automations', 'mailpoet'),
__('Set up automations to send abandoned cart reminders, welcome new subscribers, celebrate first-time buyers, and much more.', 'mailpoet'),
admin_url('admin.php?page=' . Menu::AUTOMATION_TEMPLATES_PAGE_SLUG . '&loadedvia=woo_multichannel_dashboard'),
$this->get_icon_url()
),
];
}
protected function generateCampaigns(): array {
return array_map(
function (array $data) {
$cost = null;
if (isset($data['price'])) {
$cost = new Price((string)$data['price']['amount'], $data['price']['currency']);
}
return new MarketingCampaign(
$data['id'],
$data['campaignType'],
$data['name'],
$data['url'],
$cost,
);
},
array_merge(
$this->channelDataController->getAutomations($this->campaignTypes[self::CAMPAIGN_TYPE_AUTOMATIONS]),
$this->channelDataController->getPostNotificationNewsletters($this->campaignTypes[self::CAMPAIGN_TYPE_POST_NOTIFICATIONS]),
$this->channelDataController->getStandardNewsletterList($this->campaignTypes[self::CAMPAIGN_TYPE_NEWSLETTERS])
)
);
}
}
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce\MultichannelMarketing;
if (!defined('ABSPATH')) exit;
class MPMarketingChannelController {
/**
* @var MPMarketingChannelDataController
*/
private $channelDataController;
public function __construct(
MPMarketingChannelDataController $channelDataController
) {
$this->channelDataController = $channelDataController;
}
public function registerMarketingChannel($registeredMarketingChannels): array {
return array_merge($registeredMarketingChannels, [
new MPMarketingChannel(
$this->channelDataController
),
]);
}
}
@@ -0,0 +1,227 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce\MultichannelMarketing;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Controller\OverviewStatisticsController;
use MailPoet\Automation\Integrations\MailPoet\Analytics\Entities\QueryWithCompare;
use MailPoet\Config\Menu;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository;
use MailPoet\Newsletter\Statistics\WooCommerceRevenue;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\CdnAssetUrl;
use MailPoet\WooCommerce\Helper;
use MailPoetVendor\Carbon\Carbon;
/**
* Created to pass data to the `MPMarketingChannel`.
*
* We create an instance of this class with the MailPoet DI ContainerInterface
*
* This provides the class access to other MailPoet services,
* preventing us from overloading the `MPMarketingChannelController` class with imports and duplicating efforts
*/
class MPMarketingChannelDataController {
/** @var CdnAssetUrl */
private $cdnAssetUrl;
/**
* @var SettingsController
*/
private $settings;
/**
* @var Bridge
*/
private $bridge;
/**
* @var NewslettersRepository
*/
private $newsletterRepository;
/**
* @var Helper
*/
private $woocommerceHelper;
/**
* @var AutomationStorage
*/
private $automationStorage;
/**
* @var NewsletterStatisticsRepository
*/
private $newsletterStatisticsRepository;
/**
* @var OverviewStatisticsController
*/
private $overviewStatisticsController;
public function __construct(
CdnAssetUrl $cdnAssetUrl,
SettingsController $settings,
Bridge $bridge,
NewslettersRepository $newsletterRepository,
Helper $woocommerceHelper,
AutomationStorage $automationStorage,
NewsletterStatisticsRepository $newsletterStatisticsRepository,
OverviewStatisticsController $overviewStatisticsController
) {
$this->cdnAssetUrl = $cdnAssetUrl;
$this->settings = $settings;
$this->bridge = $bridge;
$this->newsletterRepository = $newsletterRepository;
$this->automationStorage = $automationStorage;
$this->woocommerceHelper = $woocommerceHelper;
$this->newsletterStatisticsRepository = $newsletterStatisticsRepository;
$this->overviewStatisticsController = $overviewStatisticsController;
}
public function getIconUrl(): string {
return $this->cdnAssetUrl->generateCdnUrl('icon-white-123x128.png');
}
/**
* Whether the task is completed.
* If the setting 'version' is not null it means the welcome wizard
* was already completed so we mark this task as completed as well.
*/
public function isMPSetupComplete(): bool {
$version = $this->settings->get('version');
return $version !== null;
}
/**
* Is MSS Enabled?
* @return bool
*/
public function isMailPoetSendingServiceEnabled(): bool {
return $this->bridge->isMailpoetSendingServiceEnabled();
}
/**
* Check for error status. It's null by default when there isn't an error
* @return mixed
*/
public function getMailPoetSendingStatus() {
return $this->settings->get('mta_log.status');
}
/**
* Get the number of errors available
* Mostly likely sending errors
* @return int
*/
public function getErrorCount(): int {
$error = $this->settings->get('mta_log.error');
$count = 0;
if (!empty($error)) {
$count++;
}
$validationError = $this->settings->get(AuthorizedEmailsController::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING);
if ($validationError && isset($validationError['invalid_sender_address'])) {
$count++;
}
return $count;
}
public function getStandardNewsletterList($campaignType): array {
return $this->getNewsletterTypeLists(
// fetch the most recently sent post-notification history newsletters limited to ten
$this->newsletterRepository->getStandardNewsletterListWithMultipleStatuses(10),
$campaignType
);
}
public function getPostNotificationNewsletters($campaignType): array {
return $this->getNewsletterTypeLists(
// fetch the most recently sent post-notification history newsletters limited to ten
$this->newsletterRepository->getNotificationHistoryItems(10),
$campaignType
);
}
public function getAutomations($campaignType): array {
$result = [];
// Fetch Automation stats within the last 90 days
$primaryAfter = new \DateTimeImmutable((string)Carbon::now()->subDays(90)->toISOString());
$primaryBefore = new \DateTimeImmutable((string)Carbon::now()->toISOString());
$now = new \DateTimeImmutable('');
$query = new QueryWithCompare($primaryAfter, $primaryBefore, $now, $now);
$userCurrency = $this->woocommerceHelper->getWoocommerceCurrency();
foreach ($this->automationStorage->getAutomations([Automation::STATUS_ACTIVE]) as $automation) {
$automationId = (string)$automation->getId();
$automationStatistics = $this->overviewStatisticsController->getStatisticsForAutomation($automation, $query);
$result[] = [
'id' => $automationId,
'name' => $automation->getName(),
'campaignType' => $campaignType,
'url' => admin_url('admin.php?page=' . Menu::AUTOMATION_ANALYTICS_PAGE_SLUG . '&id=' . $automationId),
'price' => [
'amount' => isset($automationStatistics['revenue']['current']) ? $this->formatPrice($automationStatistics['revenue']['current']) : 0,
'currency' => $userCurrency,
],
];
}
return $result;
}
private function getNewsletterTypeLists($allNewsletters, $campaignType): array {
$result = [];
$userCurrency = $this->woocommerceHelper->getWoocommerceCurrency();
// fetch the most recent newsletters limited to ten
foreach ($allNewsletters as $newsletter) {
$newsLetterId = (string)$newsletter->getId();
/** @var ?WooCommerceRevenue $wooRevenue */
$wooRevenue = $this->newsletterStatisticsRepository->getWooCommerceRevenue($newsletter);
$result[] = [
'id' => $newsLetterId,
'name' => $newsletter->getSubject(),
'campaignType' => $campaignType,
'url' => admin_url('admin.php?page=' . Menu::EMAILS_PAGE_SLUG . '/#/stats/' . $newsLetterId),
'price' => [
'amount' => $wooRevenue ? $this->formatPrice($wooRevenue->getValue()) : 0,
'currency' => $userCurrency,
],
];
}
return $result;
}
/**
* Format amount to 2 dp
* @param string|int|float $amount
* @return string
*/
private function formatPrice($amount): string {
return number_format((float)$amount, 2, '.', '');
}
}
@@ -0,0 +1,50 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Renderer;
use MailPoet\Settings\SettingsController;
class Settings {
/** @var Renderer */
private $renderer;
/** @var SettingsController */
private $settings;
public function __construct(
Renderer $renderer,
SettingsController $settings
) {
$this->renderer = $renderer;
$this->settings = $settings;
}
public function disableWooCommerceSettings() {
if (
!isset($_GET['tab'])
|| $_GET['tab'] !== 'email'
|| isset($_GET['section'])
) {
return;
}
//The templates are in our control and the inputs are sanitized.
//phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->renderer->render('woocommerce/settings_button.html', [
'woocommerce_template_id' => (int)$this->settings->get(TransactionalEmails::SETTING_EMAIL_ID),
]);
if (!(bool)$this->settings->get('woocommerce.use_mailpoet_editor')) {
return;
}
// The templates are in our control and the inputs are sanitized.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->renderer->render('woocommerce/settings_overlay.html', [
'woocommerce_template_id' => (int)$this->settings->get(TransactionalEmails::SETTING_EMAIL_ID),
]);
}
}
@@ -0,0 +1,55 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Subscribers\SubscribersRepository;
use WC_Order;
class SubscriberEngagement {
/** @var Helper */
private $woocommerceHelper;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
Helper $woocommerceHelper,
SubscribersRepository $subscribersRepository
) {
$this->woocommerceHelper = $woocommerceHelper;
$this->subscribersRepository = $subscribersRepository;
}
public function updateSubscriberEngagement($orderId): void {
$order = $this->woocommerceHelper->wcGetOrder($orderId);
if (!$order instanceof WC_Order) {
return;
}
$subscriber = $this->subscribersRepository->findOneBy(['email' => $order->get_billing_email()]);
if (!$subscriber instanceof SubscriberEntity) {
return;
}
$this->subscribersRepository->maybeUpdateLastEngagement($subscriber);
}
public function updateSubscriberLastPurchase($orderId): void {
$order = $this->woocommerceHelper->wcGetOrder($orderId);
if (!$order instanceof WC_Order) {
return;
}
$subscriber = $this->subscribersRepository->findOneBy(['email' => $order->get_billing_email()]);
if (!$subscriber instanceof SubscriberEntity) {
return;
}
$this->subscribersRepository->maybeUpdateLastPurchaseAt($subscriber);
}
}
@@ -0,0 +1,261 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\ConfirmationEmailMailer;
use MailPoet\Subscribers\Source;
use MailPoet\Subscribers\SubscriberSegmentRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class Subscription {
const CHECKOUT_OPTIN_INPUT_NAME = 'mailpoet_woocommerce_checkout_optin';
const CHECKOUT_OPTIN_PRESENCE_CHECK_INPUT_NAME = 'mailpoet_woocommerce_checkout_optin_present';
const OPTIN_ENABLED_SETTING_NAME = 'woocommerce.optin_on_checkout.enabled';
const OPTIN_SEGMENTS_SETTING_NAME = 'woocommerce.optin_on_checkout.segments';
const OPTIN_MESSAGE_SETTING_NAME = 'woocommerce.optin_on_checkout.message';
const OPTIN_POSITION_SETTING_NAME = 'woocommerce.optin_on_checkout.position';
private $allowedHtml = [
'input' => [
'type' => true,
'name' => true,
'id' => true,
'class' => true,
'value' => true,
'checked' => true,
],
'span' => [
'class' => true,
],
'label' => [
'class' => true,
'data-automation-id' => true,
'for' => true,
],
'p' => [
'class' => true,
'id' => true,
'data-priority' => true,
],
];
/** @var SettingsController */
private $settings;
/** @var WPFunctions */
private $wp;
/** @var Helper */
private $wcHelper;
/** @var ConfirmationEmailMailer */
private $confirmationEmailMailer;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var SubscriberSegmentRepository */
private $subscriberSegmentRepository;
public function __construct(
SettingsController $settings,
ConfirmationEmailMailer $confirmationEmailMailer,
WPFunctions $wp,
Helper $wcHelper,
SubscribersRepository $subscribersRepository,
SegmentsRepository $segmentsRepository,
SubscriberSegmentRepository $subscriberSegmentRepository
) {
$this->settings = $settings;
$this->wp = $wp;
$this->wcHelper = $wcHelper;
$this->confirmationEmailMailer = $confirmationEmailMailer;
$this->subscribersRepository = $subscribersRepository;
$this->segmentsRepository = $segmentsRepository;
$this->subscriberSegmentRepository = $subscriberSegmentRepository;
}
public function extendWooCommerceCheckoutForm() {
$inputName = self::CHECKOUT_OPTIN_INPUT_NAME;
$checked = false;
if (!empty($_POST[self::CHECKOUT_OPTIN_INPUT_NAME])) {
$checked = true;
}
$labelString = $this->settings->get(self::OPTIN_MESSAGE_SETTING_NAME);
$template = (string)$this->wp->applyFilters(
'mailpoet_woocommerce_checkout_optin_template',
wp_kses(
$this->getSubscriptionField($inputName, $checked, $labelString),
$this->allowedHtml
),
$inputName,
$checked,
$labelString
);
// The template has been sanitized above and can be considered safe.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $template;
if ($template) {
$field = $this->getSubscriptionPresenceCheckField();
echo wp_kses($field, $this->allowedHtml);
}
}
private function getSubscriptionField($inputName, $checked, $labelString) {
$checked = checked($checked, true, false);
return '<label class="woocommerce-form__label woocommerce-form__label-for-checkbox checkbox" data-automation-id="woo-commerce-subscription-opt-in">
<input id="mailpoet_woocommerce_checkout_optin" class="woocommerce-form__input woocommerce-form__input-checkbox input-checkbox" ' . $checked . ' type="checkbox" name="' . $this->wp->escAttr($inputName) . '" value="1" />
<span>' . $this->wp->escHtml($labelString) . '</span>
</label>';
}
private function getSubscriptionPresenceCheckField() {
$field = $this->wcHelper->woocommerceFormField(
self::CHECKOUT_OPTIN_PRESENCE_CHECK_INPUT_NAME,
[
'type' => 'hidden',
'return' => true,
],
1
);
if ($field) {
return $field;
}
// Workaround for older WooCommerce versions (below 4.6.0) that don't support hidden fields
// We can remove it after we drop support of older WooCommerce
$field = $this->wcHelper->woocommerceFormField(
self::CHECKOUT_OPTIN_PRESENCE_CHECK_INPUT_NAME,
[
'type' => 'text',
'return' => true,
],
1
);
return str_replace('type="text"', 'type="hidden"', $field);
}
public function subscribeOnOrderPay($orderId) {
$wcOrder = $this->wcHelper->wcGetOrder($orderId);
if (!$wcOrder instanceof \WC_Order) {
return null;
}
$data['billing_email'] = $wcOrder->get_billing_email();
$this->subscribeOnCheckout($orderId, $data);
}
public function subscribeOnCheckout($orderId, $data) {
$this->triggerAutomateWooOptin();
if (empty($data['billing_email'])) {
// no email in posted order data
return null;
}
$subscriber = $this->subscribersRepository->findOneBy(
['email' => $data['billing_email'], 'isWoocommerceUser' => 1]
);
if (!$subscriber) {
// no subscriber: WooCommerce sync didn't work
return null;
}
$checkoutOptin = !empty($_POST[self::CHECKOUT_OPTIN_INPUT_NAME]);
return $this->handleSubscriberOptin($subscriber, $checkoutOptin);
}
/**
* Subscribe a subscriber.
*
* @param SubscriberEntity $subscriber Subscriber object
* @param bool $shouldSubscribe Whether the subscriber should be subscribed
*/
public function handleSubscriberOptin(SubscriberEntity $subscriber, bool $shouldSubscribe): bool {
$wcSegment = $this->segmentsRepository->getWooCommerceSegment();
$segmentIds = (array)$this->settings->get(self::OPTIN_SEGMENTS_SETTING_NAME, []);
$moreSegmentsToSubscribe = [];
if (!empty($segmentIds)) {
$moreSegmentsToSubscribe = $this->segmentsRepository->findBy(['id' => $segmentIds]);
}
$signupConfirmation = $this->settings->get('signup_confirmation');
if ($shouldSubscribe) {
$subscriber->setSource(Source::WOOCOMMERCE_CHECKOUT);
if (
($subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED)
|| ((bool)$signupConfirmation['enabled'] === false)
) {
$this->subscribe($subscriber);
} else {
$this->requireSubscriptionConfirmation($subscriber);
}
$this->subscriberSegmentRepository->subscribeToSegments($subscriber, array_merge([$wcSegment], $moreSegmentsToSubscribe));
return true;
} else {
return false;
}
}
public function hideAutomateWooOptinCheckbox(): void {
if (!$this->wp->isPluginActive('automatewoo/automatewoo.php')) {
return;
}
// Hide AutomateWoo checkout opt-in so we won't end up with two opt-ins
$this->wp->removeAction(
'woocommerce_checkout_after_terms_and_conditions',
['AutomateWoo\Frontend', 'output_checkout_optin_checkbox']
);
}
private function triggerAutomateWooOptin(): void {
if (
!$this->wp->isPluginActive('automatewoo/automatewoo.php')
|| empty($_POST[self::CHECKOUT_OPTIN_INPUT_NAME])
) {
return;
}
// Emulate checkout opt-in triggering for AutomateWoo
$_POST['automatewoo_optin'] = 'On';
}
private function subscribe(SubscriberEntity $subscriber) {
$subscriber->setStatus(SubscriberEntity::STATUS_SUBSCRIBED);
if (empty($subscriber->getConfirmedIp()) && empty($subscriber->getConfirmedAt())) {
$subscriber->setConfirmedIp(Helpers::getIP());
$subscriber->setConfirmedAt(new Carbon());
}
$this->subscribersRepository->persist($subscriber);
$this->subscribersRepository->flush();
}
private function requireSubscriptionConfirmation(SubscriberEntity $subscriber) {
$subscriber->setStatus(SubscriberEntity::STATUS_UNCONFIRMED);
$this->subscribersRepository->persist($subscriber);
$this->subscribersRepository->flush();
try {
$this->confirmationEmailMailer->sendConfirmationEmailOnce($subscriber);
} catch (\Exception $e) {
// ignore errors
}
}
}
@@ -0,0 +1,70 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterOptionFieldEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Statistics\StatisticsWooCommercePurchasesRepository;
class Tracker {
/** @var StatisticsWooCommercePurchasesRepository */
private $wooPurchasesRepository;
/** @var LoggerFactory */
private $loggerFactory;
/** @var Helper */
private $wooHelper;
/** @var NewslettersRepository */
private $newslettersRepository;
public function __construct(
StatisticsWooCommercePurchasesRepository $wooPurchasesRepository,
NewslettersRepository $newslettersRepository,
Helper $wooHelper,
LoggerFactory $loggerFactory
) {
$this->wooPurchasesRepository = $wooPurchasesRepository;
$this->newslettersRepository = $newslettersRepository;
$this->wooHelper = $wooHelper;
$this->loggerFactory = $loggerFactory;
}
public function addTrackingData(array $data): array {
try {
$currency = $this->wooHelper->getWoocommerceCurrency();
$analyticsData = $this->newslettersRepository->getAnalytics();
$data['extensions']['mailpoet'] = [
'campaigns_count' => $analyticsData['campaigns_count'],
];
$campaignData = $this->formatCampaignsData($this->wooPurchasesRepository->getRevenuesByCampaigns($currency));
$data['extensions']['mailpoet'] = array_merge($data['extensions']['mailpoet'], $campaignData);
} catch (\Throwable $e) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_TRACKING)->error($e->getMessage());
return $data;
}
return $data;
}
/**
* @param array<int, array{revenue: float, campaign_id: string|null, campaign_type: string, orders_count: int}> $campaignsData
* @return array<string, string|int|float>
*/
private function formatCampaignsData(array $campaignsData): array {
return array_reduce($campaignsData, function($result, array $campaign): array {
$newsletter = $this->newslettersRepository->findOneById((int)$campaign['campaign_id']);
$keyPrefix = 'campaign_' . ($campaign['campaign_id'] ?? 0);
$result[$keyPrefix . '_revenue'] = $campaign['revenue'];
$result[$keyPrefix . '_orders_count'] = $campaign['orders_count'];
$result[$keyPrefix . '_type'] = $campaign['campaign_type'];
$result[$keyPrefix . '_event'] = $newsletter ? (string)$newsletter->getOptionValue(NewsletterOptionFieldEntity::NAME_EVENT) : '';
return $result;
}, []);
}
}
@@ -0,0 +1,122 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\WooCommerce\TransactionalEmails\Renderer;
use MailPoet\WP\Functions as WPFunctions;
class TransactionalEmailHooks {
/** @var WPFunctions */
private $wp;
/** @var SettingsController */
private $settings;
/** @var Renderer */
private $renderer;
/** @var NewslettersRepository */
private $newsletterRepository;
/** @var TransactionalEmails */
private $transactionalEmails;
public function __construct(
WPFunctions $wp,
SettingsController $settings,
Renderer $renderer,
NewslettersRepository $newsletterRepository,
TransactionalEmails $transactionalEmails
) {
$this->wp = $wp;
$this->settings = $settings;
$this->renderer = $renderer;
$this->newsletterRepository = $newsletterRepository;
$this->transactionalEmails = $transactionalEmails;
}
public function useTemplateForWoocommerceEmails() {
$this->wp->addAction('woocommerce_email', function($wcEmails) {
/** @var callable */
$emailHeaderCallback = [$wcEmails, 'email_header'];
/** @var callable */
$emailFooterCallback = [$wcEmails, 'email_footer'];
$this->wp->removeAction('woocommerce_email_header', $emailHeaderCallback);
$this->wp->removeAction('woocommerce_email_footer', $emailFooterCallback);
$this->wp->addAction('woocommerce_email_header', function($emailHeading) {
$newsletterEntity = $this->getNewsletter();
if ($newsletterEntity) {
$this->renderer->render($newsletterEntity, $emailHeading);
// The HTML is generated from a $newsletter entity and can be considered safe
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->renderer->getHTMLBeforeContent();
}
});
$this->wp->addAction('woocommerce_email_footer', function() {
// The HTML is generated from a $newsletter entity and can be considered safe
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->renderer->getHTMLAfterContent();
// Woo uses output buffer to collect the rendered email content. We read it ourselves modify it and push it back to the buffer.
$newsletterEntity = $this->getNewsletter();
$renderedEmail = ob_get_clean();
if ($newsletterEntity && $renderedEmail) {
ob_start();
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->renderer->updateRenderedContent($newsletterEntity, $renderedEmail);
}
});
$this->wp->addAction('woocommerce_email_styles', [$this, 'alterEmailStyles']);
});
}
private function getNewsletter(): ?NewsletterEntity {
if (empty($this->settings->get(TransactionalEmails::SETTING_EMAIL_ID))) {
return null;
}
$newsletter = $this->newsletterRepository->findOneById($this->settings->get(TransactionalEmails::SETTING_EMAIL_ID));
if (!$newsletter) {
// the newsletter should always be present in the database, if it s not we shouldn't keep using this feature
// we need to recreate the newsletter and turn off the feature
$this->transactionalEmails->init();
$this->settings->set('woocommerce.use_mailpoet_editor', false);
}
return $newsletter;
}
public function overrideStylesForWooEmails() {
$this->wp->addAction('option_woocommerce_email_background_color', function($value) {
$newsletter = $this->getNewsletter();
if (!$newsletter instanceof NewsletterEntity) return $value;
return $newsletter->getGlobalStyle('body', 'backgroundColor') ?? $value;
});
$this->wp->addAction('option_woocommerce_email_base_color', function($value) {
$newsletter = $this->getNewsletter();
if (!$newsletter instanceof NewsletterEntity) return $value;
return $newsletter->getGlobalStyle('woocommerce', 'brandingColor') ?? $value;
});
$this->wp->addAction('option_woocommerce_email_body_background_color', function($value) {
$newsletter = $this->getNewsletter();
if (!$newsletter instanceof NewsletterEntity) return $value;
return $newsletter->getGlobalStyle('wrapper', 'backgroundColor') ?? $value;
});
$this->wp->addAction('option_woocommerce_email_text_color', function($value) {
$newsletter = $this->getNewsletter();
if (!$newsletter instanceof NewsletterEntity) return $value;
return $newsletter->getGlobalStyle('text', 'fontColor') ?? $value;
});
}
public function alterEmailStyles($styles) {
$newsletter = $this->getNewsletter();
if (!$newsletter) {
return $styles;
}
return $this->renderer->enhanceCss($styles, $newsletter);
}
}
@@ -0,0 +1,145 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Env;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\WooCommerce\TransactionalEmails\Template;
use MailPoet\WP\Functions as WPFunctions;
class TransactionalEmails {
const SETTING_EMAIL_ID = 'woocommerce.transactional_email_id';
/** @var WPFunctions */
private $wp;
/** @var SettingsController */
private $settings;
/** @var Template */
private $template;
/** @var Helper */
private $woocommerceHelper;
/** @var array */
private $emailHeadings = [];
/** @var NewslettersRepository */
private $newslettersRepository;
public function __construct(
WPFunctions $wp,
SettingsController $settings,
Template $template,
Helper $woocommerceHelper,
NewslettersRepository $newslettersRepository
) {
$this->wp = $wp;
$this->settings = $settings;
$this->template = $template;
$this->woocommerceHelper = $woocommerceHelper;
$this->newslettersRepository = $newslettersRepository;
}
public function setupEmailHeadings() {
$this->emailHeadings = [
'new_account' => [
'option_name' => 'woocommerce_new_order_settings',
'default' => __('New Order: #{order_number}', 'woocommerce'),
],
'processing_order' => [
'option_name' => 'woocommerce_customer_processing_order_settings',
'default' => __('Thank you for your order', 'woocommerce'),
],
'completed_order' => [
'option_name' => 'woocommerce_customer_completed_order_settings',
'default' => __('Thanks for shopping with us', 'woocommerce'),
],
'customer_note' => [
'option_name' => 'woocommerce_customer_note_settings',
'default' => __('A note has been added to your order', 'woocommerce'),
],
];
}
public function init() {
$savedEmailId = (bool)$this->settings->get(self::SETTING_EMAIL_ID, false);
if (!$savedEmailId) {
$email = $this->createNewsletter();
$this->settings->set(self::SETTING_EMAIL_ID, $email->getId());
}
}
public function getEmailHeadings() {
if (empty($this->emailHeadings)) {
$this->setupEmailHeadings();
}
$values = [];
foreach ($this->emailHeadings as $name => $heading) {
$settings = $this->wp->getOption($heading['option_name']);
if (!$settings) {
$values[$name] = $this->replacePlaceholders($heading['default']);
} else {
$value = !empty($settings['heading']) ? $settings['heading'] : $heading['default'];
$values[$name] = $this->replacePlaceholders($value);
}
}
return $values;
}
private function createNewsletter() {
$wcEmailSettings = $this->getWCEmailSettings();
$newsletter = new NewsletterEntity;
$newsletter->setType(NewsletterEntity::TYPE_WC_TRANSACTIONAL_EMAIL);
$newsletter->setSubject('WooCommerce Transactional Email');
$newsletter->setBody($this->template->create($wcEmailSettings));
$this->newslettersRepository->persist($newsletter);
$this->newslettersRepository->flush();
return $newsletter;
}
private function replacePlaceholders($text) {
$title = $this->wp->wpSpecialcharsDecode($this->wp->getOption('blogname'), ENT_QUOTES);
$address = $this->wp->wpParseUrl($this->wp->homeUrl(), PHP_URL_HOST);
$orderDate = date('Y-m-d');
return str_replace(
['{site_title}', '{site_address}', '{order_date}', '{order_number}'],
[$title, $address, $orderDate, '0001'],
$text
);
}
public function getWCEmailSettings() {
$wcEmailSettings = [
'woocommerce_email_background_color' => '#f7f7f7',
'woocommerce_email_base_color' => '#333333',
'woocommerce_email_body_background_color' => '#ffffff',
'woocommerce_email_footer_text' => _x('Footer text', 'Default footer text for a WooCommerce transactional email', 'mailpoet'),
'woocommerce_email_header_image' => Env::$assetsUrl . '/img/newsletter_editor/wc-default-logo.png',
'woocommerce_email_text_color' => '#111111',
];
$result = [];
foreach ($wcEmailSettings as $name => $default) {
$value = $this->wp->getOption($name);
$key = preg_replace('/^woocommerce_email_/', '', $name);
$result[$key] = $value ?: $default;
}
$result['base_text_color'] = $this->woocommerceHelper->wcLightOrDark($result['base_color'], '#202020', '#ffffff');
if ($this->woocommerceHelper->wcHexIsLight($result['body_background_color'])) {
$result['link_color'] = $this->woocommerceHelper->wcHexIsLight($result['base_color']) ? $result['base_text_color'] : $result['base_color'];
} else {
$result['link_color'] = $this->woocommerceHelper->wcHexIsLight($result['base_color']) ? $result['base_color'] : $result['base_text_color'];
}
$result['footer_text'] = $this->replacePlaceholders($result['footer_text']);
// The footer text is placed inside a paragraph in a text block so we keep only tags we allow in the text block in the newsletter editor
$result['footer_text'] = strip_tags($result['footer_text'], '<em><strong><br><a><span><s><del>');
return $result;
}
}
@@ -0,0 +1,64 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\WooCommerce\TransactionalEmails;
if (!defined('ABSPATH')) exit;
use MailPoet\Newsletter\Editor\LayoutHelper;
use MailPoet\WooCommerce\TransactionalEmails;
class ContentPreprocessor {
public const WC_HEADING_PLACEHOLDER = '[mailpoet_woocommerce_heading_placeholder]';
public const WC_CONTENT_PLACEHOLDER = '[mailpoet_woocommerce_content_placeholder]';
public const WC_HEADING_BEFORE = '
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
<tr>
<td class="mailpoet_text" valign="top" style="padding-top:20px;padding-bottom:20px;word-break:break-word;word-wrap:break-word;">';
public const WC_HEADING_AFTER = '
</td>
</tr>
</table>';
/** @var TransactionalEmails */
private $transactionalEmails;
public function __construct(
TransactionalEmails $transactionalEmails
) {
$this->transactionalEmails = $transactionalEmails;
}
public function preprocessContent() {
return $this->renderPlaceholderBlock(self::WC_CONTENT_PLACEHOLDER);
}
public function preprocessHeader() {
$wcEmailSettings = $this->transactionalEmails->getWCEmailSettings();
$content = self::WC_HEADING_BEFORE . '<h1 id="mailpoet-woo-email-header" style="color:' . $wcEmailSettings['base_text_color'] . ';">' . self::WC_HEADING_PLACEHOLDER . '</h1>' . self::WC_HEADING_AFTER;
return $this->renderTextBlock($content, ['backgroundColor' => $wcEmailSettings['base_color']]);
}
private function renderTextBlock(string $text, array $styles = []): array {
return [
LayoutHelper::row([
LayoutHelper::col([[
'type' => 'text',
'text' => $text,
]]),
], $styles),
];
}
private function renderPlaceholderBlock(string $placeholder): array {
return [
LayoutHelper::row([
LayoutHelper::col([[
'type' => 'placeholder',
'placeholder' => $placeholder,
]]),
]),
];
}
}
@@ -0,0 +1,206 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\WooCommerce\TransactionalEmails;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\Renderer\Renderer as NewsletterRenderer;
use MailPoet\Newsletter\Shortcodes\Shortcodes;
use MailPoetVendor\csstidy;
use MailPoetVendor\csstidy_print;
class Renderer {
const CONTENT_CONTAINER_ID = 'mailpoet_woocommerce_container';
/** @var csstidy */
private $cssParser;
/** @var NewsletterRenderer */
private $renderer;
/** @var string */
private $htmlBeforeContent;
/** @var string */
private $htmlAfterContent;
/** @var Shortcodes */
private $shortcodes;
public function __construct(
csstidy $cssParser,
NewsletterRenderer $renderer,
Shortcodes $shortcodes
) {
$this->cssParser = $cssParser;
$this->htmlBeforeContent = '';
$this->htmlAfterContent = '';
$this->renderer = $renderer;
$this->shortcodes = $shortcodes;
}
public function render(NewsletterEntity $newsletter, ?string $subject = null) {
$preparedNewsletter = $this->prepareNewsletterForRendering($newsletter);
$renderedNewsletter = $this->renderer->renderAsPreview($preparedNewsletter, 'html', $subject);
$headingText = $subject ?? '';
$renderedHtml = $this->processShortcodes($preparedNewsletter, $renderedNewsletter);
$renderedHtml = str_replace(ContentPreprocessor::WC_HEADING_PLACEHOLDER, $headingText, $renderedHtml);
$html = explode(ContentPreprocessor::WC_CONTENT_PLACEHOLDER, $renderedHtml);
$this->htmlBeforeContent = $html[0];
$this->htmlAfterContent = $html[1];
}
public function getHTMLBeforeContent() {
if (empty($this->htmlBeforeContent)) {
throw new \Exception("You should call 'render' before 'getHTMLBeforeContent'");
}
return $this->htmlBeforeContent . '<!--WooContent--><div id="' . self::CONTENT_CONTAINER_ID . '"><div id="body_content"><div id="body_content_inner"><table style="width: 100%"><tr><td style="padding: 10px 20px;">';
}
public function getHTMLAfterContent() {
if (empty($this->htmlAfterContent)) {
throw new \Exception("You should call 'render' before 'getHTMLAfterContent'");
}
return '<!--WooContent--></td></tr></table></div></div></div>' . $this->htmlAfterContent;
}
/**
* In this method we alter the rendered content that is output when processing the WooCommerce email template.
* - We update inlined font-family rules in the content block generated by Woo
*/
public function updateRenderedContent(NewsletterEntity $newsletter, string $content): string {
$isSavedWithStyledWooBlock = $newsletter->getGlobalStyle('woocommerce', 'isSavedWithUpdatedStyles');
// For Backward compatibility do not apply styles for content unless the template was edited with the editor
// and user visually checked and is aware of updated styles feature.
if (!$isSavedWithStyledWooBlock) {
return $content;
}
$contentParts = explode('<!--WooContent-->', $content);
if (count($contentParts) !== 3) {
return $content;
}
[$beforeWooContent, $wooContent, $afterWooContent] = $contentParts;
$fontFamily = $newsletter->getGlobalStyle('text', 'fontFamily');
$replaceFontFamilyCallback = function ($matches) use ($fontFamily) {
$pattern = '/font-family\s*:\s*[^;]+;/i';
$style = $matches[1];
$style = preg_replace($pattern, "font-family:$fontFamily;", $style);
return 'style="' . esc_attr($style) . '"';
};
$stylePattern = '/style="(.*?)"/i';
$wooContent = (string)preg_replace_callback($stylePattern, $replaceFontFamilyCallback, $wooContent);
return implode('', [$beforeWooContent, $wooContent, $afterWooContent]);
}
/**
* In this method we alter CSS that is later inlined into the WooCommerce email template. WooCommerce use Emogrifier to inline CSS.
* The inlining is called after the rendering and after the modifications we apply to the rendered content in self::updateRenderedContent
* - We prefix the original selectors to avoid inlining those rules into content added int the MailPoet's editor.
* - We update the font-family in the original CSS if it's set in the editor.
* - We update the font-size for the inner content if it's set in the editor.
*/
public function enhanceCss(string $css, NewsletterEntity $newsletter): string {
$this->cssParser->settings['compress_colors'] = false;
$this->cssParser->parse($css);
foreach ($this->cssParser->css as $index => $rules) {
$this->cssParser->css[$index] = [];
foreach ($rules as $selectors => $properties) {
$properties = $this->updateStyleDefinition($selectors, $newsletter, $properties);
$selectors = explode(',', $selectors);
$selectors = array_map(function($selector) {
return '#' . self::CONTENT_CONTAINER_ID . ' ' . $selector;
}, $selectors);
$selectors = implode(',', $selectors);
$this->cssParser->css[$index][$selectors] = $properties;
}
}
/** @var csstidy_print */
$print = $this->cssParser->print;
$css = $print->plain();
// Enforce the special heading color for the WooCommerce email header
$wooHeadingColor = $newsletter->getGlobalStyle('woocommerce', 'headingFontColor');
if ($wooHeadingColor) {
$css .= "#mailpoet-woo-email-header { color: $wooHeadingColor !important; }";
}
return $css;
}
private function processShortcodes(NewsletterEntity $newsletter, $content) {
$this->shortcodes->setQueue(null);
$this->shortcodes->setSubscriber(null);
$this->shortcodes->setNewsletter($newsletter);
return $this->shortcodes->replace($content);
}
/**
* This method prepares the newsletter for rendering
* - We ensure that the font-family and branding color are used as default for all headings
*/
private function prepareNewsletterForRendering(NewsletterEntity $newsletter): NewsletterEntity {
$newsletterClone = clone($newsletter);
$headingFontFamily = $newsletter->getGlobalStyle('woocommerce', 'headingFontFamily');
if ($headingFontFamily) {
$newsletterClone->setGlobalStyle('h1', 'fontFamily', $headingFontFamily);
$newsletterClone->setGlobalStyle('h2', 'fontFamily', $headingFontFamily);
$newsletterClone->setGlobalStyle('h3', 'fontFamily', $headingFontFamily);
}
$brandingColor = $newsletter->getGlobalStyle('woocommerce', 'brandingColor');
$contentHeadingColor = $newsletter->getGlobalStyle('woocommerce', 'contentHeadingFontColor') ?? $brandingColor;
if ($contentHeadingColor) {
$newsletterClone->setGlobalStyle('h1', 'color', $contentHeadingColor);
$newsletterClone->setGlobalStyle('h2', 'color', $contentHeadingColor);
$newsletterClone->setGlobalStyle('h3', 'color', $contentHeadingColor);
}
return $newsletterClone;
}
private function updateStyleDefinition(string $selectors, NewsletterEntity $newsletter, $properties) {
// For Backward compatibility do not apply styles for content unless the template was edited with the editor
// and user visually checked and is aware of updated styles feature.
$isSavedWithStyledWooBlock = $newsletter->getGlobalStyle('woocommerce', 'isSavedWithUpdatedStyles');
if (!$isSavedWithStyledWooBlock) {
return $properties;
}
if (!is_array($properties)) {
$properties = [];
}
$fontFamily = $newsletter->getGlobalStyle('text', 'fontFamily');
$headingFontFamily = $newsletter->getGlobalStyle('woocommerce', 'headingFontFamily');
$fontSize = $newsletter->getGlobalStyle('text', 'fontSize');
$brandingColor = $newsletter->getGlobalStyle('woocommerce', 'brandingColor');
$contentHeadingColor = $newsletter->getGlobalStyle('woocommerce', 'contentHeadingFontColor') ?? $brandingColor;
// Update font family if it's set in the editor
if ($fontFamily && !empty($properties['font-family'])) {
$properties['font-family'] = $fontFamily;
}
// Update font size for inner content
if ($fontSize && ($selectors === '#body_content_inner')) {
$properties['font-size'] = $fontSize;
}
// Update heading font sizes and font family
$supportedHeadings = ['h1', 'h2', 'h3'];
foreach ($supportedHeadings as $heading) {
$headingFontSize = $newsletter->getGlobalStyle($heading, 'fontSize');
if ($headingFontSize && ($selectors === $heading)) {
$properties['font-size'] = $headingFontSize;
}
if ($headingFontFamily && ($selectors === $heading)) {
$properties['font-family'] = $headingFontFamily;
}
if ($contentHeadingColor && ($selectors === $heading)) {
$properties['color'] = $contentHeadingColor;
}
}
return $properties;
}
}
@@ -0,0 +1,724 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\WooCommerce\TransactionalEmails;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Env;
class Template {
public function create($wcEmailSettings) {
$socialIconUrl = Env::$assetsUrl . '/img/newsletter_editor/social-icons';
return [
'content' =>
[
'type' => 'container',
'columnLayout' => false,
'orientation' => 'vertical',
'image' =>
[
'src' => null,
'display' => 'scale',
],
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
],
'blocks' =>
[
0 =>
[
'type' => 'container',
'columnLayout' => false,
'orientation' => 'horizontal',
'image' =>
[
'src' => null,
'display' => 'scale',
],
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
],
'blocks' =>
[
0 =>
[
'type' => 'container',
'columnLayout' => false,
'orientation' => 'vertical',
'image' =>
[
'src' => null,
'display' => 'scale',
],
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
],
'blocks' =>
[
0 =>
[
'type' => 'spacer',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'height' => '20px',
],
],
],
1 =>
[
'type' => 'image',
'link' => '',
'src' => $wcEmailSettings['header_image'],
'alt' => '',
'fullWidth' => false,
'width' => '180px',
'height' => '362px',
'styles' =>
[
'block' =>
[
'textAlign' => 'center',
],
],
],
2 =>
[
'type' => 'spacer',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'height' => '20px',
],
],
],
],
],
],
],
1 =>
[
'type' => 'woocommerceHeading',
],
2 =>
[
'type' => 'container',
'columnLayout' => false,
'orientation' => 'horizontal',
'image' =>
[
'src' => null,
'display' => 'scale',
],
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
],
'blocks' =>
[
0 =>
[
'type' => 'container',
'columnLayout' => false,
'orientation' => 'vertical',
'image' =>
[
'src' => null,
'display' => 'scale',
],
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
],
'blocks' =>
[
0 =>
[
'type' => 'spacer',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'height' => '20px',
],
],
],
],
],
],
],
3 =>
[
'type' => 'woocommerceContent',
],
4 =>
[
'type' => 'container',
'columnLayout' => false,
'orientation' => 'horizontal',
'image' =>
[
'src' => null,
'display' => 'scale',
],
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
],
'blocks' =>
[
0 =>
[
'type' => 'container',
'columnLayout' => false,
'orientation' => 'vertical',
'image' =>
[
'src' => null,
'display' => 'scale',
],
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
],
'blocks' =>
[
0 =>
[
'type' => 'spacer',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'height' => '20px',
],
],
],
],
],
],
],
5 =>
[
'type' => 'container',
'columnLayout' => false,
'orientation' => 'horizontal',
'image' =>
[
'src' => null,
'display' => 'scale',
],
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
],
'blocks' =>
[
0 =>
[
'type' => 'container',
'columnLayout' => false,
'orientation' => 'vertical',
'image' =>
[
'src' => null,
'display' => 'scale',
],
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
],
'blocks' =>
[
0 =>
[
'type' => 'spacer',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'height' => '20px',
],
],
],
1 =>
[
'type' => 'text',
'text' => '<p style="text-align: center;">' . $wcEmailSettings['footer_text'] . '</p>',
],
],
],
],
],
],
],
'globalStyles' =>
[
'text' =>
[
'fontColor' => $wcEmailSettings['text_color'],
'fontFamily' => 'Arial',
'fontSize' => '14px',
'lineHeight' => '1.6',
],
'h1' =>
[
'fontColor' => $wcEmailSettings['base_color'],
'fontFamily' => 'Source Sans Pro',
'fontSize' => '36px',
'lineHeight' => '1.6',
],
'h2' =>
[
'fontColor' => $wcEmailSettings['base_color'],
'fontFamily' => 'Verdana',
'fontSize' => '24px',
'lineHeight' => '1.6',
],
'h3' =>
[
'fontColor' => $wcEmailSettings['base_color'],
'fontFamily' => 'Trebuchet MS',
'fontSize' => '22px',
'lineHeight' => '1.6',
],
'link' =>
[
'fontColor' => $wcEmailSettings['link_color'],
'textDecoration' => 'underline',
],
'wrapper' =>
[
'backgroundColor' => $wcEmailSettings['body_background_color'],
],
'body' =>
[
'backgroundColor' => $wcEmailSettings['background_color'],
],
'woocommerce' =>
[
'brandingColor' => $wcEmailSettings['base_color'],
'headingFontColor' => $wcEmailSettings['base_text_color'],
'headingFontFamily' => 'Arial',
],
],
'blockDefaults' =>
[
'automatedLatestContent' =>
[
'amount' => '5',
'withLayout' => false,
'contentType' => 'post',
'inclusionType' => 'include',
'displayType' => 'excerpt',
'titleFormat' => 'h1',
'titleAlignment' => 'left',
'titleIsLink' => false,
'imageFullWidth' => false,
'featuredImagePosition' => 'belowTitle',
'showAuthor' => 'no',
'authorPrecededBy' => 'Author:',
'showCategories' => 'no',
'categoriesPrecededBy' => 'Categories:',
'readMoreType' => 'button',
'readMoreText' => 'Read more',
'readMoreButton' =>
[
'text' => 'Read more',
'url' => '[postLink]',
'context' => 'automatedLatestContent.readMoreButton',
'styles' =>
[
'block' =>
[
'backgroundColor' => '#2ea1cd',
'borderColor' => '#0074a2',
'borderWidth' => '1px',
'borderRadius' => '5px',
'borderStyle' => 'solid',
'width' => '180px',
'lineHeight' => '40px',
'fontColor' => '#ffffff',
'fontFamily' => 'Verdana',
'fontSize' => '18px',
'fontWeight' => 'normal',
'textAlign' => 'center',
],
],
],
'sortBy' => 'newest',
'showDivider' => true,
'divider' =>
[
'context' => 'automatedLatestContent.divider',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'padding' => '13px',
'borderStyle' => 'solid',
'borderWidth' => '3px',
'borderColor' => '#aaaaaa',
],
],
],
'backgroundColor' => '#ffffff',
'backgroundColorAlternate' => '#eeeeee',
],
'automatedLatestContentLayout' =>
[
'amount' => '5',
'withLayout' => true,
'contentType' => 'post',
'inclusionType' => 'include',
'displayType' => 'excerpt',
'titleFormat' => 'h1',
'titleAlignment' => 'left',
'titleIsLink' => false,
'imageFullWidth' => false,
'featuredImagePosition' => 'alternate',
'showAuthor' => 'no',
'authorPrecededBy' => 'Author:',
'showCategories' => 'no',
'categoriesPrecededBy' => 'Categories:',
'readMoreType' => 'button',
'readMoreText' => 'Read more',
'readMoreButton' =>
[
'text' => 'Read more',
'url' => '[postLink]',
'context' => 'automatedLatestContentLayout.readMoreButton',
'styles' =>
[
'block' =>
[
'backgroundColor' => '#2ea1cd',
'borderColor' => '#0074a2',
'borderWidth' => '1px',
'borderRadius' => '5px',
'borderStyle' => 'solid',
'width' => '180px',
'lineHeight' => '40px',
'fontColor' => '#ffffff',
'fontFamily' => 'Verdana',
'fontSize' => '18px',
'fontWeight' => 'normal',
'textAlign' => 'center',
],
],
],
'sortBy' => 'newest',
'showDivider' => true,
'divider' =>
[
'context' => 'automatedLatestContentLayout.divider',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'padding' => '13px',
'borderStyle' => 'solid',
'borderWidth' => '3px',
'borderColor' => '#aaaaaa',
],
],
],
'backgroundColor' => '#ffffff',
'backgroundColorAlternate' => '#eeeeee',
],
'button' =>
[
'text' => 'Button',
'url' => '',
'styles' =>
[
'block' =>
[
'backgroundColor' => '#2ea1cd',
'borderColor' => '#0074a2',
'borderWidth' => '1px',
'borderRadius' => '5px',
'borderStyle' => 'solid',
'width' => '180px',
'lineHeight' => '40px',
'fontColor' => '#ffffff',
'fontFamily' => 'Verdana',
'fontSize' => '18px',
'fontWeight' => 'normal',
'textAlign' => 'center',
],
],
],
'divider' =>
[
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'padding' => '13px',
'borderStyle' => 'solid',
'borderWidth' => '3px',
'borderColor' => '#aaaaaa',
],
],
],
'footer' =>
[
'text' => '<p><a href="[link:subscription_unsubscribe_url]">Unsubscribe</a> | <a href="[link:subscription_manage_url]">Manage subscription</a><br />Add your postal address here!</p>',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
'text' =>
[
'fontColor' => '#222222',
'fontFamily' => 'Arial',
'fontSize' => '12px',
'textAlign' => 'center',
],
'link' =>
[
'fontColor' => '#6cb7d4',
'textDecoration' => 'none',
],
],
],
'posts' =>
[
'amount' => '10',
'withLayout' => true,
'contentType' => 'post',
'postStatus' => 'publish',
'inclusionType' => 'include',
'displayType' => 'excerpt',
'titleFormat' => 'h1',
'titleAlignment' => 'left',
'titleIsLink' => false,
'imageFullWidth' => false,
'featuredImagePosition' => 'alternate',
'showAuthor' => 'no',
'authorPrecededBy' => 'Author:',
'showCategories' => 'no',
'categoriesPrecededBy' => 'Categories:',
'readMoreType' => 'link',
'readMoreText' => 'Read more',
'readMoreButton' =>
[
'text' => 'Read more',
'url' => '[postLink]',
'context' => 'posts.readMoreButton',
'styles' =>
[
'block' =>
[
'backgroundColor' => '#2ea1cd',
'borderColor' => '#0074a2',
'borderWidth' => '1px',
'borderRadius' => '5px',
'borderStyle' => 'solid',
'width' => '180px',
'lineHeight' => '40px',
'fontColor' => '#ffffff',
'fontFamily' => 'Verdana',
'fontSize' => '18px',
'fontWeight' => 'normal',
'textAlign' => 'center',
],
],
],
'sortBy' => 'newest',
'showDivider' => true,
'divider' =>
[
'context' => 'posts.divider',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'padding' => '13px',
'borderStyle' => 'solid',
'borderWidth' => '3px',
'borderColor' => '#aaaaaa',
],
],
],
'backgroundColor' => '#ffffff',
'backgroundColorAlternate' => '#eeeeee',
],
'products' =>
[
'amount' => '10',
'withLayout' => true,
'contentType' => 'product',
'postStatus' => 'publish',
'inclusionType' => 'include',
'displayType' => 'excerpt',
'titleFormat' => 'h1',
'titleAlignment' => 'left',
'titleIsLink' => false,
'imageFullWidth' => false,
'featuredImagePosition' => 'alternate',
'pricePosition' => 'below',
'readMoreType' => 'link',
'readMoreText' => 'Buy now',
'readMoreButton' =>
[
'text' => 'Buy now',
'url' => '[postLink]',
'context' => 'posts.readMoreButton',
'styles' =>
[
'block' =>
[
'backgroundColor' => '#2ea1cd',
'borderColor' => '#0074a2',
'borderWidth' => '1px',
'borderRadius' => '5px',
'borderStyle' => 'solid',
'width' => '180px',
'lineHeight' => '40px',
'fontColor' => '#ffffff',
'fontFamily' => 'Verdana',
'fontSize' => '18px',
'fontWeight' => 'normal',
'textAlign' => 'center',
],
],
],
'sortBy' => 'newest',
'showDivider' => true,
'divider' =>
[
'context' => 'posts.divider',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'padding' => '13px',
'borderStyle' => 'solid',
'borderWidth' => '3px',
'borderColor' => '#aaaaaa',
],
],
],
'backgroundColor' => '#ffffff',
'backgroundColorAlternate' => '#eeeeee',
],
'social' =>
[
'iconSet' => 'default',
'styles' =>
[
'block' =>
[
'textAlign' => 'center',
],
],
'icons' =>
[
0 =>
[
'type' => 'socialIcon',
'iconType' => 'facebook',
'link' => 'http://www.facebook.com',
'image' => $socialIconUrl . '/01-social/Facebook.png',
'height' => '32px',
'width' => '32px',
'text' => 'Facebook',
],
1 =>
[
'type' => 'socialIcon',
'iconType' => 'twitter',
'link' => 'http://www.twitter.com',
'image' => $socialIconUrl . '/01-social/Twitter.png',
'height' => '32px',
'width' => '32px',
'text' => 'Twitter',
],
],
],
'spacer' =>
[
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
'height' => '20px',
],
],
'type' => 'spacer',
],
'header' =>
[
'text' => 'Display problems?&nbsp;<a href="[link:newsletter_view_in_browser_url]">Open this email in your web browser.</a>',
'styles' =>
[
'block' =>
[
'backgroundColor' => 'transparent',
],
'text' =>
[
'fontColor' => '#222222',
'fontFamily' => 'Arial',
'fontSize' => '12px',
'textAlign' => 'center',
],
'link' =>
[
'fontColor' => '#6cb7d4',
'textDecoration' => 'underline',
],
],
],
],
];
}
}
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce\WooCommerceSubscriptions;
if (!defined('ABSPATH')) exit;
use MailPoet\WP\Functions;
class Helper {
private $wp;
public function __construct(
Functions $wp
) {
$this->wp = $wp;
}
public function isWooCommerceSubscriptionsActive() {
return $this->wp->isPluginActive('woocommerce-subscriptions/woocommerce-subscriptions.php');
}
/**
* @return array<string, string>
*/
public function wcsGetSubscriptionStatuses(): array {
if (!function_exists('wcs_get_subscription_statuses')) {
return [];
}
return wcs_get_subscription_statuses();
}
public function wcsGetBillingPeriodStrings(): array {
if (!function_exists('wcs_get_subscription_period_strings')) {
return [];
}
$strings = wcs_get_subscription_period_strings();
if (!is_array($strings)) {
return [];
}
return $strings;
}
public function wcsGetSubscriptionTrialPeriodStrings(): array {
if (!function_exists('wcs_get_subscription_trial_period_strings')) {
return [];
}
return wcs_get_subscription_trial_period_strings();
}
/**
* @param int $id
* @return false|\WC_Subscription
*/
public function wcsGetSubscription(int $id) {
if (!function_exists('wcs_get_subscription')) {
return false;
}
return wcs_get_subscription($id);
}
}
@@ -0,0 +1,55 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\CronHelper;
use MailPoet\Router\Endpoints\CronDaemon;
use MailPoet\Settings\SettingsController;
class WooSystemInfo {
private $cronHelper;
/** @var SettingsController */
private $settings;
public function __construct(
CronHelper $cronHelper,
SettingsController $settings
) {
$this->cronHelper = $cronHelper;
$this->settings = $settings;
}
public function sendingMethod(): string {
return $this->settings->get('mta.method');
}
public function transactionalEmails(): string {
return $this->settings->get('send_transactional_emails') ?
__('Current sending method', 'mailpoet') :
__('Default WordPress sending method', 'mailpoet');
}
public function taskSchedulerMethod(): string {
return $this->settings->get('cron_trigger.method');
}
public function cronPingUrl(): string {
return $this->cronHelper->getCronUrl(CronDaemon::ACTION_PING);
}
public function toArray(): array {
return [
'sending_method' => $this->sendingMethod(),
'transactional_emails' => $this->transactionalEmails(),
'task_scheduler_method' => $this->taskSchedulerMethod(),
'cron_ping_url' => $this->cronPingUrl(),
];
}
}
@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);
namespace MailPoet\WooCommerce;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Renderer;
class WooSystemInfoController {
/** @var WooSystemInfo */
private $systemInfo;
private $renderer;
public function __construct(
WooSystemInfo $systemInfo,
Renderer $renderer
) {
$this->systemInfo = $systemInfo;
$this->renderer = $renderer;
}
public function render() {
$output = $this->renderer->render('woo_system_info.html', [
'system_info' => $this->systemInfo->toArray(),
]);
// We are in control of the template and the data can be considered safe at this point
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function addFields($response) {
$response->data['mailpoet'] = $this->systemInfo->toArray();
return $response;
}
public function addSchema($schema) {
$schema['mailpoet'] = [
[
'description' => __('MailPoet', 'mailpoet'),
'type' => 'object',
'context' => ['view'],
'readonly' => true,
'properties' => [
'sending_method' => [
'description' => __('What method is used to sent out newsletters?', 'mailpoet'),
'type' => 'boolean',
'context' => ['view'],
'readonly' => true,
],
'transactional_emails' => [
'description' => __('With which method are transactional emails sent?', 'mailpoet'),
'type' => 'string',
'context' => ['view'],
'readonly' => true,
],
'task_scheduler_method' => [
'description' => __('What method controls the cron job?', 'mailpoet'),
'type' => 'string',
'context' => ['view'],
'readonly' => true,
],
'cron_ping_url' => [
'description' => __('The URL which needs to be pinged to get the cron started?', 'mailpoet'),
'type' => 'string',
'context' => ['view'],
'readonly' => true,
],
],
],
];
return $schema;
}
}
@@ -0,0 +1 @@
<?php