Changeset 3458122
- Timestamp:
- 02/10/2026 02:29:36 PM (7 weeks ago)
- Location:
- firstpromoter/trunk
- Files:
-
- 11 edited
-
assets/css/admin.css (modified) (1 diff)
-
assets/js/admin.js (modified) (2 diffs)
-
firstpromoter.php (modified) (9 diffs)
-
includes/class-fp-helpers.php (modified) (2 diffs)
-
includes/class-fp-settings-page.php (modified) (17 diffs)
-
integrations/class-fp-integration-base.php (modified) (14 diffs)
-
integrations/class-fp-integration-contactform7.php (modified) (2 diffs)
-
integrations/class-fp-integration-memberpress.php (modified) (11 diffs)
-
integrations/class-fp-integration-optimizepress.php (modified) (1 diff)
-
integrations/class-fp-integration-woocommerce.php (modified) (17 diffs)
-
readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
firstpromoter/trunk/assets/css/admin.css
r3399029 r3458122 620 620 } 621 621 622 /* Custom Forms Repeater */ 623 .fp-custom-forms-repeater { 624 margin-top: 12px; 625 } 626 627 .fp-custom-form-row { 628 display: flex; 629 align-items: center; 630 gap: 10px; 631 padding: 12px; 632 margin-bottom: 8px; 633 background: #f9fafb; 634 border: 1px solid #e5e7eb; 635 border-radius: 8px; 636 } 637 638 .fp-custom-form-fields { 639 flex: 1; 640 display: flex; 641 gap: 10px; 642 } 643 644 .fp-custom-form-fields .fp-input.regular-text { 645 flex: 1; 646 margin-bottom: 0; 647 } 648 649 .fp-remove-form-row { 650 background: none; 651 border: 1px solid #d1d5db; 652 border-radius: 6px; 653 width: 32px; 654 height: 32px; 655 font-size: 18px; 656 line-height: 1; 657 cursor: pointer; 658 color: #6b7280; 659 display: flex; 660 align-items: center; 661 justify-content: center; 662 flex-shrink: 0; 663 padding: 0; 664 } 665 666 .fp-remove-form-row:hover { 667 color: #ef4444; 668 border-color: #ef4444; 669 background: #fef2f2; 670 } 671 672 .fp-add-form-row { 673 background: none; 674 border: 2px dashed #d1d5db; 675 border-radius: 8px; 676 padding: 10px 20px; 677 color: #6b7280; 678 font-size: 14px; 679 cursor: pointer; 680 width: 100%; 681 margin-top: 4px; 682 } 683 684 .fp-add-form-row:hover { 685 border-color: #2271b1; 686 color: #2271b1; 687 background: rgba(34, 113, 177, 0.04); 688 } 689 690 @media (max-width: 768px) { 691 .fp-custom-form-fields { 692 flex-direction: column; 693 } 694 } 695 622 696 /* Button disabled state */ 623 697 .button-primary:disabled, -
firstpromoter/trunk/assets/js/admin.js
r3399029 r3458122 13 13 this.setupTooltips(); 14 14 this.setupFieldHighlight(); 15 this.setupCustomFormsRepeater(); 15 16 }, 16 17 … … 223 224 var originalText = $btn.text(); 224 225 $btn.text('Copied!').addClass('copied'); 225 226 226 227 setTimeout(function() { 227 228 $btn.text(originalText).removeClass('copied'); 228 229 }, 2000); 230 }, 231 232 // Custom Forms repeater add/remove rows 233 setupCustomFormsRepeater: function() { 234 var $container = $('.fp-custom-form-rows'); 235 if (!$container.length) return; 236 237 // Toggle multiple forms mode 238 $('.fp-multiple-forms-checkbox').on('change', function() { 239 var $repeater = $(this).closest('.fp-custom-forms-repeater'); 240 var $addBtn = $repeater.find('.fp-add-form-row'); 241 var $removeBtns = $repeater.find('.fp-remove-form-row'); 242 243 if ($(this).is(':checked')) { 244 $addBtn.show(); 245 $removeBtns.show(); 246 } else { 247 $addBtn.hide(); 248 // Remove all rows except the first, clear extra fields 249 var $rows = $container.find('.fp-custom-form-row'); 250 $rows.each(function(i) { 251 if (i > 0) { 252 $(this).remove(); 253 } 254 }); 255 // Re-index the remaining row 256 $container.find('.fp-custom-form-row').each(function(idx) { 257 $(this).find('input[type="text"]').each(function() { 258 var name = $(this).attr('name'); 259 $(this).attr('name', name.replace(/\[custom_forms\]\[\d+\]/, '[custom_forms][' + idx + ']')); 260 }); 261 }); 262 $removeBtns.hide(); 263 } 264 }); 265 266 // Add row 267 $(document).on('click', '.fp-add-form-row', function() { 268 var $rows = $container.find('.fp-custom-form-row'); 269 var newIndex = $rows.length; 270 var $row = $( 271 '<div class="fp-custom-form-row" style="display:none;">' + 272 '<div class="fp-custom-form-fields">' + 273 '<input type="text" name="firstpromoter_integration_settings[custom_forms][' + newIndex + '][email_selector]" value="" placeholder="Email input selector, e.g. #signup-email" class="fp-input regular-text">' + 274 '<input type="text" name="firstpromoter_integration_settings[custom_forms][' + newIndex + '][submit_selector]" value="" placeholder="Submit selector, e.g. #signup-btn or form.my-form" class="fp-input regular-text">' + 275 '</div>' + 276 '<button type="button" class="fp-remove-form-row" title="Remove row">×</button>' + 277 '</div>' 278 ); 279 $container.append($row); 280 $row.fadeIn(200); 281 }); 282 283 // Remove row 284 $(document).on('click', '.fp-remove-form-row', function() { 285 var $rows = $container.find('.fp-custom-form-row'); 286 var $row = $(this).closest('.fp-custom-form-row'); 287 288 if ($rows.length <= 1) { 289 $row.find('input').val(''); 290 return; 291 } 292 293 $row.fadeOut(200, function() { 294 $(this).remove(); 295 // Re-index remaining rows 296 $container.find('.fp-custom-form-row').each(function(i) { 297 $(this).find('input').each(function() { 298 var name = $(this).attr('name'); 299 $(this).attr('name', name.replace(/\[custom_forms\]\[\d+\]/, '[custom_forms][' + i + ']')); 300 }); 301 }); 302 // If only 1 row left, uncheck multi toggle and hide remove buttons 303 if ($container.find('.fp-custom-form-row').length <= 1) { 304 var $repeater = $container.closest('.fp-custom-forms-repeater'); 305 $repeater.find('.fp-multiple-forms-checkbox').prop('checked', false); 306 $repeater.find('.fp-add-form-row').slideUp(200); 307 $repeater.find('.fp-remove-form-row').hide(); 308 } 309 }); 310 }); 229 311 } 230 312 }; -
firstpromoter/trunk/firstpromoter.php
r3404859 r3458122 3 3 Plugin Name: FirstPromoter 4 4 Description: FirstPromoter tracking scripts with WooCommerce, OptimizePress, Contact Form 7 & MemberPress 5 Version: 0. 2.15 Version: 0.3.0 6 6 Author: FirstPromoter 7 7 Author URI: https://firstpromoter.com … … 19 19 } 20 20 21 //composer autoload22 require 'vendor/autoload.php';23 24 21 // Define plugin constants 25 define('FIRSTPROMOTER_VERSION', '0. 2.1');22 define('FIRSTPROMOTER_VERSION', '0.3.0'); 26 23 define('FIRSTPROMOTER_PLUGIN_PATH', plugin_dir_path(__FILE__)); 27 24 define('FIRSTPROMOTER_PLUGIN_URL', plugin_dir_url(__FILE__)); … … 34 31 require_once FIRSTPROMOTER_PLUGIN_PATH . 'integrations/class-fp-integration-contactform7.php'; 35 32 require_once FIRSTPROMOTER_PLUGIN_PATH . 'integrations/class-fp-integration-memberpress.php'; 33 require_once FIRSTPROMOTER_PLUGIN_PATH . 'integrations/class-fp-integration-customforms.php'; 36 34 37 35 class FirstPromoter_Core … … 54 52 55 53 // Check for URL parameters before anything else 56 add_action('init', [$this, 'check_url_parameters'], 1);54 add_action('init', array($this, 'check_url_parameters'), 1); 57 55 58 56 // Initialize settings page with available integrations … … 60 58 61 59 // Add base tracking script - this should run on all pages 62 add_action('wp_enqueue_scripts', [$this, 'add_base_tracking_script'], 5); 60 add_action('wp_enqueue_scripts', array($this, 'add_base_tracking_script'), 5); 61 62 // Register AJAX handler once (not per-integration) 63 add_action('wp_ajax_nopriv_firstpromoter_track_api_fallback', array($this, 'handle_api_fallback')); 64 add_action('wp_ajax_firstpromoter_track_api_fallback', array($this, 'handle_api_fallback')); 63 65 64 66 // Initialize the enabled integrations 65 67 $this->load_integrations(); 68 } 69 70 /** 71 * Handle AJAX API fallback - delegates to the first available integration 72 */ 73 public function handle_api_fallback() 74 { 75 // Create a minimal integration instance to handle the fallback 76 // We need an integration with access to the base API methods 77 $integration_settings = get_option('firstpromoter_integration_settings'); 78 79 // Try WooCommerce first, then any available integration 80 if (class_exists('FirstPromoter_Integration_WooCommerce')) { 81 $handler = new FirstPromoter_Integration_WooCommerce($integration_settings); 82 } elseif (class_exists('FirstPromoter_Integration_MemberPress')) { 83 $handler = new FirstPromoter_Integration_MemberPress($integration_settings); 84 } elseif (class_exists('FirstPromoter_Integration_ContactForm7')) { 85 $handler = new FirstPromoter_Integration_ContactForm7($integration_settings); 86 } elseif (class_exists('FirstPromoter_Integration_CustomForms')) { 87 $handler = new FirstPromoter_Integration_CustomForms($integration_settings); 88 } else { 89 wp_die(); 90 return; 91 } 92 93 $handler->handle_api_fallback(); 66 94 } 67 95 … … 136 164 137 165 // Prepare tracking configuration 138 $hasCrossDomains = isset($base_settings['cross_domain']) && $base_settings['cross_domain']; 166 $cross_domain_value = isset($base_settings['cross_domain']) ? $base_settings['cross_domain'] : ''; 167 $hasCrossDomains = ! empty($cross_domain_value); 139 168 $cross_domains = $hasCrossDomains ? wp_json_encode( 140 array_map( 141 'trim', 142 explode(',', isset($base_settings['cross_domain']) ? $base_settings['cross_domain'] : '') 143 ) 169 array_map('trim', explode(',', $cross_domain_value)) 144 170 ) : ''; 145 171 $force_new_cookies_on_refresh = isset($base_settings['force_new_cookies_on_refresh']) && $base_settings['force_new_cookies_on_refresh']; … … 181 207 $integration_settings = get_option('firstpromoter_integration_settings'); 182 208 183 // Load WooCommerce integration if WooCommerce is active and integration is enabled 209 $integrations = array( 210 'woocommerce' => array('key' => 'woo_enabled', 'class' => 'FirstPromoter_Integration_WooCommerce'), 211 'optimizepress' => array('key' => 'op_enabled', 'class' => 'FirstPromoter_Integration_OptimizePress'), 212 'contactform7' => array('key' => 'cf7_enabled', 'class' => 'FirstPromoter_Integration_ContactForm7'), 213 'memberpress' => array('key' => 'mepr_enabled', 'class' => 'FirstPromoter_Integration_MemberPress'), 214 ); 215 216 foreach ($integrations as $slug => $config) { 217 if ( 218 isset($integration_settings[$config['key']]) && 219 $integration_settings[$config['key']] && 220 isset($this->available_integrations[$slug]) && 221 $this->available_integrations[$slug] 222 ) { 223 new $config['class']($integration_settings); 224 } 225 } 226 227 // Custom Forms integration - always available (no plugin dependency) 228 if (defined('WP_DEBUG') && WP_DEBUG) { 229 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 230 error_log('FirstPromoter: Custom Forms Enabled Setting: ' . (isset($integration_settings['custom_forms_enabled']) ? $integration_settings['custom_forms_enabled'] : 'NOT SET')); 231 } 232 184 233 if ( 185 isset($integration_settings['woo_enabled']) && 186 $integration_settings['woo_enabled'] && 187 isset($this->available_integrations['woocommerce']) && 188 $this->available_integrations['woocommerce'] 234 isset($integration_settings['custom_forms_enabled']) && 235 $integration_settings['custom_forms_enabled'] 189 236 ) { 190 new FirstPromoter_Integration_WooCommerce($integration_settings); 191 } 192 193 // Load OptimizePress integration if OptimizePress is active and integration is enabled 194 if ( 195 isset($integration_settings['op_enabled']) && 196 $integration_settings['op_enabled'] && 197 isset($this->available_integrations['optimizepress']) && 198 $this->available_integrations['optimizepress'] 199 ) { 200 new FirstPromoter_Integration_OptimizePress($integration_settings); 201 } 202 203 // Load Contact Form 7 integration if Contact Form 7 is active and integration is enabled 204 if ( 205 isset($integration_settings['cf7_enabled']) && 206 $integration_settings['cf7_enabled'] && 207 isset($this->available_integrations['contactform7']) && 208 $this->available_integrations['contactform7'] 209 ) { 210 new FirstPromoter_Integration_ContactForm7($integration_settings); 211 } 212 213 // Load MemberPress integration if MemberPress is active and integration is enabled 214 if ( 215 isset($integration_settings['mepr_enabled']) && 216 $integration_settings['mepr_enabled'] && 217 isset($this->available_integrations['memberpress']) && 218 $this->available_integrations['memberpress'] 219 ) { 220 new FirstPromoter_Integration_MemberPress($integration_settings); 221 } 222 223 // Allow other integrations to be added 237 if (defined('WP_DEBUG') && WP_DEBUG) { 238 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 239 error_log('FirstPromoter: Custom Forms Integration Initializing...'); 240 } 241 new FirstPromoter_Integration_CustomForms($integration_settings); 242 } 243 224 244 do_action('firstpromoter_load_integrations', $integration_settings, $this->available_integrations); 225 245 } … … 238 258 } 239 259 240 $param_names = ['fpr', 'via', '_fprom'];260 $param_names = array('fpr', 'via', '_fprom'); 241 261 $ref_id = null; 242 262 … … 251 271 252 272 if (!empty($ref_id) && empty($_COOKIE['_fprom_ref'])) { 253 // Set cookies with appropriate expiration (30 days)254 273 $expiry = time() + (30 * DAY_IN_SECONDS); 255 $path = '/';256 $domain = '';257 274 $secure = is_ssl(); 258 $httponly = true; 259 260 setcookie('_fprom_ref_fallback', $ref_id, $expiry, $path, $domain, $secure, $httponly); 275 276 if (PHP_VERSION_ID >= 70300) { 277 setcookie('_fprom_ref_fallback', $ref_id, array( 278 'expires' => $expiry, 279 'path' => '/', 280 'domain' => '', 281 'secure' => $secure, 282 'httponly' => true, 283 'samesite' => 'Lax', 284 )); 285 } else { 286 setcookie('_fprom_ref_fallback', $ref_id, $expiry, '/; SameSite=Lax', '', $secure, true); 287 } 261 288 } 262 289 } -
firstpromoter/trunk/includes/class-fp-helpers.php
r3399288 r3458122 14 14 { 15 15 // Set default field parameters 16 $field = wp_parse_args($field, [16 $field = wp_parse_args($field, array( 17 17 'type' => 'text', 18 18 'id' => '', … … 23 23 'rows' => 4, 24 24 'placeholder' => '' 25 ]);25 )); 26 26 27 27 // Escape attributes -
firstpromoter/trunk/includes/class-fp-settings-page.php
r3402820 r3458122 5 5 private $base_settings; 6 6 private $integration_settings; 7 private $availableIntegrations = [];7 private $availableIntegrations = array(); 8 8 9 9 public function __construct() 10 10 { 11 11 include_once plugin_dir_path(__FILE__) . 'class-fp-helpers.php'; 12 add_action('admin_menu', [$this, 'add_settings_page']);13 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);14 add_action('admin_init', [$this, 'register_settings']);15 add_action('admin_notices', [$this, 'show_api_key_notices']);12 add_action('admin_menu', array($this, 'add_settings_page')); 13 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); 14 add_action('admin_init', array($this, 'register_settings')); 15 add_action('admin_notices', array($this, 'show_api_key_notices')); 16 16 17 17 $this->base_settings = get_option('firstpromoter_base_settings'); … … 96 96 ]; 97 97 } 98 99 // Custom Form Tracking - always available (no plugin dependency) 100 $this->availableIntegrations['CustomForms'] = [ 101 'title' => 'Custom Form Tracking', 102 'settings' => [ 103 'enabled_key' => 'custom_forms_enabled', 104 ], 105 'options' => [], 106 'custom_content' => true, 107 'description' => 'Track email submissions from any form on your site using CSS selectors. Works with any form builder, custom HTML forms, or dynamically loaded popups.', 108 ]; 98 109 } 99 110 … … 107 118 'manage_options', 108 119 'first-promoter', 109 [$this, 'render_settings_page'],120 array($this, 'render_settings_page'), 110 121 'data:image/svg+xml;base64,' . $base64 111 122 ); … … 121 132 'first-promoter-admin', 122 133 plugins_url('assets/css/admin.css', dirname(__FILE__)), 123 [],124 '1.0.0'134 array(), 135 FIRSTPROMOTER_VERSION 125 136 ); 126 137 … … 128 139 'first-promoter-admin', 129 140 plugins_url('assets/js/admin.js', dirname(__FILE__)), 130 ['jquery'],131 '1.0.0',141 array('jquery'), 142 FIRSTPROMOTER_VERSION, 132 143 true 133 144 ); … … 177 188 id="firstpromoter_cid" 178 189 name="firstpromoter_base_settings[cid]" 179 value="<?php echo esc_attr( $this->base_settings['cid'] ?? ''); ?>"190 value="<?php echo esc_attr((isset($this->base_settings['cid']) ? $this->base_settings['cid'] : '')); ?>" 180 191 class="fp-input regular-text" 181 192 required> … … 192 203 id="firstpromoter_api_key" 193 204 name="firstpromoter_base_settings[api_key]" 194 value="<?php echo esc_attr( $this->base_settings['api_key'] ?? ''); ?>"205 value="<?php echo esc_attr((isset($this->base_settings['api_key']) ? $this->base_settings['api_key'] : '')); ?>" 195 206 class="fp-input regular-text" 196 207 required> … … 209 220 name="firstpromoter_base_settings[cross_domain]" 210 221 placeholder="mywebsite.com,myotherwebsite.com" 211 value="<?php echo esc_attr( $this->base_settings['cross_domain'] ?? ''); ?>"222 value="<?php echo esc_attr((isset($this->base_settings['cross_domain']) ? $this->base_settings['cross_domain'] : '')); ?>" 212 223 class="fp-input regular-text"> 213 224 <p class="description">This is useful when you are tracking accross multiple domains. For example mywebsite.com and myotherwebsite.com. <br> Please enter the domains separated with a comma.<br> This is not needed if you are just using subdomains.</p> … … 290 301 </div> 291 302 292 <div class="fp-integration-option-details"> 303 <?php $is_enabled = isset($this->integration_settings[$integration['settings']['enabled_key']]) && $this->integration_settings[$integration['settings']['enabled_key']]; ?> 304 <div class="fp-integration-option-details"<?php if (!$is_enabled) echo ' style="display:none"'; ?>> 293 305 <?php if (isset($integration['description']) && !empty($integration['description'])): ?> 294 306 <p class="description" style="margin: 12px 0 8px 0; color: #666;"> … … 299 311 <?php 300 312 foreach ($integration["options"] as $option) { 301 $value = $this->integration_settings[$option['id']] ??'';313 $value = isset($this->integration_settings[$option['id']]) ? $this->integration_settings[$option['id']] : ''; 302 314 FirstPromoter_Helpers::firstpromoter_render_field($option, $value); 303 315 } 304 316 ?> 305 317 </div> 318 319 <?php if (!empty($integration['custom_content'])): ?> 320 <?php 321 $saved_forms = isset($this->integration_settings['custom_forms']) ? $this->integration_settings['custom_forms'] : array(); 322 if (empty($saved_forms)) { 323 $saved_forms = array(array('email_selector' => '', 'submit_selector' => '')); 324 } 325 $has_multiple = count($saved_forms) > 1; 326 ?> 327 <div class="fp-custom-forms-repeater"> 328 <p class="firstpromoter_label" style="margin-top: 12px;">Form Selectors</p> 329 <div class="fp-custom-form-rows"> 330 <?php foreach ($saved_forms as $idx => $form_row): ?> 331 <div class="fp-custom-form-row"<?php if ($idx > 0 && !$has_multiple) echo ' style="display:none"'; ?>> 332 <div class="fp-custom-form-fields"> 333 <input type="text" 334 name="firstpromoter_integration_settings[custom_forms][<?php echo (int) $idx; ?>][email_selector]" 335 value="<?php echo esc_attr(isset($form_row['email_selector']) ? $form_row['email_selector'] : ''); ?>" 336 placeholder="Email input selector, e.g. #signup-email" 337 class="fp-input regular-text"> 338 <input type="text" 339 name="firstpromoter_integration_settings[custom_forms][<?php echo (int) $idx; ?>][submit_selector]" 340 value="<?php echo esc_attr(isset($form_row['submit_selector']) ? $form_row['submit_selector'] : ''); ?>" 341 placeholder="Submit selector, e.g. #signup-btn or form.my-form" 342 class="fp-input regular-text"> 343 </div> 344 <button type="button" class="fp-remove-form-row" title="Remove row"<?php if (!$has_multiple) echo ' style="display:none"'; ?>>×</button> 345 </div> 346 <?php endforeach; ?> 347 </div> 348 <div class="fp-multiple-forms-toggle" style="margin-top: 8px;"> 349 <label style="display: flex; align-items: center; gap: 8px; font-size: 14px; color: #374151;"> 350 <input type="checkbox" class="fp-multiple-forms-checkbox" <?php checked($has_multiple); ?>> 351 I have multiple forms to track 352 </label> 353 </div> 354 <button type="button" class="fp-add-form-row"<?php if (!$has_multiple) echo ' style="display:none"'; ?>>+ Add Another Form</button> 355 <p class="description" style="margin-top: 10px;"> 356 Enter a CSS selector for the email input and either a form element or submit button.<br> 357 Examples: <code>#signup-email</code> / <code>#signup-btn</code> — <code>.popup-email</code> / <code>form.popup-form</code> 358 </p> 359 </div> 360 <?php endif; ?> 306 361 </div> 307 362 </div> … … 322 377 public function sanitize_integration_settings($input) 323 378 { 324 $sanitized = [];379 $sanitized = array(); 325 380 326 381 // Sanitize toggle checkboxes 327 $toggle_keys = ['woo_enabled', 'op_enabled', 'cf7_enabled', 'mepr_enabled' ];382 $toggle_keys = ['woo_enabled', 'op_enabled', 'cf7_enabled', 'mepr_enabled', 'custom_forms_enabled']; 328 383 foreach ($toggle_keys as $key) { 329 384 $sanitized[$key] = isset($input[$key]) ? 1 : 0; … … 342 397 } 343 398 399 // Sanitize custom form selectors 400 $sanitized['custom_forms'] = array(); 401 if (isset($input['custom_forms']) && is_array($input['custom_forms'])) { 402 foreach ($input['custom_forms'] as $row) { 403 $email_sel = isset($row['email_selector']) ? sanitize_text_field($row['email_selector']) : ''; 404 $submit_sel = isset($row['submit_selector']) ? sanitize_text_field($row['submit_selector']) : ''; 405 if ($email_sel !== '' || $submit_sel !== '') { 406 $sanitized['custom_forms'][] = array( 407 'email_selector' => $email_sel, 408 'submit_selector' => $submit_sel, 409 ); 410 } 411 } 412 } 413 344 414 // Validate API key requirements 345 415 $base_settings = get_option('firstpromoter_base_settings'); … … 347 417 348 418 if (empty($api_key)) { 349 $api_required_features = [];419 $api_required_features = array(); 350 420 351 421 // Check if Contact Form 7 is enabled … … 383 453 public function sanitize_base_settings($input) 384 454 { 385 $sanitized = [];455 $sanitized = array(); 386 456 387 457 // Sanitize CID (required) 388 $sanitized['cid'] = sanitize_text_field( $input['cid'] ??'');458 $sanitized['cid'] = sanitize_text_field(isset($input['cid']) ? $input['cid'] : ''); 389 459 390 460 // Sanitize API key (sensitive data) 391 $sanitized['api_key'] = sanitize_text_field( $input['api_key'] ??'');461 $sanitized['api_key'] = sanitize_text_field(isset($input['api_key']) ? $input['api_key'] : ''); 392 462 393 463 // Sanitize cross domain 394 $sanitized['cross_domain'] = sanitize_text_field( $input['cross_domain'] ??'');464 $sanitized['cross_domain'] = sanitize_text_field(isset($input['cross_domain']) ? $input['cross_domain'] : ''); 395 465 396 466 // Sanitize checkbox … … 408 478 'firstpromoter_base_settings', 409 479 'firstpromoter_base_settings', 410 [$this, 'sanitize_base_settings']480 array($this, 'sanitize_base_settings') 411 481 ); 412 482 … … 415 485 'firstpromoter_integration_settings', 416 486 'firstpromoter_integration_settings', 417 [$this, 'sanitize_integration_settings']487 array($this, 'sanitize_integration_settings') 418 488 ); 419 489 } … … 433 503 434 504 if (empty($api_key)) { 435 $requires_api = [];505 $requires_api = array(); 436 506 437 507 // Check Contact Form 7 -
firstpromoter/trunk/integrations/class-fp-integration-base.php
r3404859 r3458122 16 16 $this->integration_settings = $integration_settings; 17 17 $this->base_settings = get_option('firstpromoter_base_settings'); 18 add_action('wp_ajax_nopriv_firstpromoter_track_api_fallback', [$this, 'handle_api_fallback']);19 add_action('wp_ajax_firstpromoter_track_api_fallback', [$this, 'handle_api_fallback']);20 18 $this->init(); 21 19 } 22 20 23 21 abstract protected function init(); 22 23 /** 24 * Log a debug message if WP_DEBUG is enabled 25 * 26 * @param string $message Message to log 27 */ 28 protected function log($message) 29 { 30 if (defined('WP_DEBUG') && WP_DEBUG) { 31 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 32 error_log('FirstPromoter: ' . $message); 33 } 34 } 24 35 25 36 protected function track_referral($email, $userId = null) … … 35 46 } 36 47 37 $data = [48 $data = array( 38 49 'email' => $email, 39 ];50 ); 40 51 41 52 if (! empty($userId)) { … … 45 56 // Check if wp_enqueue_scripts has already fired 46 57 if (did_action('wp_enqueue_scripts')) { 47 // Too late for wp_enqueue_scripts, use wp_footer instead48 58 add_action('wp_footer', function () use ($data) { 49 59 $this->add_referral_tracking_scripts($data); 50 60 }, 20); 51 61 } else { 52 // Normal case - wp_enqueue_scripts hasn't fired yet53 62 add_action('wp_enqueue_scripts', function () use ($data) { 54 63 $this->add_referral_tracking_scripts($data); … … 69 78 70 79 if ($has_fallback_cookie && ! $has_main_ref_cookie) { 71 // Fallback cookie exists but main cookie doesn't - ad blocker likely blocking cookies 72 // Make direct server-side API call 73 if (defined('WP_DEBUG') && WP_DEBUG) { 74 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 75 error_log('FirstPromoter: Fallback cookie detected without main cookies. Making direct API call for signup tracking.'); 76 } 80 $this->log('Fallback cookie detected without main cookies. Making direct API call for signup tracking.'); 77 81 $this->track_signup_via_api($email, $userId); 78 82 return; … … 85 89 86 90 // No cookies at all - add JavaScript fallback that will check again client-side 87 // Check if wp_enqueue_scripts has already fired for fallback too 88 if (did_action('wp_enqueue_scripts')) { 89 add_action('wp_footer', function () use ($email, $userId) { 90 wp_enqueue_script('firstpromoter-ajax-fallback', '', [], FIRSTPROMOTER_VERSION, true); 91 92 // Localize script for AJAX URL and nonce 93 wp_localize_script('firstpromoter-ajax-fallback', 'firstpromoter_ajax', [ 94 'ajax_url' => admin_url('admin-ajax.php'), 95 'nonce' => wp_create_nonce('firstpromoter_ajax'), 96 ]); 97 98 $inline_script = ' 99 (function() { 100 setTimeout(function() { 101 if (window.fprom_loaded) { 102 return; 103 } 104 var xhr = new XMLHttpRequest(); 105 xhr.open("POST", firstpromoter_ajax.ajax_url, true); 106 xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 107 xhr.send("action=firstpromoter_track_api_fallback&email=' . esc_js($email) . '&uid=' . esc_js($userId) . '&nonce=" + firstpromoter_ajax.nonce); 108 }, 100); 109 })(); 110 '; 111 112 wp_add_inline_script('firstpromoter-ajax-fallback', $inline_script); 113 }, 30); 114 } else { 115 add_action('wp_enqueue_scripts', function () use ($email, $userId) { 116 wp_enqueue_script('firstpromoter-ajax-fallback', '', [], FIRSTPROMOTER_VERSION, true); 117 118 // Localize script for AJAX URL and nonce 119 wp_localize_script('firstpromoter-ajax-fallback', 'firstpromoter_ajax', [ 120 'ajax_url' => admin_url('admin-ajax.php'), 121 'nonce' => wp_create_nonce('firstpromoter_ajax'), 122 ]); 123 124 $inline_script = ' 125 (function() { 126 setTimeout(function() { 127 if (window.fprom_loaded) { 128 return; 129 } 130 var xhr = new XMLHttpRequest(); 131 xhr.open("POST", firstpromoter_ajax.ajax_url, true); 132 xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 133 xhr.send("action=firstpromoter_track_api_fallback&email=' . esc_js($email) . '&uid=' . esc_js($userId) . '&nonce=" + firstpromoter_ajax.nonce); 134 }, 100); 135 })(); 136 '; 137 138 wp_add_inline_script('firstpromoter-ajax-fallback', $inline_script); 139 }, 30); 140 } 141 } catch (\Exception $e) { 142 if (defined('WP_DEBUG') && WP_DEBUG) { 143 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 144 error_log('FirstPromoter track_referral error: ' . $e->getMessage()); 145 } 146 // Silently fail to ensure application functionality is not affected 91 $this->enqueue_ajax_fallback($email, $userId); 92 } catch (\Exception $e) { 93 $this->log('track_referral error: ' . $e->getMessage()); 94 } 95 } 96 97 /** 98 * Enqueue the AJAX fallback script for ad-blocker bypass 99 * 100 * @param string $email 101 * @param mixed $userId 102 */ 103 private function enqueue_ajax_fallback($email, $userId) 104 { 105 $callback = function () use ($email, $userId) { 106 wp_enqueue_script('firstpromoter-ajax-fallback', '', array(), FIRSTPROMOTER_VERSION, true); 107 108 wp_localize_script('firstpromoter-ajax-fallback', 'firstpromoter_ajax', array( 109 'ajax_url' => admin_url('admin-ajax.php'), 110 'nonce' => wp_create_nonce('firstpromoter_ajax'), 111 )); 112 113 $inline_script = ' 114 (function() { 115 setTimeout(function() { 116 if (window.fprom_loaded) { 117 return; 118 } 119 var xhr = new XMLHttpRequest(); 120 xhr.open("POST", firstpromoter_ajax.ajax_url, true); 121 xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 122 xhr.send("action=firstpromoter_track_api_fallback&email=' . esc_js($email) . '&uid=' . esc_js($userId) . '&nonce=" + firstpromoter_ajax.nonce); 123 }, 100); 124 })(); 125 '; 126 127 wp_add_inline_script('firstpromoter-ajax-fallback', $inline_script); 128 }; 129 130 if (did_action('wp_enqueue_scripts')) { 131 add_action('wp_footer', $callback, 30); 132 } else { 133 add_action('wp_enqueue_scripts', $callback, 30); 147 134 } 148 135 } … … 166 153 // Check if we're being called from wp_footer (after wp_enqueue_scripts) 167 154 if (doing_action('wp_footer')) { 168 // Output script directly since we're already in the footer169 155 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $json_data is already safely encoded via wp_json_encode() 170 156 echo '<script type="text/javascript">fpr("referral", ' . $json_data . ');</script>' . "\n"; 171 157 } else { 172 // Normal enqueue method 173 wp_enqueue_script('firstpromoter-referral-tracking', '', [], FIRSTPROMOTER_VERSION, true); 158 wp_enqueue_script('firstpromoter-referral-tracking', '', array(), FIRSTPROMOTER_VERSION, true); 174 159 $inline_script = 'fpr("referral", ' . $json_data . ');'; 175 160 wp_add_inline_script('firstpromoter-referral-tracking', $inline_script); 176 161 } 177 162 } catch (\Exception $e) { 178 if (defined('WP_DEBUG') && WP_DEBUG) { 179 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 180 error_log('FirstPromoter add_referral_tracking_scripts error: ' . $e->getMessage()); 181 } 182 // Silently fail to ensure application functionality is not affected 183 } 163 $this->log('add_referral_tracking_scripts error: ' . $e->getMessage()); 164 } 165 } 166 167 /** 168 * Make a POST request to the FirstPromoter API 169 * 170 * @param string $endpoint API endpoint path (e.g. "/track/sale") 171 * @param array $data Request body data 172 * @return array|false Decoded response body on success, false on failure 173 */ 174 protected function api_post($endpoint, $data) 175 { 176 $apiKey = isset($this->base_settings['api_key']) ? $this->base_settings['api_key'] : ''; 177 $accountId = isset($this->base_settings['cid']) ? $this->base_settings['cid'] : ''; 178 179 if (empty($apiKey) || empty($accountId)) { 180 $this->log('API key or Account ID missing for v2 API'); 181 return false; 182 } 183 184 $url = $this->base_api_v2_url . $endpoint; 185 186 $this->log('Sending POST to ' . $url . ' with data: ' . wp_json_encode($data)); 187 188 $response = wp_remote_post($url, array( 189 'body' => wp_json_encode($data), 190 'headers' => array( 191 'Authorization' => 'Bearer ' . $apiKey, 192 'Account-ID' => $accountId, 193 'Content-Type' => 'application/json', 194 ), 195 'timeout' => 30, 196 )); 197 198 if (is_wp_error($response)) { 199 $this->log('API request error: ' . $response->get_error_message()); 200 return false; 201 } 202 203 $status_code = wp_remote_retrieve_response_code($response); 204 $body = wp_remote_retrieve_body($response); 205 206 $this->log('API response status: ' . $status_code); 207 208 if ($status_code < 200 || $status_code >= 300) { 209 $this->log('API returned error status ' . $status_code . ': ' . $body); 210 return false; 211 } 212 213 return array( 214 'status_code' => $status_code, 215 'body' => $body, 216 ); 184 217 } 185 218 … … 187 220 { 188 221 try { 189 if (defined('WP_DEBUG') && WP_DEBUG) { 190 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 191 error_log('FirstPromoter: track_sale_via_api called with email: ' . $email . ', uid: ' . ($uid ?: 'none') . ', event_id: ' . $event_id); 192 } 193 194 $apiKey = isset($this->base_settings['api_key']) ? $this->base_settings['api_key'] : ''; 195 $accountId = isset($this->base_settings['cid']) ? $this->base_settings['cid'] : ''; 196 197 if (defined('WP_DEBUG') && WP_DEBUG) { 198 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 199 error_log('FirstPromoter: API Key present: ' . (! empty($apiKey) ? 'YES' : 'NO') . ', Account ID present: ' . (! empty($accountId) ? 'YES' : 'NO')); 200 } 201 202 if (empty($apiKey) || empty($accountId)) { 203 if (defined('WP_DEBUG') && WP_DEBUG) { 204 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 205 error_log('FirstPromoter: API key or Account ID missing for v2 API'); 206 } 207 return false; 208 } 209 210 $data = array_filter([ 222 $this->log('track_sale_via_api called with email: ' . $email . ', uid: ' . ($uid ? $uid : 'none') . ', event_id: ' . $event_id); 223 224 $data = array_filter(array( 211 225 'email' => $email, 212 226 'uid' => isset($uid) && $uid != 0 ? (string) $uid : null, … … 217 231 'tid' => ! empty($tid) ? $tid : null, 218 232 'ref_id' => ! empty($ref_id) ? $ref_id : null, 219 ], function ($value) {233 ), function ($value) { 220 234 return $value !== null; 221 235 }); 222 236 223 $baseUrl = $this->base_api_v2_url . "/track/sale"; 224 225 if (defined('WP_DEBUG') && WP_DEBUG) { 226 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 227 error_log('FirstPromoter: Sending POST to ' . $baseUrl . ' with data: ' . wp_json_encode($data)); 228 } 229 230 $client = new \GuzzleHttp\Client(); 231 $response = $client->request('POST', $baseUrl, [ 232 'json' => $data, 233 'headers' => [ 234 'Authorization' => 'Bearer ' . $apiKey, 235 'Account-ID' => $accountId, 236 'Content-Type' => 'application/json', 237 ], 238 'timeout' => 30, 239 'http_errors' => true, 240 ]); 241 242 if (defined('WP_DEBUG') && WP_DEBUG) { 243 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 244 error_log('FirstPromoter: API request successful. Status code: ' . $response->getStatusCode()); 245 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 246 error_log('FirstPromoter: API response body: ' . $response->getBody()->getContents()); 247 $response->getBody()->rewind(); // Reset stream for later use 248 } 249 250 return $response; 251 } catch (\GuzzleHttp\Exception\RequestException $e) { 252 if (defined('WP_DEBUG') && WP_DEBUG) { 253 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 254 error_log('FirstPromoter sale tracking RequestException: ' . $e->getMessage()); 255 if ($e->hasResponse()) { 256 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 257 error_log('FirstPromoter API error response: ' . $e->getResponse()->getBody()->getContents()); 258 } 259 } 260 return $e; 261 } catch (\Exception $e) { 262 if (defined('WP_DEBUG') && WP_DEBUG) { 263 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 264 error_log('FirstPromoter sale tracking Exception: ' . $e->getMessage()); 265 } 266 return $e; 237 return $this->api_post('/track/sale', $data); 238 } catch (\Exception $e) { 239 $this->log('sale tracking error: ' . $e->getMessage()); 240 return false; 267 241 } 268 242 } … … 271 245 { 272 246 try { 273 if (defined('WP_DEBUG') && WP_DEBUG) { 274 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 275 error_log('FirstPromoter: track_signup_via_api called with email: ' . $email . ', uid: ' . ($uid ?: 'none')); 276 } 277 278 $apiKey = isset($this->base_settings['api_key']) ? $this->base_settings['api_key'] : ''; 279 $accountId = isset($this->base_settings['cid']) ? $this->base_settings['cid'] : ''; 280 281 if (defined('WP_DEBUG') && WP_DEBUG) { 282 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 283 error_log('FirstPromoter: API Key present: ' . (! empty($apiKey) ? 'YES' : 'NO') . ', Account ID present: ' . (! empty($accountId) ? 'YES' : 'NO')); 284 } 285 286 if (empty($apiKey) || empty($accountId)) { 287 if (defined('WP_DEBUG') && WP_DEBUG) { 288 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 289 error_log('FirstPromoter: API key or Account ID missing for v2 API'); 290 } 291 return false; 292 } 247 $this->log('track_signup_via_api called with email: ' . $email . ', uid: ' . ($uid ? $uid : 'none')); 293 248 294 249 $tid = isset($_COOKIE["_fprom_tid"]) ? sanitize_text_field(wp_unslash($_COOKIE["_fprom_tid"])) : null; 295 250 $ref_id = isset($_COOKIE["_fprom_ref"]) ? sanitize_text_field(wp_unslash($_COOKIE["_fprom_ref"])) : (isset($_COOKIE["_fprom_ref_fallback"]) ? sanitize_text_field(wp_unslash($_COOKIE["_fprom_ref_fallback"])) : null); 296 251 297 if (defined('WP_DEBUG') && WP_DEBUG) { 298 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 299 error_log('FirstPromoter: Cookies - tid: ' . ($tid ?: 'none') . ', ref_id: ' . ($ref_id ?: 'none')); 300 } 301 302 $data = array_filter([ 252 $this->log('Cookies - tid: ' . ($tid ? $tid : 'none') . ', ref_id: ' . ($ref_id ? $ref_id : 'none')); 253 254 $data = array_filter(array( 303 255 'email' => $email, 304 256 'uid' => isset($uid) && $uid != 0 ? (string) $uid : null, 305 257 'tid' => $tid, 306 258 'ref_id' => $ref_id, 307 ], function ($value) {259 ), function ($value) { 308 260 return $value !== null; 309 261 }); 310 262 311 $baseUrl = $this->base_api_v2_url . "/track/signup"; 312 313 if (defined('WP_DEBUG') && WP_DEBUG) { 314 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 315 error_log('FirstPromoter: Sending POST to ' . $baseUrl . ' with data: ' . wp_json_encode($data)); 316 } 317 318 $client = new \GuzzleHttp\Client(); 319 $response = $client->request('POST', $baseUrl, [ 320 'json' => $data, 321 'headers' => [ 322 'Authorization' => 'Bearer ' . $apiKey, 323 'Account-ID' => $accountId, 324 'Content-Type' => 'application/json', 325 ], 326 'timeout' => 30, 327 'http_errors' => true, 328 ]); 329 330 if (defined('WP_DEBUG') && WP_DEBUG) { 331 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 332 error_log('FirstPromoter: Signup API request successful. Status code: ' . $response->getStatusCode()); 333 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 334 error_log('FirstPromoter: Signup API response body: ' . $response->getBody()->getContents()); 335 $response->getBody()->rewind(); // Reset stream for later use 336 } 337 338 return $response; 339 } catch (\GuzzleHttp\Exception\RequestException $e) { 340 if (defined('WP_DEBUG') && WP_DEBUG) { 341 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 342 error_log('FirstPromoter signup tracking RequestException: ' . $e->getMessage()); 343 if ($e->hasResponse()) { 344 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 345 error_log('FirstPromoter Signup API error response: ' . $e->getResponse()->getBody()->getContents()); 346 } 347 } 348 return $e; 349 } catch (\Exception $e) { 350 if (defined('WP_DEBUG') && WP_DEBUG) { 351 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 352 error_log('FirstPromoter signup tracking Exception: ' . $e->getMessage()); 353 } 354 return $e; 263 return $this->api_post('/track/signup', $data); 264 } catch (\Exception $e) { 265 $this->log('signup tracking error: ' . $e->getMessage()); 266 return false; 355 267 } 356 268 } … … 359 271 { 360 272 try { 361 // Check nonce for security362 273 if (! wp_verify_nonce(isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '', 'firstpromoter_ajax')) { 363 274 wp_die('Security check failed', 'Security Error', 403); … … 371 282 } 372 283 } catch (\Exception $e) { 373 if (defined('WP_DEBUG') && WP_DEBUG) { 374 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 375 error_log('FirstPromoter handle_api_fallback error: ' . $e->getMessage()); 376 } 377 // Silently fail to ensure application functionality is not affected 378 } 379 380 wp_die(); // Required to terminate properly 284 $this->log('handle_api_fallback error: ' . $e->getMessage()); 285 } 286 287 wp_die(); 381 288 } 382 289 … … 391 298 * @param string $sale_event_id Original sale event ID 392 299 * @param bool $skip_email_notification Whether to skip email notifications 393 * @return mixed Response or error300 * @return array|false Response data on success, false on failure 394 301 */ 395 302 protected function track_refund_via_api($email, $uid, $event_id, $amount, $quantity = '', $sale_event_id = '', $skip_email_notification = true) 396 303 { 397 304 try { 398 $apiKey = isset($this->base_settings['api_key']) ? $this->base_settings['api_key'] : ''; 399 $accountId = isset($this->base_settings['cid']) ? $this->base_settings['cid'] : ''; 400 401 if (empty($apiKey) || empty($accountId)) { 402 if (defined('WP_DEBUG') && WP_DEBUG) { 403 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 404 error_log('FirstPromoter: API key or Account ID missing for v2 API'); 405 } 406 return false; 407 } 408 409 $data = array_filter([ 305 $data = array_filter(array( 410 306 'email' => $email, 411 307 'uid' => isset($uid) && $uid != 0 ? (string) $uid : null, … … 415 311 'sale_event_id' => ! empty($sale_event_id) ? $sale_event_id : null, 416 312 'skip_email_notification' => $skip_email_notification, 417 ], function ($value) {313 ), function ($value) { 418 314 return $value !== null; 419 315 }); 420 316 421 $baseUrl = $this->base_api_v2_url . "/track/refund"; 422 423 $client = new \GuzzleHttp\Client(); 424 $response = $client->request('POST', $baseUrl, [ 425 'json' => $data, 426 'headers' => [ 427 'Authorization' => 'Bearer ' . $apiKey, 428 'Account-ID' => $accountId, 429 'Content-Type' => 'application/json', 430 ], 431 'timeout' => 30, 432 'http_errors' => true, 433 ]); 434 435 return $response; 436 } catch (\GuzzleHttp\Exception\RequestException $e) { 437 if (defined('WP_DEBUG') && WP_DEBUG) { 438 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 439 error_log('FirstPromoter refund tracking error: ' . $e->getMessage()); 440 } 441 return $e; 442 } catch (\Exception $e) { 443 if (defined('WP_DEBUG') && WP_DEBUG) { 444 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 445 error_log('FirstPromoter refund tracking error: ' . $e->getMessage()); 446 } 447 return $e; 317 return $this->api_post('/track/refund', $data); 318 } catch (\Exception $e) { 319 $this->log('refund tracking error: ' . $e->getMessage()); 320 return false; 448 321 } 449 322 } … … 454 327 * @param string $email Customer email 455 328 * @param string $uid Customer unique identifier 456 * @return mixed Response or error329 * @return array|false Response data on success, false on failure 457 330 */ 458 331 protected function track_cancellation_via_api($email, $uid) 459 332 { 460 333 try { 461 $apiKey = isset($this->base_settings['api_key']) ? $this->base_settings['api_key'] : ''; 462 $accountId = isset($this->base_settings['cid']) ? $this->base_settings['cid'] : ''; 463 464 if (empty($apiKey) || empty($accountId)) { 465 if (defined('WP_DEBUG') && WP_DEBUG) { 466 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 467 error_log('FirstPromoter: API key or Account ID missing for v2 API'); 468 } 469 return false; 470 } 471 472 $data = array_filter([ 334 $data = array_filter(array( 473 335 'email' => $email, 474 336 'uid' => isset($uid) && $uid != 0 ? (string) $uid : null, 475 ], function ($value) {337 ), function ($value) { 476 338 return $value !== null; 477 339 }); 478 340 479 $baseUrl = $this->base_api_v2_url . "/track/cancellation"; 480 481 $client = new \GuzzleHttp\Client(); 482 $response = $client->request('POST', $baseUrl, [ 483 'json' => $data, 484 'headers' => [ 485 'Authorization' => 'Bearer ' . $apiKey, 486 'Account-ID' => $accountId, 487 'Content-Type' => 'application/json', 488 ], 489 'timeout' => 30, 490 'http_errors' => true, 491 ]); 492 493 return $response; 494 } catch (\GuzzleHttp\Exception\RequestException $e) { 495 if (defined('WP_DEBUG') && WP_DEBUG) { 496 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 497 error_log('FirstPromoter cancellation tracking error: ' . $e->getMessage()); 498 } 499 return $e; 500 } catch (\Exception $e) { 501 if (defined('WP_DEBUG') && WP_DEBUG) { 502 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 503 error_log('FirstPromoter cancellation tracking error: ' . $e->getMessage()); 504 } 505 return $e; 341 return $this->api_post('/track/cancellation', $data); 342 } catch (\Exception $e) { 343 $this->log('cancellation tracking error: ' . $e->getMessage()); 344 return false; 506 345 } 507 346 } -
firstpromoter/trunk/integrations/class-fp-integration-contactform7.php
r3402820 r3458122 12 12 { 13 13 if (isset($this->integration_settings['cf7_enabled']) && $this->integration_settings['cf7_enabled']) { 14 // Hook to capture form data before it's sent 15 add_filter('wpcf7_posted_data', [$this, 'save_contact_form_data']); 16 17 // Hook to track referral after form is successfully submitted 18 add_action('wpcf7_mail_sent', [$this, 'track_contact_form_submission']); 14 add_filter('wpcf7_posted_data', array($this, 'save_contact_form_data')); 15 add_action('wpcf7_mail_sent', array($this, 'track_contact_form_submission')); 19 16 } 20 17 } … … 67 64 68 65 if (empty($email)) { 69 if (defined('WP_DEBUG') && WP_DEBUG) { 70 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 71 error_log('FirstPromoter CF7: No email found in form submission for form ID: ' . $contact_form->id()); 72 } 66 $this->log('CF7: No email found in form submission for form ID: ' . $contact_form->id()); 73 67 return; 74 68 } 75 69 76 if (defined('WP_DEBUG') && WP_DEBUG) { 77 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 78 error_log('FirstPromoter CF7: Processing form submission for email: ' . $email . ' from form ID: ' . $contact_form->id()); 79 } 70 $this->log('CF7: Processing form submission for email: ' . $email . ' from form ID: ' . $contact_form->id()); 80 71 81 72 // CF7 submissions are AJAX-based, so we need to use server-side API tracking 82 // Get cookies for referral attribution83 73 $ref_id = isset($_COOKIE["_fprom_ref"]) ? sanitize_text_field(wp_unslash($_COOKIE["_fprom_ref"])) : (isset($_COOKIE["_fprom_ref_fallback"]) ? sanitize_text_field(wp_unslash($_COOKIE["_fprom_ref_fallback"])) : null); 84 74 $tid = isset($_COOKIE["_fprom_tid"]) ? sanitize_text_field(wp_unslash($_COOKIE["_fprom_tid"])) : null; 85 75 86 if (defined('WP_DEBUG') && WP_DEBUG) { 87 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 88 error_log('FirstPromoter CF7: Cookies - ref_id: ' . ($ref_id ?: 'none') . ', tid: ' . ($tid ?: 'none')); 89 } 76 $this->log('CF7: Cookies - ref_id: ' . ($ref_id ? $ref_id : 'none') . ', tid: ' . ($tid ? $tid : 'none')); 90 77 91 78 // Only track if we have referral cookies (otherwise there's no attribution) 92 79 if (!empty($ref_id) || !empty($tid)) { 93 // Track the signup via API94 80 $response = $this->track_signup_via_api($email, null); 95 96 if (defined('WP_DEBUG') && WP_DEBUG) { 97 if ($response && is_object($response) && method_exists($response, 'getStatusCode')) { 98 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 99 error_log('FirstPromoter CF7: Signup tracked successfully via API. Status: ' . $response->getStatusCode()); 100 } else { 101 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 102 error_log('FirstPromoter CF7: API tracking failed or returned error'); 103 } 104 } 81 $this->log('CF7: Signup tracking result: ' . ($response ? 'success' : 'failed')); 105 82 } else { 106 if (defined('WP_DEBUG') && WP_DEBUG) { 107 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 108 error_log('FirstPromoter CF7: No referral cookies found - skipping API tracking (no attribution possible)'); 109 } 83 $this->log('CF7: No referral cookies found - skipping API tracking (no attribution possible)'); 110 84 } 111 85 } -
firstpromoter/trunk/integrations/class-fp-integration-memberpress.php
r3402820 r3458122 11 11 if (isset($this->integration_settings['mepr_enabled']) && $this->integration_settings['mepr_enabled']) { 12 12 // Track new member signups 13 add_action('mepr-signup', [$this, 'track_member_signup']);13 add_action('mepr-signup', array($this, 'track_member_signup')); 14 14 15 15 // Track subscription/transaction payments (for sales tracking) 16 16 if (isset($this->integration_settings['mepr_track_sales']) && $this->integration_settings['mepr_track_sales']) { 17 add_action('mepr-event-transaction-completed', [$this, 'track_transaction_sale'], 10, 1);17 add_action('mepr-event-transaction-completed', array($this, 'track_transaction_sale'), 10, 1); 18 18 } 19 19 20 20 // Track recurring payments 21 21 if (isset($this->integration_settings['mepr_track_recurring']) && $this->integration_settings['mepr_track_recurring']) { 22 add_action('mepr-event-recurring-transaction-completed', [$this, 'track_recurring_payment'], 10, 1);22 add_action('mepr-event-recurring-transaction-completed', array($this, 'track_recurring_payment'), 10, 1); 23 23 } 24 24 } … … 36 36 } 37 37 38 // Get user information39 38 $user = get_user_by('id', $txn->user_id); 40 39 if (!$user) { … … 46 45 47 46 if (empty($email)) { 48 if (defined('WP_DEBUG') && WP_DEBUG) { 49 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 50 error_log('FirstPromoter MemberPress: No email found for user ID: ' . $user_id); 51 } 47 $this->log('MemberPress: No email found for user ID: ' . $user_id); 52 48 return; 53 49 } 54 50 55 // Track the referral (no prefix needed for JavaScript tracking)56 51 $this->track_referral($email, $user_id); 57 58 if (defined('WP_DEBUG') && WP_DEBUG) { 59 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 60 error_log('FirstPromoter MemberPress: Tracked signup for email: ' . $email . ', user ID: ' . $user_id); 61 } 52 $this->log('MemberPress: Tracked signup for email: ' . $email . ', user ID: ' . $user_id); 62 53 } 63 54 … … 78 69 } 79 70 80 // Get user information81 71 $user = get_user_by('id', $txn->user_id); 82 72 if (!$user) { … … 86 76 $email = $user->user_email; 87 77 $user_id = $txn->user_id; 88 $amount = $txn->amount * 100; // Convert to cents78 $amount = $txn->amount * 100; 89 79 90 // Skip if amount is 0 (free trial or already processed)91 80 if ($amount == 0) { 92 if (defined('WP_DEBUG') && WP_DEBUG) { 93 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 94 error_log('FirstPromoter MemberPress: Skipping transaction with $0 amount. Transaction ID: ' . $txn->id); 95 } 81 $this->log('MemberPress: Skipping transaction with $0 amount. Transaction ID: ' . $txn->id); 96 82 return; 97 83 } … … 100 86 $tracked = get_post_meta($txn->id, 'firstpromoter_sale_tracked', true); 101 87 if ($tracked) { 102 if (defined('WP_DEBUG') && WP_DEBUG) { 103 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 104 error_log('FirstPromoter MemberPress: Sale already tracked for transaction ID: ' . $txn->id); 105 } 88 $this->log('MemberPress: Sale already tracked for transaction ID: ' . $txn->id); 106 89 return; 107 90 } 108 91 109 // Get product information110 92 $product = $txn->product(); 111 93 $plan = $product ? $product->post_title : ''; 112 94 113 // Create unique event ID114 95 $event_id = 'mepr-' . $txn->id . '-' . time(); 115 96 … … 118 99 $tid = isset($_COOKIE["_fprom_tid"]) ? sanitize_text_field(wp_unslash($_COOKIE["_fprom_tid"])) : null; 119 100 120 if (defined('WP_DEBUG') && WP_DEBUG) { 121 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 122 error_log('FirstPromoter MemberPress: Tracking sale - Email: ' . $email . ', Amount: ' . $amount . ', Event ID: ' . $event_id); 123 } 101 $this->log('MemberPress: Tracking sale - Email: ' . $email . ', Amount: ' . $amount . ', Event ID: ' . $event_id); 124 102 125 // Track the sale via API (prefix user_id for API tracking to avoid conflicts)126 103 $prefixed_user_id = 'mepr-' . $user_id; 127 104 $response = $this->track_sale_via_api($email, $prefixed_user_id, $event_id, $amount, $plan, '', $tid, $ref_id); 128 105 129 // Check if response is successful 130 if ($response && is_object($response) && method_exists($response, 'getStatusCode')) { 131 if ($response->getStatusCode() == 200) { 132 update_post_meta($txn->id, 'firstpromoter_sale_tracked', true); 133 update_post_meta($txn->id, 'firstpromoter_event_id', $event_id); 134 135 if (defined('WP_DEBUG') && WP_DEBUG) { 136 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 137 error_log('FirstPromoter MemberPress: Sale successfully tracked for transaction ID: ' . $txn->id); 138 } 139 } 106 if ($response && is_array($response) && isset($response['status_code']) && $response['status_code'] == 200) { 107 update_post_meta($txn->id, 'firstpromoter_sale_tracked', true); 108 update_post_meta($txn->id, 'firstpromoter_event_id', $event_id); 109 $this->log('MemberPress: Sale successfully tracked for transaction ID: ' . $txn->id); 140 110 } 141 111 } … … 157 127 } 158 128 159 // Get user information160 129 $user = get_user_by('id', $txn->user_id); 161 130 if (!$user) { … … 165 134 $email = $user->user_email; 166 135 $user_id = $txn->user_id; 167 $amount = $txn->amount * 100; // Convert to cents136 $amount = $txn->amount * 100; 168 137 169 // Skip if amount is 0170 138 if ($amount == 0) { 171 if (defined('WP_DEBUG') && WP_DEBUG) { 172 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 173 error_log('FirstPromoter MemberPress: Skipping recurring payment with $0 amount. Transaction ID: ' . $txn->id); 174 } 139 $this->log('MemberPress: Skipping recurring payment with $0 amount. Transaction ID: ' . $txn->id); 175 140 return; 176 141 } … … 179 144 $tracked = get_post_meta($txn->id, 'firstpromoter_recurring_tracked', true); 180 145 if ($tracked) { 181 if (defined('WP_DEBUG') && WP_DEBUG) { 182 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 183 error_log('FirstPromoter MemberPress: Recurring payment already tracked for transaction ID: ' . $txn->id); 184 } 146 $this->log('MemberPress: Recurring payment already tracked for transaction ID: ' . $txn->id); 185 147 return; 186 148 } 187 149 188 // Get product information189 150 $product = $txn->product(); 190 151 $plan = $product ? $product->post_title : ''; 191 152 192 // Create unique event ID for recurring payment193 153 $event_id = 'mepr-recurring-' . $txn->id . '-' . time(); 194 154 … … 197 157 $tid = isset($_COOKIE["_fprom_tid"]) ? sanitize_text_field(wp_unslash($_COOKIE["_fprom_tid"])) : null; 198 158 199 if (defined('WP_DEBUG') && WP_DEBUG) { 200 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 201 error_log('FirstPromoter MemberPress: Tracking recurring payment - Email: ' . $email . ', Amount: ' . $amount . ', Event ID: ' . $event_id); 202 } 159 $this->log('MemberPress: Tracking recurring payment - Email: ' . $email . ', Amount: ' . $amount . ', Event ID: ' . $event_id); 203 160 204 // Track the recurring payment as a new sale (prefix user_id for API tracking to avoid conflicts)205 161 $prefixed_user_id = 'mepr-' . $user_id; 206 162 $response = $this->track_sale_via_api($email, $prefixed_user_id, $event_id, $amount, $plan, '', $tid, $ref_id); 207 163 208 // Check if response is successful 209 if ($response && is_object($response) && method_exists($response, 'getStatusCode')) { 210 if ($response->getStatusCode() == 200) { 211 update_post_meta($txn->id, 'firstpromoter_recurring_tracked', true); 212 update_post_meta($txn->id, 'firstpromoter_recurring_event_id', $event_id); 213 214 if (defined('WP_DEBUG') && WP_DEBUG) { 215 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 216 error_log('FirstPromoter MemberPress: Recurring payment successfully tracked for transaction ID: ' . $txn->id); 217 } 218 } 164 if ($response && is_array($response) && isset($response['status_code']) && $response['status_code'] == 200) { 165 update_post_meta($txn->id, 'firstpromoter_recurring_tracked', true); 166 update_post_meta($txn->id, 'firstpromoter_recurring_event_id', $event_id); 167 $this->log('MemberPress: Recurring payment successfully tracked for transaction ID: ' . $txn->id); 219 168 } 220 169 } -
firstpromoter/trunk/integrations/class-fp-integration-optimizepress.php
r3399029 r3458122 1 1 <?php 2 2 3 if (! defined('ABSPATH')) { 4 exit; 3 if (! defined('ABSPATH')) { 4 exit; 5 } 6 7 class FirstPromoter_Integration_OptimizePress extends FirstPromoter_Integration_Base 8 { 9 protected function init() 10 { 11 add_action('wp_enqueue_scripts', array($this, 'firstpromoter_track_op'), 30); 5 12 } 6 13 7 class FirstPromoter_Integration_OptimizePress extends FirstPromoter_Integration_Base14 public function firstpromoter_track_op() 8 15 { 9 protected function init() 10 { 11 add_action('wp_enqueue_scripts', [$this, 'firstpromoter_track_op'], 30); 12 } 16 if ($this->integration_settings['op_enabled']) { 17 wp_enqueue_script('firstpromoter-optimizepress', '', array(), FIRSTPROMOTER_VERSION, true); 13 18 14 public function firstpromoter_track_op() 15 { 16 if ($this->integration_settings['op_enabled']) { 17 wp_enqueue_script('firstpromoter-optimizepress', '', array(), FIRSTPROMOTER_VERSION, true); 18 19 $inline_script = ' 20 function initializeFPR() { 21 document.querySelectorAll("input[type=email]").forEach(function(emailInput) { 22 ["mousedown", "touchstart"].forEach(function(event) { 23 emailInput.closest("form") 24 .querySelector("[data-op3-element-type=button]") 25 .addEventListener(event, function() { 26 fpr("referral", { 27 email: emailInput.value 28 }) 29 }); 30 }) 19 $inline_script = ' 20 function initializeFPR() { 21 document.querySelectorAll("input[type=email]").forEach(function(emailInput) { 22 var form = emailInput.closest("form"); 23 if (!form) return; 24 var button = form.querySelector("[data-op3-element-type=button]"); 25 if (!button) return; 26 ["mousedown", "touchstart"].forEach(function(event) { 27 button.addEventListener(event, function() { 28 fpr("referral", { 29 email: emailInput.value 30 }) 31 }); 31 32 }) 32 } 33 if (window.attachEvent) { 34 window.attachEvent("onload", initializeFPR); 35 } else { 36 window.addEventListener("load", initializeFPR, false); 37 } 38 '; 39 40 wp_add_inline_script('firstpromoter-optimizepress', $inline_script); 41 } 33 }) 34 } 35 window.addEventListener("load", initializeFPR, false); 36 '; 37 38 wp_add_inline_script('firstpromoter-optimizepress', $inline_script); 42 39 } 43 40 } 41 } -
firstpromoter/trunk/integrations/class-fp-integration-woocommerce.php
r3404859 r3458122 10 10 { 11 11 if (isset($this->integration_settings['woo_signup']) && $this->integration_settings['woo_signup']) { 12 add_filter('woocommerce_registration_redirect', [$this, 'firstpromoter_add_query_to_url']);12 add_filter('woocommerce_registration_redirect', array($this, 'firstpromoter_add_query_to_url')); 13 13 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Parameter added by WooCommerce redirect, nonce not applicable 14 14 if (isset($_GET['_new_reg']) && sanitize_text_field(wp_unslash($_GET['_new_reg'])) === 'true') { 15 add_action('template_redirect', [$this, 'firstpromoter_track_new_account']);15 add_action('template_redirect', array($this, 'firstpromoter_track_new_account')); 16 16 } 17 17 } … … 19 19 // Register checkout email capture hook if enabled 20 20 if (isset($this->integration_settings['woo_checkout']) && $this->integration_settings['woo_checkout']) { 21 add_action('woocommerce_thankyou', [$this, 'track_checkout']); 22 } 23 24 // Register payment complete hook if either email capture OR sale tracking is enabled 25 $email_capture_enabled = isset($this->integration_settings['woo_checkout']) && $this->integration_settings['woo_checkout']; 21 add_action('woocommerce_thankyou', array($this, 'track_checkout')); 22 } 23 24 // Register payment complete hook for sale tracking only 26 25 $sale_tracking_enabled = isset($this->integration_settings['woo_send_sale_to_firstpromoter_on_checkout']) && $this->integration_settings['woo_send_sale_to_firstpromoter_on_checkout']; 27 26 28 if ($email_capture_enabled || $sale_tracking_enabled) {29 add_action('woocommerce_payment_complete', [$this, 'order_payment_successful'], 10, 1);30 }31 32 // Add hooks for refunds and cancellations if sale tracking is enabled33 27 if ($sale_tracking_enabled) { 34 add_action('woocommerce_order_refunded', [$this, 'track_refund'], 10, 2); 35 add_action('woocommerce_order_status_cancelled', [$this, 'track_order_cancellation'], 10, 1); 28 add_action('woocommerce_payment_complete', array($this, 'order_payment_successful'), 10, 1); 29 add_action('woocommerce_order_refunded', array($this, 'track_refund'), 10, 2); 30 add_action('woocommerce_order_status_cancelled', array($this, 'track_order_cancellation'), 10, 1); 36 31 } 37 32 } … … 40 35 { 41 36 if (wp_get_referer()) { 42 $redirect = add_query_arg( ['_new_reg' => 'true'], esc_url(wp_get_referer()));37 $redirect = add_query_arg(array('_new_reg' => 'true'), esc_url(wp_get_referer())); 43 38 } else { 44 $redirect = add_query_arg( ['_new_reg' => 'true'], esc_url(get_permalink(wc_get_page_id('myaccount'))));39 $redirect = add_query_arg(array('_new_reg' => 'true'), esc_url(get_permalink(wc_get_page_id('myaccount')))); 45 40 } 46 41 return $redirect; 47 42 } 48 43 49 public function firstpromoter_track_new_account( $customer_id)44 public function firstpromoter_track_new_account() 50 45 { 51 46 $customer_id = get_current_user_id(); … … 55 50 if ($email) { 56 51 $this->track_referral($email, $customer_id); 57 }58 }59 60 public function track_registration()61 {62 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Parameter added by WooCommerce redirect, nonce not applicable63 if (isset($_GET['_new_reg']) && sanitize_text_field(wp_unslash($_GET['_new_reg'])) === 'true') {64 add_action('wp_footer', [$this, 'track']);65 52 } 66 53 } … … 84 71 $order->save(); 85 72 } catch (\Exception $e) { 86 if (defined('WP_DEBUG') && WP_DEBUG) { 87 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 88 error_log('FirstPromoter WooCommerce track_checkout error: ' . $e->getMessage()); 89 } 90 // Silently fail to ensure checkout process is not affected 73 $this->log('WooCommerce track_checkout error: ' . $e->getMessage()); 91 74 } 92 75 } … … 95 78 { 96 79 try { 97 if (defined('WP_DEBUG') && WP_DEBUG) { 98 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 99 error_log('FirstPromoter: order_payment_successful hook fired for order #' . $order_id); 100 } 80 $this->log('order_payment_successful hook fired for order #' . $order_id); 101 81 102 82 $order = wc_get_order($order_id); 103 83 if (! $order) { 104 if (defined('WP_DEBUG') && WP_DEBUG) { 105 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 106 error_log('FirstPromoter: Order not found for order #' . $order_id); 107 } 108 return; 109 } 110 111 $email = $order->get_billing_email(); 112 $customerId = $order->get_customer_id(); 84 $this->log('Order not found for order #' . $order_id); 85 return; 86 } 87 113 88 $order_status = $order->get_status(); 114 89 115 if (defined('WP_DEBUG') && WP_DEBUG) { 116 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 117 error_log('FirstPromoter: Order #' . $order_id . ' status: ' . $order_status . ', email: ' . $email . ', customerId: ' . $customerId); 118 } 119 120 // Track referral for both processing and completed orders 121 $valid_statuses = ['processing', 'completed']; 122 if (in_array($order_status, $valid_statuses) && ($email || $customerId)) { 123 if (defined('WP_DEBUG') && WP_DEBUG) { 124 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 125 error_log('FirstPromoter: Tracking referral for order #' . $order_id); 126 } 127 $this->track_referral($email, $customerId); 128 } 129 130 $sale_tracking_enabled = isset($this->integration_settings['woo_send_sale_to_firstpromoter_on_checkout']) && $this->integration_settings['woo_send_sale_to_firstpromoter_on_checkout']; 131 132 if (defined('WP_DEBUG') && WP_DEBUG) { 133 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 134 error_log('FirstPromoter: Sale tracking enabled: ' . ($sale_tracking_enabled ? 'YES' : 'NO')); 135 } 136 137 if ($sale_tracking_enabled) { 138 if (defined('WP_DEBUG') && WP_DEBUG) { 139 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 140 error_log('FirstPromoter: Calling track_sale for order #' . $order_id); 141 } 142 $this->track_sale($order_id); 143 } 144 } catch (\Exception $e) { 145 // Log the error but don't break the payment process 146 if (defined('WP_DEBUG') && WP_DEBUG) { 147 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 148 error_log('FirstPromoter: Critical error in order_payment_successful for order #' . $order_id . ': ' . $e->getMessage()); 149 } 150 // Silently fail to ensure WooCommerce payment completion is not affected 90 // Only track for valid order statuses 91 $valid_statuses = array('processing', 'completed'); 92 if (! in_array($order_status, $valid_statuses)) { 93 $this->log('Order #' . $order_id . ' has invalid status (' . $order_status . '). Not tracking sale.'); 94 return; 95 } 96 97 $this->log('Calling track_sale for order #' . $order_id); 98 $this->track_sale($order_id); 99 } catch (\Exception $e) { 100 $this->log('Critical error in order_payment_successful for order #' . $order_id . ': ' . $e->getMessage()); 151 101 } 152 102 } … … 155 105 { 156 106 try { 157 if (defined('WP_DEBUG') && WP_DEBUG) { 158 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 159 error_log('FirstPromoter: track_sale called for order #' . $order_id); 160 } 107 $this->log('track_sale called for order #' . $order_id); 161 108 162 109 $order = wc_get_order($order_id); 163 110 if (!$order) { 164 if (defined('WP_DEBUG') && WP_DEBUG) { 165 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 166 error_log('FirstPromoter: Order not found for order #' . $order_id); 167 } 168 return; 169 } 170 171 $order_data = $order->get_data(); 172 $email = $order->get_billing_email(); 173 $customerId = $order->get_customer_id() != 0 ? $order->get_customer_id() : null; 111 $this->log('Order not found for order #' . $order_id); 112 return; 113 } 114 115 $order_data = $order->get_data(); 116 $email = $order->get_billing_email(); 117 $customerId = $order->get_customer_id() != 0 ? $order->get_customer_id() : null; 174 118 $order_status = $order->get_status(); 175 119 176 if (defined('WP_DEBUG') && WP_DEBUG) { 177 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 178 error_log('FirstPromoter: Order #' . $order_id . ' - Status: ' . $order_status . ', Email: ' . $email . ', Customer ID: ' . ($customerId ?: 'none')); 179 } 180 181 // Check if order is in a valid status for tracking (processing or completed) 182 $valid_statuses = ['processing', 'completed']; 183 if (!in_array($order_status, $valid_statuses)) { 184 if (defined('WP_DEBUG') && WP_DEBUG) { 185 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 186 error_log('FirstPromoter: Order #' . $order_id . ' has invalid status (' . $order_status . '). Not tracking sale.'); 187 } 188 return; 189 } 120 $this->log('Order #' . $order_id . ' - Status: ' . $order_status . ', Email: ' . $email . ', Customer ID: ' . ($customerId ? $customerId : 'none')); 190 121 191 122 if (!$email && !$customerId) { 192 if (defined('WP_DEBUG') && WP_DEBUG) { 193 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 194 error_log('FirstPromoter: Order #' . $order_id . ' has no email or customer ID. Not tracking sale.'); 195 } 123 $this->log('Order #' . $order_id . ' has no email or customer ID. Not tracking sale.'); 196 124 return; 197 125 } … … 199 127 // Check if sale was already tracked to prevent duplicates 200 128 if ($order->get_meta('firstpromoter_sale_tracked_on_fp')) { 201 if (defined('WP_DEBUG') && WP_DEBUG) { 202 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 203 error_log('FirstPromoter: Sale already tracked for order #' . $order_id); 204 } 129 $this->log('Sale already tracked for order #' . $order_id); 205 130 return; 206 131 } … … 214 139 $items = $order->get_items(); 215 140 216 $product_names = [];141 $product_names = array(); 217 142 foreach ($items as $item) { 218 143 $product_names[] = $item->get_name(); … … 224 149 $tid = isset($_COOKIE["_fprom_tid"]) ? sanitize_text_field(wp_unslash($_COOKIE["_fprom_tid"])) : null; 225 150 226 if (defined('WP_DEBUG') && WP_DEBUG) { 227 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 228 error_log('FirstPromoter: Order #' . $order_id . ' - Cookies - ref_id: ' . ($ref_id ?: 'none') . ', tid: ' . ($tid ?: 'none')); 229 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 230 error_log('FirstPromoter: Order #' . $order_id . ' - Sale data - event_id: ' . $event_id . ', amount: ' . $order_total . ', plan: ' . $plan); 231 } 232 233 if (defined('WP_DEBUG') && WP_DEBUG) { 234 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 235 error_log('FirstPromoter: Calling track_sale_via_api for order #' . $order_id); 236 } 151 $this->log('Order #' . $order_id . ' - Sale data - event_id: ' . $event_id . ', amount: ' . $order_total . ', plan: ' . $plan); 237 152 238 153 $response = $this->track_sale_via_api($email, $customerId, $event_id, $order_total, $plan, $coupon_code, $tid, $ref_id); 239 154 240 // Check if response is an actual response object (not an exception) 241 if ($response && is_object($response) && method_exists($response, 'getStatusCode')) { 242 if (defined('WP_DEBUG') && WP_DEBUG) { 243 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 244 error_log('FirstPromoter: API response status code: ' . $response->getStatusCode()); 245 } 246 247 if ($response->getStatusCode() == 200) { 248 $order->update_meta_data('firstpromoter_event_id', $event_id); 249 $order->update_meta_data('firstpromoter_sale_tracked_on_fp', true); 250 $order->save(); 251 252 if (defined('WP_DEBUG') && WP_DEBUG) { 253 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 254 error_log('FirstPromoter: Sale successfully tracked for order #' . $order_id); 255 } 256 } else { 257 if (defined('WP_DEBUG') && WP_DEBUG) { 258 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 259 error_log('FirstPromoter: API returned non-200 status code: ' . $response->getStatusCode() . ' for order #' . $order_id); 260 } 261 } 262 } elseif ($response && $response instanceof \Exception) { 263 // Response is actually an exception 264 if (defined('WP_DEBUG') && WP_DEBUG) { 265 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 266 error_log('FirstPromoter: API call failed with exception: ' . $response->getMessage()); 267 } 155 if ($response && is_array($response) && isset($response['status_code']) && $response['status_code'] == 200) { 156 $order->update_meta_data('firstpromoter_event_id', $event_id); 157 $order->update_meta_data('firstpromoter_sale_tracked_on_fp', true); 158 $order->save(); 159 160 $this->log('Sale successfully tracked for order #' . $order_id); 268 161 } else { 269 if (defined('WP_DEBUG') && WP_DEBUG) { 270 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 271 error_log('FirstPromoter: API call returned false/null for order #' . $order_id); 272 } 273 } 274 } catch (\Exception $e) { 275 if (defined('WP_DEBUG') && WP_DEBUG) { 276 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 277 error_log('FirstPromoter Integration WooCommerce Error: ' . $e->getMessage()); 278 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 279 error_log('FirstPromoter Integration WooCommerce Stack trace: ' . $e->getTraceAsString()); 280 } 281 // Silently fail to ensure order processing is not affected 162 $this->log('API call failed for order #' . $order_id); 163 } 164 } catch (\Exception $e) { 165 $this->log('WooCommerce track_sale error: ' . $e->getMessage()); 282 166 } 283 167 } … … 285 169 /** 286 170 * Track WooCommerce refund 287 * 171 * 288 172 * @param int $order_id Order ID 289 173 * @param int $refund_id Refund ID … … 304 188 if ($email || $customerId) { 305 189 $refund_event_id = $refund_id . "-" . time(); 306 $refund_amount = abs($refund->get_amount()) * 100; // Convert to cents190 $refund_amount = abs($refund->get_amount()) * 100; 307 191 $original_event_id = $order->get_meta('firstpromoter_event_id'); 308 192 … … 312 196 $refund_event_id, 313 197 $refund_amount, 314 '', // quantity - WooCommerce doesn't provide this easily198 '', 315 199 $original_event_id, 316 true // skip email notification200 true 317 201 ); 318 202 319 if ($response && method_exists($response, 'getStatusCode') && $response->getStatusCode()== 200) {203 if ($response && is_array($response) && isset($response['status_code']) && $response['status_code'] == 200) { 320 204 $refund->update_meta_data('firstpromoter_refund_tracked', true); 321 205 $refund->update_meta_data('firstpromoter_refund_event_id', $refund_event_id); … … 324 208 } 325 209 } catch (\Exception $e) { 326 if (defined('WP_DEBUG') && WP_DEBUG) { 327 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 328 error_log('FirstPromoter WooCommerce refund tracking error: ' . $e->getMessage()); 329 } 330 // Silently fail to ensure refund processing is not affected 210 $this->log('WooCommerce refund tracking error: ' . $e->getMessage()); 331 211 } 332 212 } … … 334 214 /** 335 215 * Track WooCommerce order cancellation 336 * 216 * 337 217 * @param int $order_id Order ID 338 218 */ … … 352 232 $response = $this->track_cancellation_via_api($email, $customerId); 353 233 354 if ($response && method_exists($response, 'getStatusCode') && $response->getStatusCode()== 200) {234 if ($response && is_array($response) && isset($response['status_code']) && $response['status_code'] == 200) { 355 235 $order->update_meta_data('firstpromoter_cancellation_tracked', true); 356 236 $order->save(); … … 358 238 } 359 239 } catch (\Exception $e) { 360 if (defined('WP_DEBUG') && WP_DEBUG) { 361 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs in debug mode 362 error_log('FirstPromoter WooCommerce cancellation tracking error: ' . $e->getMessage()); 363 } 364 // Silently fail to ensure order cancellation is not affected 240 $this->log('WooCommerce cancellation tracking error: ' . $e->getMessage()); 365 241 } 366 242 } -
firstpromoter/trunk/readme.txt
r3404859 r3458122 4 4 Requires at least: 5.0 5 5 Tested up to: 6.8 6 Stable tag: 0. 2.17 Requires PHP: 7.26 Stable tag: 0.3.0 7 Requires PHP: 5.6 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 151 151 == Changelog == 152 152 153 = 0.3.0 = 154 * New: Custom Form Tracking integration — track email submissions from any form using CSS selectors 155 * Supports multiple form selector pairs (email input + submit element) 156 * MutationObserver support for dynamically loaded forms (popups, AJAX content) 157 * Server-side AJAX fallback for ad-blocker bypass when force tracking is enabled 158 * Admin repeater UI for managing form selectors 159 * Removed Guzzle dependency - replaced with WordPress native wp_remote_post() for all API calls 160 * Lowered PHP requirement from 7.2 to 5.6 for broader hosting compatibility 161 * Fixed: AJAX fallback handler now registers once instead of per-integration 162 * Fixed: WooCommerce double-tracking on checkout (track_checkout and order_payment_successful both calling track_referral) 163 * Fixed: OptimizePress JS crash when button element not found in form 164 * Fixed: Removed IE-only window.attachEvent from OptimizePress integration 165 * Fixed: setcookie now includes SameSite attribute for modern browser compatibility 166 * Improved: Centralized API request handling via api_post() helper method 167 * Improved: Consolidated debug logging via log() helper to reduce boilerplate 168 * Improved: All API methods now return false on failure instead of exception objects 169 * Removed dead track_registration() method from WooCommerce integration 170 * Removed redundant cross-domain setting check 171 153 172 = 0.2.1 = 154 173 * Fixed: Comprehensive error handling to prevent API failures from breaking payments … … 183 202 == Upgrade Notice == 184 203 204 = 0.3.0 = 205 Removed Guzzle dependency, now uses WordPress native HTTP functions. PHP requirement lowered to 5.6. Multiple bug fixes for WooCommerce double-tracking and OptimizePress JS errors. 206 185 207 = 0.2.1 = 186 208 Critical bug fix: Ensure all FirstPromoter API calls Fail gracefully.
Note: See TracChangeset
for help on using the changeset viewer.