PluginProbe
Email Marketing for WordPress and WooCommerce – Retainful / trunk
Email Marketing for WordPress and WooCommerce – Retainful vtrunk
1.0.9 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8
retainful / App / Controllers / Store / Common.php

Common.php in Email Marketing for WordPress and WooCommerce – Retainful trunk, at App/Controllers/Store/Common.php

467 lines 17.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace RTFL\App\Controllers\Store;
4
5
6 defined( "ABSPATH" ) or exit;
7
8 use RTFL\App\Controllers\Controller;
9 use RTFL\App\Controllers\Orders;
10 use RTFL\App\Controllers\Payloads\AbandonedCart;
11 use RTFL\App\Controllers\Webhook;
12 use RTFL\App\Helpers\Settings;
13 use RTFL\App\Helpers\Customer;
14 use RTFL\App\Helpers\Input;
15 use RTFL\App\Helpers\Util;
16 use RTFL\App\Helpers\Dispatch;
17 use RTFL\App\Helpers\Cart;
18 use RTFL\App\Helpers\Guard;
19 use RTFL\App\Helpers\Order;
20 use RTFL\App\Helpers\Plugins;
21
22 class Common implements Controller {
23 public static function register() {
24 add_action('wp_enqueue_scripts',[self::class,'addPopupScripts']);
25 add_action( 'wp_enqueue_scripts', [ self::class, 'addCartTrackingScripts' ] );
26 add_filter( 'woocommerce_checkout_fields', [ self::class, 'guestGdprMessage' ], 10, 1 );
27 add_action( 'woocommerce_checkout_after_terms_and_conditions', [ self::class, 'guestTermGdprMessage' ] );
28 //add_action( 'woocommerce_init', [ self::class, 'setIdentityData' ], 5 );
29 add_action( 'wp', [ self::class, 'setIdentityData' ], 5 );
30 // Alternative PHP hooks for cart sync (in addition to JavaScript events)
31 //add_action( 'woocommerce_add_to_cart', [ Webhook::class, 'syncCart' ], 10 );
32 add_action( 'woocommerce_cart_updated', [ Webhook::class, 'syncCart' ] );
33
34 // AJAX handler for tracking user data from checkout
35 add_action( 'wp_ajax_rnoc_track_user_data', [ Webhook::class,'syncCart' ] );
36 add_action( 'wp_ajax_nopriv_rnoc_track_user_data', [ Webhook::class,'syncCart' ] );
37
38 // Scheduled action handler for cart sync after user data update
39 add_action( 'rtfl_track_user_data_cart_sync', [ self::class, 'handleScheduledCartSync' ] );
40 add_action( 'wp', [ Popup::class, 'applyPopupCoupon' ] );
41 // Apply abandoned cart coupon from URL parameter
42 add_action( 'wp', [ self::class, 'applyAbandonedCartCoupon' ] );
43
44 self::registerBlockCheckoutFields();
45 Orders::register();
46 }
47
48
49 /**
50 * Get cart tracking js url.
51 *
52 * @return string
53 */
54 public static function getCartTrackingJsUrl() {
55 return apply_filters( 'rtfl_get_abandoned_cart_tracking_js_engine_url', RTFL_PLUGIN_URL . 'assets/store/js/retainful-store.js' );
56 }
57
58 public static function addPopupScripts() {
59 if ( !Settings::shouldEnqueueRetainfulPopupScripts() ) {
60 return;
61 }
62 $popup_data = [
63 'popup_data' => Settings::getConnectionDetails(),
64 //'popup_json' => get_option( 'retainful_popup_json', [] ),
65 'ajax_url' => admin_url( 'admin-ajax.php' ),
66 'retainful_coupon_nonce' => \RTFL\App\Helpers\Common::createNonce( 'retainful_apply_popup_coupon' ),
67 'popup_redirect_timeout' => apply_filters( 'rtfl_popup_redirect_time_after_add_to_cart', 1500 )
68
69 ];
70 wp_enqueue_script( RTFL_PLUGIN_PREFIX . 'popups-site', RTFL_PLUGIN_URL . 'assets/store/js/retainful-popup.js', array( 'jquery' ), RTFL_VERSION, true );
71 wp_localize_script( RTFL_PLUGIN_PREFIX . 'popups-site', 'retainful_popup_localize_data', $popup_data );
72 wp_enqueue_script( RTFL_PLUGIN_PREFIX . 'popups', Settings::getPopupEngineScriptBaseUrl(), array( 'jquery' ), RTFL_VERSION, true );
73 }
74
75
76 /**
77 * add retainful js file
78 *
79 * @return void
80 */
81 public static function addCartTrackingScripts() {
82 if( ! Guard::isWCGuard() ){
83 return;
84 }
85 if ( ! wp_script_is( RTFL_PLUGIN_PREFIX . 'track-user-cart' ) ) {
86 wp_enqueue_script( RTFL_PLUGIN_PREFIX . 'track-user-cart', self::getCartTrackingJsUrl(),['jquery'], RTFL_VERSION, false );
87 $data = [
88 'ajax_url' => admin_url( 'admin-ajax.php' ),
89 'nonce' => \RTFL\App\Helpers\Common::createNonce( 'rtfl-cart-sync' ),
90
91 ];
92 if ( function_exists( 'is_product' ) && is_product() ) {
93 $product_id = function_exists( 'get_the_ID' ) ? get_the_ID() : 0;
94 if ( $product_id && function_exists( 'wc_get_product' ) ) {
95 $product = \RTFL\App\Helpers\Product::getProduct( $product_id );
96 if ( $product && is_a( $product, 'WC_Product' ) ) {
97 $stock_quantity = $product->is_in_stock() ? 1 : 0;
98 $data['viewed_product'] = [
99 'product_id' => (int) $product_id,
100 'quantity' => $stock_quantity,
101 ];
102 }
103 }
104 }
105
106 $data = apply_filters( 'rtfl_add_cart_tracking_scripts', $data );
107 $data['viewed_product_tracking'] = [
108 'enabled' => (bool)Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'enable_product_view' ),
109 'customer_email' => self::getIdentifiedCustomerEmail(), // new helper below
110 'dedup_window' => apply_filters( RTFL_PLUGIN_PREFIX.'viewed_product_dedup_minutes', 10 ),
111 ];
112 wp_localize_script( RTFL_PLUGIN_PREFIX . 'track-user-cart', 'retainful_localize_data', $data );
113 }
114
115 }
116
117 /**
118 * Get the identified customer's email for product view tracking.
119 * Checks logged-in user first, then WooCommerce customer session.
120 * Never exposes email to unauthenticated visitors who haven't engaged yet.
121 */
122 private static function getIdentifiedCustomerEmail(): string {
123 // 1. Logged-in user — most reliable
124 $user = wp_get_current_user();
125 if ( $user->ID > 0 && is_email( $user->user_email ) ) {
126 return $user->user_email;
127 }
128
129 // 2. Guest who already typed email into checkout (stored in WC session)
130 if ( function_exists( 'WC' ) && WC()->session ) {
131 $session_email = WC()->session->get( 'rtfl_customer_email', '' );
132 if ( is_email( $session_email ) ) {
133 return $session_email;
134 }
135 }
136
137 // 3. Guest who submitted a popup form (stored in WooCommerce session by Popup handler)
138 $guest_email = \RTFL\App\Helpers\Common::getGuestEmail();
139 if ( is_email( $guest_email ) ) {
140 return $guest_email;
141 }
142
143 return ''; // Not identified — JS will skip tracking
144 }
145
146 public static function guestGdprMessage( $fields ) {
147 $enable_gdpr_compliance = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'enable_gdpr_compliance' );
148 $message = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'cart_capture_msg' ) ?? 'Keep me up-to-date on news and exclusive offers';
149 $field_name = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'gdpr_display_position' ) ?? 'after_billing_email';
150 //sms consent settings
151 $enable_sms_compliance = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'enable_sms_consent' );
152
153 $sms_message = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'sms_capture_msg' ) ?? 'Keep me up-to-date on news and exclusive offers';
154 $sms_field_name = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'sms_consent_display_position' ) ?? 'after_billing_phone';
155 if ( $enable_gdpr_compliance && $field_name == 'after_billing_email' && $message && isset( $fields['billing']['billing_email'] ) ) {
156 $fields['billing'][ RTFL_PLUGIN_PREFIX . 'allow_gdpr' ] = [
157 //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
158 'label' => __( $message, 'retainful' ),
159 'type' => 'checkbox',
160 'priority' => $fields['billing']['billing_email']['priority'],
161 'default' => (int) AbandonedCart::isBuyerAcceptsMarketing()
162 ];
163 }
164
165 if ( $enable_sms_compliance && $sms_field_name == 'after_billing_phone' && $sms_message && isset( $fields['billing']['billing_phone'] ) ) {
166 $fields['billing'][ RTFL_PLUGIN_PREFIX . 'sms_consent' ] = [
167 //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
168 'label' => __( $sms_message, 'retainful' ),
169 'type' => 'checkbox',
170 'priority' => $fields['billing']['billing_phone']['priority'],
171 'default' => (int) AbandonedCart::isSmsConsent()
172 ];
173
174 }
175
176 return $fields;
177 }
178
179 public static function guestTermGdprMessage() {
180 $enable_gdpr_compliance = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'enable_gdpr_compliance' );
181 $message = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'cart_capture_msg' ) ?? 'Keep me up-to-date on news and exclusive offers';
182 $field_name = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'gdpr_display_position' ) ?? 'after_billing_email';
183 //sms consent settings
184 $enable_sms_compliance = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'enable_sms_consent' );
185 $sms_message = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'sms_capture_msg' ) ?? 'Keep me up-to-date on news and exclusive offers';
186 $sms_field_name = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'sms_consent_display_position' ) ?? 'after_billing_phone';
187
188 if ( $enable_gdpr_compliance && $field_name == 'after_term_and_condition' && $message ) {
189 echo '<input type="checkbox" class="woocommerce-form__input woocommerce-form__input-checkbox input-checkbox"
190 name="' . esc_attr( RTFL_PLUGIN_PREFIX . 'allow_gdpr' ) . '" id="' . esc_attr( RTFL_PLUGIN_PREFIX . 'allow_gdpr' ) . '" ' . ( AbandonedCart::isBuyerAcceptsMarketing() ? 'checked="checked"' : '' ) . ' />
191 <span class="woocommerce-terms-and-conditions-checkbox-text">' . esc_html__( $message, 'retainful' ) . ' ' . esc_html__( '(optional)', 'retainful' ) . '</span><br>'; //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
192 }
193 if ( $enable_sms_compliance && $sms_field_name == 'after_term_and_condition' && $sms_message ) {
194 echo '<input type="checkbox" class="woocommerce-form__input woocommerce-form__input-checkbox input-checkbox"
195 name="' . esc_attr( RTFL_PLUGIN_PREFIX . 'sms_consent' ) . '" id="' . esc_attr( RTFL_PLUGIN_PREFIX . 'sms_consent' ) . '" ' . ( AbandonedCart::isSmsConsent() ? 'checked="checked"' : '' ) . ' />
196 <span class="woocommerce-terms-and-conditions-checkbox-text">' . esc_html__( $sms_message, 'retainful' ) . ' ' . esc_html__( '(optional)', 'retainful' ) . '</span><br>'; //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
197 }
198 }
199
200 /**
201 * Set login identity data.
202 *
203 * @return void
204 */
205 public static function setIdentityData() {
206 if( ! Guard::isWCGuard() ){
207 return;
208 }
209 // Skip on REST API requests. wp_doing_ajax() does NOT cover
210 // REST endpoints (REST does not set DOING_AJAX), so without
211 // this guard the WC Store API endpoints used by Blocks
212 // checkout / cart and many third-party search/filter
213 // plugins would still mark WC()->customer dirty on every
214 // concurrent request and reproduce the wc_sessions
215 // contention pattern that caused PHP-FPM "Premature end of
216 // script headers" 500s under FiboSearch load.
217 // Retainful's identity flow does not run via REST.
218 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
219 return;
220 }
221
222 if(function_exists('wp_doing_ajax') && wp_doing_ajax()) {
223 $current_ajax_action = Input::get('action', '');
224 if(empty($current_ajax_action) || !in_array($current_ajax_action, ['rnoc_track_user_data','retainful_integration','retainful_set_popup_coupon_code','rtfl_track_product_viewed',])) {
225 return;
226 }
227 }
228
229 // It is customer viewing page or front end ajax request
230 if(!Customer::isCustomerPage()) {
231 return;
232 }
233
234 // Checkout already have email, no need to set identity
235 $customer_billing_email = Customer::getCustomerBillingEmail();
236 if(!empty($customer_billing_email)) {
237 return;
238 }
239
240 // Set identity for logged in user
241 $cookie_email = self::getIdentity( '_wc_retainful_tk_session' );
242 if(is_user_logged_in() && empty($cookie_email)) {
243 $user = wp_get_current_user();
244 if ( is_object( $user ) && ! empty( $user->user_email ) ) {
245 self::setIdentity( $user->user_email );
246 }
247 }
248
249 // we have to get cookie email again, because identity may changed in logged in user
250 $cookie_email = self::getIdentity( '_wc_retainful_tk_session' );
251 // Set identity
252 if ( ! empty( $cookie_email ) ) {
253 $cookie_email = json_decode( base64_decode( $cookie_email ), true );
254 if ( is_array( $cookie_email ) && ! empty( $cookie_email['email'] ) ) {
255 Customer::setCustomerEmail( $cookie_email['email'] );
256 }
257 }
258 }
259
260 /**
261 * Get identity.
262 *
263 * @param $key
264 * @param $default_value
265 *
266 * @return mixed|string|null
267 */
268 public static function getIdentity( $key, $default_value = '' ) {
269
270 if ( isset( $_COOKIE[ $key ] ) ) {
271 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
272 return $_COOKIE[ $key ];
273 }
274
275 return $default_value;
276 }
277
278 /**
279 * Set identity.
280 *
281 * @param $value
282 *
283 * @return void
284 */
285 public static function setIdentity( $value = '' ) {
286 if ( ! Customer::isCustomerPage() || empty( $value ) || ! Guard::isWCGuard() ) {
287 return;
288 }
289 $cookie_data = [ 'email' => trim( $value ) ];
290 unset( $_COOKIE['_wc_retainful_tk_session'] );
291 if ( function_exists( 'wc_setcookie' ) ) {
292 wc_setcookie( '_wc_retainful_tk_session', base64_encode( json_encode( $cookie_data ) ), strtotime( '+30 days' ) );
293 }
294
295 }
296
297 /**
298 * Add abandon cart coupon automatically from URL parameter.
299 * Handles the retainful_ac_coupon URL parameter and applies it to the cart.
300 *
301 * @return void
302 * @since 2.0.0
303 */
304 public static function applyAbandonedCartCoupon() {
305 if( ! Guard::isWCGuard() ){
306 return;
307 }
308
309 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
310 return;
311 }
312
313 // Skip WooCommerce session access on any AJAX request.
314 if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) {
315 return;
316 }
317
318 // Don't run in admin area
319 if ( is_admin() ) {
320 return;
321 }
322
323 // Get coupon code from URL parameter
324 $coupon_code = Input::get( 'retainful_ac_coupon', '' );
325 $session = Settings::getStorage();
326 // Store coupon in session if found in URL
327 if ( ! empty( $coupon_code ) ) {
328
329 $session->set( 'rtfl_ac_coupon', sanitize_text_field( $coupon_code ) );
330 }
331
332 // Get coupon from session (either from URL or previously stored)
333 $session_coupon = $session->get( 'rtfl_ac_coupon' );
334
335 if ( empty( $session_coupon ) ) {
336 return;
337 }
338
339 // Validate the coupon
340 $is_valid = Order::isValidCoupon( $session_coupon );
341
342 if ( $is_valid instanceof \WP_Error || ! $is_valid ) {
343 // Invalid coupon, remove from session
344 $session->remove( 'rtfl_ac_coupon' );
345 return;
346 }
347
348 // Check if WooCommerce cart is available
349 if ( ! function_exists( 'WC' ) || ! is_object( WC()->cart ) ) {
350 return;
351 }
352
353 // Check if cart is empty
354 if ( Cart::isCartEmpty() ) {
355 return;
356 }
357
358 // Check if coupon is already applied
359 $applied_coupons = Cart::getAppliedCartCoupons();
360 $is_already_applied = false;
361
362 if ( ! empty( $applied_coupons ) ) {
363 foreach ( $applied_coupons as $applied_coupon ) {
364 $applied_coupon_code = $applied_coupon instanceof \WC_Coupon
365 ? $applied_coupon->get_code()
366 : $applied_coupon;
367
368 if ( strtolower( $applied_coupon_code ) === strtolower( $session_coupon ) ) {
369 $is_already_applied = true;
370 break;
371 }
372 }
373 }
374
375 // If coupon is already applied, remove from session and return
376 if ( $is_already_applied ) {
377 $session->remove( 'rtfl_ac_coupon' );
378 return;
379 }
380
381 // Apply the coupon to cart
382 if ( Util::isMethodExists( WC()->cart, 'apply_coupon' ) ) {
383 try {
384 $result = WC()->cart->apply_coupon( $session_coupon );
385 if ( $result ) {
386 $session->remove( 'rtfl_ac_coupon' );
387 Settings::logMessage(
388 sprintf( 'Abandoned cart coupon applied successfully from URL: %s', $session_coupon ),
389 'applyAbandonedCartCoupon_success'
390 );
391 }
392 } catch ( \Exception $e ) {
393 // Handle any exceptions during coupon application
394 Settings::logMessage(
395 sprintf( 'Exception applying abandoned cart coupon: %s - %s', $session_coupon, $e->getMessage() ),
396 'applyAbandonedCartCoupon_exception'
397 );
398 $session->remove( 'rtfl_ac_coupon' );
399 }
400 }
401 }
402
403 public static function registerBlockCheckoutFields() {
404 if(!Plugins::isActive('woocommerce/woocommerce.php')) {
405 return;
406 }
407 $enable_gdpr_compliance = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'enable_gdpr_compliance' );
408 $enable_sms_compliance = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'enable_sms_consent' );
409
410 if ( ! $enable_gdpr_compliance && ! $enable_sms_compliance ) {
411 return;
412 }
413
414 $gdpr_message = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'cart_capture_msg' ) ?? 'Keep me up-to-date on news and exclusive offers';
415 $sms_message = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'sms_capture_msg' ) ?? 'Keep me up-to-date on news and exclusive offers';
416 $gdpr_position = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'gdpr_display_position' ) ?? 'after_billing_email';
417 $sms_position = Settings::getAdminSetting( RTFL_PLUGIN_PREFIX . 'sms_consent_display_position' ) ?? 'after_billing_phone';
418
419 $marketing_location = 'contact';
420 $sms_location = 'contact';
421
422 if ( $gdpr_position === 'after_term_and_condition' ) {
423 $marketing_location = 'order';
424 } elseif ( $gdpr_position === 'after_billing_phone' ) {
425 $marketing_location = 'address';
426 }
427
428 if ( $sms_position === 'after_term_and_condition' ) {
429 $sms_location = 'order';
430 } elseif ( $sms_position === 'after_billing_phone' ) {
431 $sms_location = 'address';
432 }
433
434 // IMPORTANT: namespace/name format for block checkout fields.
435 $namespace = 'rtfl';
436
437 // woocommerce_register_additional_checkout_field() was introduced
438 // in WooCommerce 8.6. Guard with function_exists so the plugin
439 // does not fatal on WC < 8.6 when consent features are enabled.
440 if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) {
441 return;
442 }
443
444 if ( $enable_gdpr_compliance ) {
445 woocommerce_register_additional_checkout_field( [
446 'id' => $namespace . '/allow_gdpr',
447 'label' => $gdpr_message,
448 'location' => $marketing_location,
449 'type' => 'checkbox',
450 'required' => false,
451 ] );
452 }
453
454 if ( $enable_sms_compliance ) {
455 woocommerce_register_additional_checkout_field( [
456 'id' => $namespace . '/sms_consent',
457 'label' => $sms_message,
458 'location' => $sms_location,
459 'type' => 'checkbox',
460 'required' => false,
461 ] );
462 }
463 }
464
465
466
467 }