AbandonedCart.php
1 year ago
FirstPurchase.php
3 months ago
PurchasedInCategory.php
3 months ago
PurchasedProduct.php
3 months ago
index.php
3 years ago
AbandonedCart.php
243 lines
| 1 | <?php declare(strict_types = 1); |
| 2 | |
| 3 | namespace MailPoet\AutomaticEmails\WooCommerce\Events; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use MailPoet\AutomaticEmails\WooCommerce\WooCommerce as WooCommerceEmail; |
| 9 | use MailPoet\Entities\SubscriberEntity; |
| 10 | use MailPoet\Newsletter\Scheduler\AutomaticEmailScheduler; |
| 11 | use MailPoet\Statistics\Track\SubscriberActivityTracker; |
| 12 | use MailPoet\Statistics\Track\SubscriberCookie; |
| 13 | use MailPoet\Subscribers\SubscribersRepository; |
| 14 | use MailPoet\WooCommerce\Helper as WooCommerceHelper; |
| 15 | use MailPoet\WP\Functions as WPFunctions; |
| 16 | |
| 17 | class AbandonedCart { |
| 18 | const SLUG = 'woocommerce_abandoned_shopping_cart'; |
| 19 | const TASK_META_NAME = 'cart_product_ids'; |
| 20 | |
| 21 | |
| 22 | const HOOK_SCHEDULE = 'mailpoet_abandoned_cart_schedule'; |
| 23 | const HOOK_RE_SCHEDULE = 'mailpoet_abandoned_cart_reschedule'; |
| 24 | const HOOK_CANCEL = 'mailpoet_abandoned_cart_cancel'; |
| 25 | |
| 26 | /** @var WPFunctions */ |
| 27 | private $wp; |
| 28 | |
| 29 | /** @var WooCommerceHelper */ |
| 30 | private $wooCommerceHelper; |
| 31 | |
| 32 | /** @var SubscriberCookie */ |
| 33 | private $subscriberCookie; |
| 34 | |
| 35 | /** @var AutomaticEmailScheduler */ |
| 36 | private $scheduler; |
| 37 | |
| 38 | /** @var SubscriberActivityTracker */ |
| 39 | private $subscriberActivityTracker; |
| 40 | |
| 41 | /** @var SubscribersRepository */ |
| 42 | private $subscribersRepository; |
| 43 | |
| 44 | /** @var bool */ |
| 45 | private $loadSavedCartAfterLogin; |
| 46 | |
| 47 | public function __construct( |
| 48 | WPFunctions $wp, |
| 49 | WooCommerceHelper $wooCommerceHelper, |
| 50 | SubscriberCookie $subscriberCookie, |
| 51 | SubscriberActivityTracker $subscriberActivityTracker, |
| 52 | AutomaticEmailScheduler $scheduler, |
| 53 | SubscribersRepository $subscribersRepository |
| 54 | ) { |
| 55 | $this->wp = $wp; |
| 56 | $this->wooCommerceHelper = $wooCommerceHelper; |
| 57 | $this->subscriberCookie = $subscriberCookie; |
| 58 | $this->subscriberActivityTracker = $subscriberActivityTracker; |
| 59 | $this->scheduler = $scheduler; |
| 60 | $this->subscribersRepository = $subscribersRepository; |
| 61 | } |
| 62 | |
| 63 | public function getEventDetails() { |
| 64 | return [ |
| 65 | 'slug' => self::SLUG, |
| 66 | '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'), |
| 67 | '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'), |
| 68 | 'listingScheduleDisplayText' => _x('Send the email when a customer abandons their cart.', 'Description of Abandoned Shopping Cart email', 'mailpoet'), |
| 69 | 'afterDelayText' => __('after abandoning the cart', 'mailpoet'), |
| 70 | 'badge' => [ |
| 71 | 'text' => __('Must-have', 'mailpoet'), |
| 72 | 'style' => 'red', |
| 73 | ], |
| 74 | 'timeDelayValues' => [ |
| 75 | 'minutes' => [ |
| 76 | 'text' => __('minute(s)', 'mailpoet'), |
| 77 | 'displayAfterTimeNumberField' => true, |
| 78 | ], |
| 79 | 'hours' => [ |
| 80 | 'text' => __('hour(s)', 'mailpoet'), |
| 81 | 'displayAfterTimeNumberField' => true, |
| 82 | ], |
| 83 | 'days' => [ |
| 84 | 'text' => __('day(s)', 'mailpoet'), |
| 85 | 'displayAfterTimeNumberField' => true, |
| 86 | ], |
| 87 | 'weeks' => [ |
| 88 | 'text' => __('week(s)', 'mailpoet'), |
| 89 | 'displayAfterTimeNumberField' => true, |
| 90 | ], |
| 91 | ], |
| 92 | 'defaultAfterTimeType' => 'minutes', |
| 93 | 'schedulingReadMoreLink' => [ |
| 94 | 'link' => 'https://www.mailpoet.com/blog/abandoned-cart-woocommerce', |
| 95 | 'text' => __('We recommend setting up 3 abandoned cart emails. Here’s why.', 'mailpoet'), |
| 96 | ], |
| 97 | ]; |
| 98 | } |
| 99 | |
| 100 | public function init() { |
| 101 | if (!$this->wooCommerceHelper->isWooCommerceActive()) { |
| 102 | return; |
| 103 | } |
| 104 | |
| 105 | // item added to cart (not fired on quantity changes) |
| 106 | $this->wp->addAction( |
| 107 | 'woocommerce_add_to_cart', |
| 108 | [$this, 'handleCartChange'], |
| 109 | 10 |
| 110 | ); |
| 111 | |
| 112 | // item removed from cart (not fired on quantity changes, not even change to zero) |
| 113 | $this->wp->addAction( |
| 114 | 'woocommerce_cart_item_removed', |
| 115 | [$this, 'handleCartChange'], |
| 116 | 10 |
| 117 | ); |
| 118 | |
| 119 | // item quantity updated (not fired when quantity updated to zero) |
| 120 | $this->wp->addAction( |
| 121 | 'woocommerce_after_cart_item_quantity_update', |
| 122 | [$this, 'handleCartChange'], |
| 123 | 10 |
| 124 | ); |
| 125 | |
| 126 | // item quantity set to zero |
| 127 | $this->wp->addAction( |
| 128 | 'woocommerce_remove_cart_item', |
| 129 | [$this, 'handleCartChange'], |
| 130 | 10 |
| 131 | ); |
| 132 | |
| 133 | // cart emptied (not called when all items removed) |
| 134 | $this->wp->addAction( |
| 135 | 'woocommerce_cart_emptied', |
| 136 | [$this, 'handleCartChange'], |
| 137 | 10 |
| 138 | ); |
| 139 | |
| 140 | // undo removal of item from cart or cart emptying (does not fire any other cart change hook) |
| 141 | $this->wp->addAction( |
| 142 | 'woocommerce_cart_item_restored', |
| 143 | [$this, 'handleCartChange'], |
| 144 | 10 |
| 145 | ); |
| 146 | |
| 147 | // we should handle loading cart from session if user logs in |
| 148 | $this->wp->addAction( |
| 149 | 'woocommerce_load_cart_from_session', |
| 150 | [$this, 'handleUserLogin'], |
| 151 | 10 |
| 152 | ); |
| 153 | |
| 154 | // cart loaded from session (only processed after login, not on every page load) |
| 155 | $this->wp->addAction( |
| 156 | 'woocommerce_cart_loaded_from_session', |
| 157 | [$this, 'handleCartChangeOnLogin'], |
| 158 | 10 |
| 159 | ); |
| 160 | |
| 161 | $this->subscriberActivityTracker->registerCallback( |
| 162 | 'mailpoet_abandoned_cart', |
| 163 | [$this, 'handleSubscriberActivity'] |
| 164 | ); |
| 165 | } |
| 166 | |
| 167 | public function handleCartChange() { |
| 168 | $cart = $this->wooCommerceHelper->WC()->cart; |
| 169 | |
| 170 | $currentAction = current_action(); |
| 171 | if ($currentAction !== 'woocommerce_cart_emptied' && $cart && !$cart->is_empty()) { |
| 172 | $this->scheduleAbandonedCartEmail($this->getCartProductIds($cart)); |
| 173 | } else { |
| 174 | $this->cancelAbandonedCartEmail(); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | public function handleUserLogin() { |
| 179 | $wpUserId = $this->wp->getCurrentUserId(); |
| 180 | if (!$wpUserId) { |
| 181 | return false; |
| 182 | } |
| 183 | $this->loadSavedCartAfterLogin = (bool)$this->wp->getUserMeta($wpUserId, '_woocommerce_load_saved_cart_after_login', true); |
| 184 | } |
| 185 | |
| 186 | public function handleCartChangeOnLogin() { |
| 187 | if (!$this->loadSavedCartAfterLogin) { |
| 188 | return false; |
| 189 | } |
| 190 | $this->handleCartChange(); |
| 191 | } |
| 192 | |
| 193 | public function handleSubscriberActivity(SubscriberEntity $subscriber) { |
| 194 | // on subscriber activity on site reschedule all currently scheduled (not yet sent) emails for given subscriber |
| 195 | // (it tracks at most once per minute to avoid processing many calls at the same time, i.e. AJAX) |
| 196 | $this->rescheduleAbandonedCartEmail($subscriber); |
| 197 | } |
| 198 | |
| 199 | private function getCartProductIds($cart) { |
| 200 | $cartItems = $cart->get_cart() ?: []; |
| 201 | return array_column($cartItems, 'product_id'); |
| 202 | } |
| 203 | |
| 204 | private function scheduleAbandonedCartEmail(array $cartProductIds = []) { |
| 205 | $subscriber = $this->getSubscriber(); |
| 206 | if (!$subscriber) { |
| 207 | return; |
| 208 | } |
| 209 | |
| 210 | $this->wp->doAction(self::HOOK_SCHEDULE, $subscriber, $cartProductIds); |
| 211 | $meta = [self::TASK_META_NAME => $cartProductIds]; |
| 212 | $this->scheduler->scheduleOrRescheduleAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber, $meta); |
| 213 | } |
| 214 | |
| 215 | private function rescheduleAbandonedCartEmail(SubscriberEntity $subscriber) { |
| 216 | $this->wp->doAction(self::HOOK_RE_SCHEDULE, $subscriber); |
| 217 | $this->scheduler->rescheduleAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber); |
| 218 | } |
| 219 | |
| 220 | private function cancelAbandonedCartEmail() { |
| 221 | $subscriber = $this->getSubscriber(); |
| 222 | if (!$subscriber) { |
| 223 | return; |
| 224 | } |
| 225 | $this->wp->doAction(self::HOOK_CANCEL, $subscriber); |
| 226 | $this->scheduler->cancelAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber); |
| 227 | } |
| 228 | |
| 229 | private function getSubscriber(): ?SubscriberEntity { |
| 230 | $wpUser = $this->wp->wpGetCurrentUser(); |
| 231 | if ($wpUser->exists()) { |
| 232 | return $this->subscribersRepository->findOneBy(['wpUserId' => $wpUser->ID]); |
| 233 | } |
| 234 | |
| 235 | // if user not logged in, try to find subscriber by cookie |
| 236 | $subscriberId = $this->subscriberCookie->getSubscriberId(); |
| 237 | if ($subscriberId) { |
| 238 | return $this->subscribersRepository->findOneById($subscriberId); |
| 239 | } |
| 240 | return null; |
| 241 | } |
| 242 | } |
| 243 |