Initially, I had opted to use getReference() to avoid querying the database to get the segment entities as all we need is the segment ID. But I hadn't realized that this could cause problems if a segment that is saved in the settings in the option woocommerce.segments is deleted. Using findBy() here protects against this problem as, if the segment doesn't exist anymore, it won't be returned. [MAILPOET-4365]
289 lines
9.4 KiB
PHP
289 lines
9.4 KiB
PHP
<?php
|
|
|
|
namespace MailPoet\WooCommerce;
|
|
|
|
use MailPoet\Entities\StatisticsUnsubscribeEntity;
|
|
use MailPoet\Entities\SubscriberEntity;
|
|
use MailPoet\Entities\SubscriberSegmentEntity;
|
|
use MailPoet\Segments\SegmentsRepository;
|
|
use MailPoet\Settings\SettingsController;
|
|
use MailPoet\Statistics\Track\Unsubscribes;
|
|
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';
|
|
|
|
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 Unsubscribes */
|
|
private $unsubscribesTracker;
|
|
|
|
/** @var SegmentsRepository */
|
|
private $segmentsRepository;
|
|
|
|
/** @var SubscriberSegmentRepository */
|
|
private $subscriberSegmentRepository;
|
|
|
|
public function __construct(
|
|
SettingsController $settings,
|
|
ConfirmationEmailMailer $confirmationEmailMailer,
|
|
WPFunctions $wp,
|
|
Helper $wcHelper,
|
|
SubscribersRepository $subscribersRepository,
|
|
Unsubscribes $unsubscribesTracker,
|
|
SegmentsRepository $segmentsRepository,
|
|
SubscriberSegmentRepository $subscriberSegmentRepository
|
|
) {
|
|
$this->settings = $settings;
|
|
$this->wp = $wp;
|
|
$this->wcHelper = $wcHelper;
|
|
$this->confirmationEmailMailer = $confirmationEmailMailer;
|
|
$this->subscribersRepository = $subscribersRepository;
|
|
$this->unsubscribesTracker = $unsubscribesTracker;
|
|
$this->segmentsRepository = $segmentsRepository;
|
|
$this->subscriberSegmentRepository = $subscriberSegmentRepository;
|
|
}
|
|
|
|
public function extendWooCommerceCheckoutForm() {
|
|
$inputName = self::CHECKOUT_OPTIN_INPUT_NAME;
|
|
$checked = $this->isCurrentUserSubscribed();
|
|
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, WordPressDotOrg.sniffs.OutputEscaping.UnescapedOutputParameter
|
|
echo $template;
|
|
if ($template) {
|
|
$field = $this->getSubscriptionPresenceCheckField();
|
|
echo wp_kses($field, $this->allowedHtml);
|
|
}
|
|
}
|
|
|
|
private function getSubscriptionField($inputName, $checked, $labelString) {
|
|
return $this->wcHelper->woocommerceFormField(
|
|
$this->wp->escAttr($inputName),
|
|
[
|
|
'type' => 'checkbox',
|
|
'label' => $this->wp->escHtml($labelString),
|
|
'input_class' => ['woocommerce-form__input', 'woocommerce-form__input-checkbox', 'input-checkbox'],
|
|
'label_class' => ['woocommerce-form__label', 'woocommerce-form__label-for-checkbox', 'checkbox'],
|
|
'custom_attributes' => ['data-automation-id' => 'woo-commerce-subscription-opt-in'],
|
|
'return' => true,
|
|
],
|
|
$checked ? '1' : '0'
|
|
);
|
|
}
|
|
|
|
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 isCurrentUserSubscribed() {
|
|
$subscriber = $this->subscribersRepository->getCurrentWPUser();
|
|
if (!$subscriber instanceof SubscriberEntity) {
|
|
return false;
|
|
}
|
|
|
|
$wcSegment = $this->segmentsRepository->getWooCommerceSegment();
|
|
$subscriberSegment = $this->subscriberSegmentRepository->findOneBy(
|
|
['subscriber' => $subscriber->getId(), 'segment' => $wcSegment->getId()]
|
|
);
|
|
|
|
return $subscriberSegment instanceof SubscriberSegmentEntity
|
|
&& $subscriberSegment->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED;
|
|
}
|
|
|
|
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) {
|
|
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;
|
|
}
|
|
|
|
$checkoutOptinEnabled = (bool)$this->settings->get(self::OPTIN_ENABLED_SETTING_NAME);
|
|
$checkoutOptin = !empty($_POST[self::CHECKOUT_OPTIN_INPUT_NAME]);
|
|
|
|
return $this->handleSubscriberOptin($subscriber, $checkoutOptinEnabled, $checkoutOptin);
|
|
}
|
|
|
|
/**
|
|
* Subscribe or unsubscribe a subscriber.
|
|
*
|
|
* @param SubscriberEntity $subscriber Subscriber object
|
|
* @param bool $checkoutOptinEnabled
|
|
* @param bool $checkoutOptin
|
|
*/
|
|
public function handleSubscriberOptin(SubscriberEntity $subscriber, bool $checkoutOptinEnabled, bool $checkoutOptin): 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 (!$checkoutOptin) {
|
|
// Opt-in is disabled or checkbox is unchecked
|
|
$this->subscriberSegmentRepository->unsubscribeFromSegments($subscriber, [$wcSegment]);
|
|
|
|
// Unsubscribe from configured segment only when opt-in is enabled
|
|
if ($checkoutOptinEnabled && $moreSegmentsToSubscribe) {
|
|
$this->subscriberSegmentRepository->unsubscribeFromSegments($subscriber, $moreSegmentsToSubscribe);
|
|
}
|
|
// Update global status only in case the opt-in is enabled
|
|
if ($checkoutOptinEnabled) {
|
|
$this->updateSubscriberStatus($subscriber);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
private function updateSubscriberStatus(SubscriberEntity $subscriber) {
|
|
$segmentsCount = $subscriber->getSubscribedSegments()->count();
|
|
|
|
if (!$segmentsCount) {
|
|
$subscriber->setStatus(SubscriberEntity::STATUS_UNSUBSCRIBED);
|
|
$this->subscribersRepository->persist($subscriber);
|
|
$this->subscribersRepository->flush();
|
|
$this->unsubscribesTracker->track((int)$subscriber->getId(), StatisticsUnsubscribeEntity::SOURCE_ORDER_CHECKOUT);
|
|
}
|
|
}
|
|
}
|