210 lines
7.1 KiB
PHP
210 lines
7.1 KiB
PHP
<?php declare(strict_types = 1);
|
||
|
||
namespace MailPoet\AutomaticEmails\WooCommerce\Events;
|
||
|
||
if (!defined('ABSPATH')) exit;
|
||
|
||
|
||
use MailPoet\AutomaticEmails\WooCommerce\WooCommerce as WooCommerceEmail;
|
||
use MailPoet\Entities\SubscriberEntity;
|
||
use MailPoet\Newsletter\Scheduler\AutomaticEmailScheduler;
|
||
use MailPoet\Statistics\Track\SubscriberActivityTracker;
|
||
use MailPoet\Statistics\Track\SubscriberCookie;
|
||
use MailPoet\Subscribers\SubscribersRepository;
|
||
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
|
||
use MailPoet\WP\Functions as WPFunctions;
|
||
|
||
class AbandonedCart {
|
||
const SLUG = 'woocommerce_abandoned_shopping_cart';
|
||
const TASK_META_NAME = 'cart_product_ids';
|
||
|
||
|
||
const HOOK_SCHEDULE = 'mailpoet_abandoned_cart_schedule';
|
||
const HOOK_RE_SCHEDULE = 'mailpoet_abandoned_cart_reschedule';
|
||
const HOOK_CANCEL = 'mailpoet_abandoned_cart_cancel';
|
||
|
||
/** @var WPFunctions */
|
||
private $wp;
|
||
|
||
/** @var WooCommerceHelper */
|
||
private $wooCommerceHelper;
|
||
|
||
/** @var SubscriberCookie */
|
||
private $subscriberCookie;
|
||
|
||
/** @var AutomaticEmailScheduler */
|
||
private $scheduler;
|
||
|
||
/** @var SubscriberActivityTracker */
|
||
private $subscriberActivityTracker;
|
||
|
||
/** @var SubscribersRepository */
|
||
private $subscribersRepository;
|
||
|
||
public function __construct(
|
||
WPFunctions $wp,
|
||
WooCommerceHelper $wooCommerceHelper,
|
||
SubscriberCookie $subscriberCookie,
|
||
SubscriberActivityTracker $subscriberActivityTracker,
|
||
AutomaticEmailScheduler $scheduler,
|
||
SubscribersRepository $subscribersRepository
|
||
) {
|
||
$this->wp = $wp;
|
||
$this->wooCommerceHelper = $wooCommerceHelper;
|
||
$this->subscriberCookie = $subscriberCookie;
|
||
$this->subscriberActivityTracker = $subscriberActivityTracker;
|
||
$this->scheduler = $scheduler;
|
||
$this->subscribersRepository = $subscribersRepository;
|
||
}
|
||
|
||
public function getEventDetails() {
|
||
return [
|
||
'slug' => self::SLUG,
|
||
'title' => _x('Abandoned Shopping Cart', 'This is the name of a type of automatic email for ecommerce. Those emails are sent automatically when a customer adds product to his shopping cart but never complete the checkout process.', 'mailpoet'),
|
||
'description' => __('Send an email to logged-in visitors who have items in their shopping carts but left your website without checking out. Can convert up to 5% of abandoned carts.', 'mailpoet'),
|
||
'listingScheduleDisplayText' => _x('Send the email when a customer abandons their cart.', 'Description of Abandoned Shopping Cart email', 'mailpoet'),
|
||
'afterDelayText' => __('after abandoning the cart', 'mailpoet'),
|
||
'badge' => [
|
||
'text' => __('Must-have', 'mailpoet'),
|
||
'style' => 'red',
|
||
],
|
||
'timeDelayValues' => [
|
||
'minutes' => [
|
||
'text' => __('minute(s)', 'mailpoet'),
|
||
'displayAfterTimeNumberField' => true,
|
||
],
|
||
'hours' => [
|
||
'text' => __('hour(s)', 'mailpoet'),
|
||
'displayAfterTimeNumberField' => true,
|
||
],
|
||
'days' => [
|
||
'text' => __('day(s)', 'mailpoet'),
|
||
'displayAfterTimeNumberField' => true,
|
||
],
|
||
'weeks' => [
|
||
'text' => __('week(s)', 'mailpoet'),
|
||
'displayAfterTimeNumberField' => true,
|
||
],
|
||
],
|
||
'defaultAfterTimeType' => 'minutes',
|
||
'schedulingReadMoreLink' => [
|
||
'link' => 'https://www.mailpoet.com/blog/abandoned-cart-woocommerce',
|
||
'text' => __('We recommend setting up 3 abandoned cart emails. Here’s why.', 'mailpoet'),
|
||
],
|
||
];
|
||
}
|
||
|
||
public function init() {
|
||
if (!$this->wooCommerceHelper->isWooCommerceActive()) {
|
||
return;
|
||
}
|
||
|
||
// item added to cart (not fired on quantity changes)
|
||
$this->wp->addAction(
|
||
'woocommerce_add_to_cart',
|
||
[$this, 'handleCartChange'],
|
||
10
|
||
);
|
||
|
||
// item removed from cart (not fired on quantity changes, not even change to zero)
|
||
$this->wp->addAction(
|
||
'woocommerce_cart_item_removed',
|
||
[$this, 'handleCartChange'],
|
||
10
|
||
);
|
||
|
||
// item quantity updated (not fired when quantity updated to zero)
|
||
$this->wp->addAction(
|
||
'woocommerce_after_cart_item_quantity_update',
|
||
[$this, 'handleCartChange'],
|
||
10
|
||
);
|
||
|
||
// item quantity set to zero (it removes the item but does not fire remove event)
|
||
$this->wp->addAction(
|
||
'woocommerce_before_cart_item_quantity_zero',
|
||
[$this, 'handleCartChange'],
|
||
10
|
||
);
|
||
|
||
// cart emptied (not called when all items removed)
|
||
$this->wp->addAction(
|
||
'woocommerce_cart_emptied',
|
||
[$this, 'handleCartChange'],
|
||
10
|
||
);
|
||
|
||
// undo removal of item from cart or cart emptying (does not fire any other cart change hook)
|
||
$this->wp->addAction(
|
||
'woocommerce_cart_item_restored',
|
||
[$this, 'handleCartChange'],
|
||
10
|
||
);
|
||
|
||
$this->subscriberActivityTracker->registerCallback(
|
||
'mailpoet_abandoned_cart',
|
||
[$this, 'handleSubscriberActivity']
|
||
);
|
||
}
|
||
|
||
public function handleCartChange() {
|
||
$cart = $this->wooCommerceHelper->WC()->cart;
|
||
|
||
$currentAction = current_action();
|
||
if ($currentAction !== 'woocommerce_cart_emptied' && $cart && !$cart->is_empty()) {
|
||
$this->scheduleAbandonedCartEmail($this->getCartProductIds($cart));
|
||
} else {
|
||
$this->cancelAbandonedCartEmail();
|
||
}
|
||
}
|
||
|
||
public function handleSubscriberActivity(SubscriberEntity $subscriber) {
|
||
// on subscriber activity on site reschedule all currently scheduled (not yet sent) emails for given subscriber
|
||
// (it tracks at most once per minute to avoid processing many calls at the same time, i.e. AJAX)
|
||
$this->rescheduleAbandonedCartEmail($subscriber);
|
||
}
|
||
|
||
private function getCartProductIds($cart) {
|
||
$cartItems = $cart->get_cart() ?: [];
|
||
return array_column($cartItems, 'product_id');
|
||
}
|
||
|
||
private function scheduleAbandonedCartEmail(array $cartProductIds = []) {
|
||
$subscriber = $this->getSubscriber();
|
||
if (!$subscriber || $subscriber->getStatus() !== SubscriberEntity::STATUS_SUBSCRIBED) {
|
||
return;
|
||
}
|
||
|
||
$this->wp->doAction(self::HOOK_SCHEDULE, $subscriber, $cartProductIds);
|
||
$meta = [self::TASK_META_NAME => $cartProductIds];
|
||
$this->scheduler->scheduleOrRescheduleAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber, $meta);
|
||
}
|
||
|
||
private function rescheduleAbandonedCartEmail(SubscriberEntity $subscriber) {
|
||
$this->wp->doAction(self::HOOK_RE_SCHEDULE, $subscriber);
|
||
$this->scheduler->rescheduleAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber);
|
||
}
|
||
|
||
private function cancelAbandonedCartEmail() {
|
||
$subscriber = $this->getSubscriber();
|
||
if (!$subscriber) {
|
||
return;
|
||
}
|
||
$this->wp->doAction(self::HOOK_CANCEL, $subscriber);
|
||
$this->scheduler->cancelAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber);
|
||
}
|
||
|
||
private function getSubscriber(): ?SubscriberEntity {
|
||
$wpUser = $this->wp->wpGetCurrentUser();
|
||
if ($wpUser->exists()) {
|
||
return $this->subscribersRepository->findOneBy(['wpUserId' => $wpUser->ID]);
|
||
}
|
||
|
||
// if user not logged in, try to find subscriber by cookie
|
||
$subscriberId = $this->subscriberCookie->getSubscriberId();
|
||
if ($subscriberId) {
|
||
return $this->subscribersRepository->findOneById($subscriberId);
|
||
}
|
||
return null;
|
||
}
|
||
}
|