Geen omschrijving

AbandonedCart.php 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <?php
  2. namespace MailPoet\AutomaticEmails\WooCommerce\Events;
  3. if (!defined('ABSPATH')) exit;
  4. use MailPoet\AutomaticEmails\WooCommerce\WooCommerce as WooCommerceEmail;
  5. use MailPoet\Models\Subscriber;
  6. use MailPoet\Newsletter\Scheduler\AutomaticEmailScheduler;
  7. use MailPoet\Statistics\Track\Clicks;
  8. use MailPoet\Util\Cookies;
  9. use MailPoet\WooCommerce\Helper as WooCommerceHelper;
  10. use MailPoet\WP\Functions as WPFunctions;
  11. class AbandonedCart {
  12. const SLUG = 'woocommerce_abandoned_shopping_cart';
  13. const LAST_VISIT_TIMESTAMP_OPTION_NAME = 'mailpoet_last_visit_timestamp';
  14. const TASK_META_NAME = 'cart_product_ids';
  15. /** @var WPFunctions */
  16. private $wp;
  17. /** @var WooCommerceHelper */
  18. private $wooCommerceHelper;
  19. /** @var Cookies */
  20. private $cookies;
  21. /** @var AbandonedCartPageVisitTracker */
  22. private $pageVisitTracker;
  23. /** @var AutomaticEmailScheduler */
  24. private $scheduler;
  25. public function __construct() {
  26. $this->wp = WPFunctions::get();
  27. $this->wooCommerceHelper = new WooCommerceHelper();
  28. $this->cookies = new Cookies();
  29. $this->pageVisitTracker = new AbandonedCartPageVisitTracker($this->wp, $this->wooCommerceHelper, $this->cookies);
  30. $this->scheduler = new AutomaticEmailScheduler();
  31. }
  32. public function getEventDetails() {
  33. return [
  34. 'slug' => self::SLUG,
  35. 'title' => WPFunctions::get()->_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'),
  36. 'description' => WPFunctions::get()->__('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'),
  37. 'listingScheduleDisplayText' => WPFunctions::get()->_x('Email sent when a customer abandons his cart.', 'Description of Abandoned Shopping Cart email', 'mailpoet'),
  38. 'badge' => [
  39. 'text' => WPFunctions::get()->__('Must-have', 'mailpoet'),
  40. 'style' => 'red',
  41. ],
  42. 'timeDelayValues' => [
  43. 'minutes' => [
  44. 'text' => _x('30 minutes after last page loaded', 'This is a trigger setting. It means that we will send an automatic email to a visitor 30 minutes after this visitor had left the website.', 'mailpoet'),
  45. 'displayAfterTimeNumberField' => false,
  46. ],
  47. 'hours' => [
  48. 'text' => __('hour(s) later', 'mailpoet'),
  49. 'displayAfterTimeNumberField' => true,
  50. ],
  51. 'days' => [
  52. 'text' => __('day(s) later', 'mailpoet'),
  53. 'displayAfterTimeNumberField' => true,
  54. ],
  55. 'weeks' => [
  56. 'text' => __('week(s) later', 'mailpoet'),
  57. 'displayAfterTimeNumberField' => true,
  58. ],
  59. ],
  60. 'defaultAfterTimeType' => 'minutes',
  61. 'schedulingReadMoreLink' => [
  62. 'link' => 'https://www.mailpoet.com/blog/abandoned-cart-woocommerce',
  63. 'text' => __('We recommend setting up 3 abandoned cart emails. Here’s why.', 'mailpoet'),
  64. ],
  65. ];
  66. }
  67. public function init() {
  68. if (!$this->wooCommerceHelper->isWooCommerceActive()) {
  69. return;
  70. }
  71. // item added to cart (not fired on quantity changes)
  72. $this->wp->addAction(
  73. 'woocommerce_add_to_cart',
  74. [$this, 'handleCartChange'],
  75. 10
  76. );
  77. // item removed from cart (not fired on quantity changes, not even change to zero)
  78. $this->wp->addAction(
  79. 'woocommerce_cart_item_removed',
  80. [$this, 'handleCartChange'],
  81. 10
  82. );
  83. // item quantity updated (not fired when quantity updated to zero)
  84. $this->wp->addAction(
  85. 'woocommerce_after_cart_item_quantity_update',
  86. [$this, 'handleCartChange'],
  87. 10
  88. );
  89. // item quantity set to zero (it removes the item but does not fire remove event)
  90. $this->wp->addAction(
  91. 'woocommerce_before_cart_item_quantity_zero',
  92. [$this, 'handleCartChange'],
  93. 10
  94. );
  95. // cart emptied (not called when all items removed)
  96. $this->wp->addAction(
  97. 'woocommerce_cart_emptied',
  98. [$this, 'handleCartChange'],
  99. 10
  100. );
  101. // undo removal of item from cart or cart emptying (does not fire any other cart change hook)
  102. $this->wp->addAction(
  103. 'woocommerce_cart_item_restored',
  104. [$this, 'handleCartChange'],
  105. 10
  106. );
  107. $this->wp->addAction(
  108. 'wp',
  109. [$this, 'trackPageVisit'],
  110. 10
  111. );
  112. }
  113. public function handleCartChange() {
  114. $cart = $this->wooCommerceHelper->WC()->cart;
  115. if (current_action() !== 'woocommerce_cart_emptied' && $cart && !$cart->is_empty()) {
  116. $this->scheduleAbandonedCartEmail($this->getCartProductIds($cart));
  117. } else {
  118. $this->cancelAbandonedCartEmail();
  119. $this->pageVisitTracker->stopTracking();
  120. }
  121. }
  122. public function trackPageVisit() {
  123. // on page visit reschedule all currently scheduled (not yet sent) emails for given subscriber
  124. // (it tracks at most once per minute to avoid processing many calls at the same time, i.e. AJAX)
  125. $this->pageVisitTracker->trackVisit(function () {
  126. $this->rescheduleAbandonedCartEmail();
  127. });
  128. }
  129. private function getCartProductIds($cart) {
  130. $cartItems = $cart->get_cart() ?: [];
  131. return array_column($cartItems, 'product_id');
  132. }
  133. private function scheduleAbandonedCartEmail(array $cartProductIds = []) {
  134. $subscriber = $this->getSubscriber();
  135. if (!$subscriber || $subscriber->status !== Subscriber::STATUS_SUBSCRIBED) {
  136. return;
  137. }
  138. $meta = [self::TASK_META_NAME => $cartProductIds];
  139. $this->scheduler->scheduleOrRescheduleAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber->id, $meta);
  140. // start tracking page visits to detect inactivity
  141. $this->pageVisitTracker->startTracking();
  142. }
  143. private function rescheduleAbandonedCartEmail() {
  144. $subscriber = $this->getSubscriber();
  145. if (!$subscriber) {
  146. return;
  147. }
  148. $this->scheduler->rescheduleAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber->id);
  149. }
  150. private function cancelAbandonedCartEmail() {
  151. $subscriber = $this->getSubscriber();
  152. if (!$subscriber) {
  153. return;
  154. }
  155. $this->scheduler->cancelAutomaticEmail(WooCommerceEmail::SLUG, self::SLUG, $subscriber->id);
  156. }
  157. private function getSubscriber() {
  158. $wpUser = $this->wp->wpGetCurrentUser();
  159. if ($wpUser->exists()) {
  160. return Subscriber::where('wp_user_id', $wpUser->ID)->findOne() ?: null;
  161. }
  162. // if user not logged in, try to find subscriber by cookie
  163. $cookieData = $this->cookies->get(Clicks::ABANDONED_CART_COOKIE_NAME);
  164. if ($cookieData && isset($cookieData['subscriber_id'])) {
  165. return Subscriber::findOne($cookieData['subscriber_id']) ?: null;
  166. }
  167. return null;
  168. }
  169. }