api_client = $api_client; $this->settings = $settings; $this->init_hooks(); } /** * Initialize WordPress hooks */ private function init_hooks() { // Add consent field to registration form (only once). add_action( 'woocommerce_register_form', array( $this, 'add_registration_consent_field' ) ); // Add consent field to my-account page. add_action( 'woocommerce_edit_account_form', array( $this, 'add_account_consent_field' ) ); // Validate consent field. add_action( 'woocommerce_register_post', array( $this, 'validate_registration_consent' ), 10, 3 ); // Save consent when user is created. add_action( 'woocommerce_created_customer', array( $this, 'save_registration_consent' ), 10, 3 ); // Save consent when account is updated. add_action( 'woocommerce_save_account_details', array( $this, 'save_account_consent' ), 10, 1 ); // Handle user registration to ActiveTrail. add_action( 'woocommerce_created_customer', array( $this, 'handle_user_registration' ), 15, 3 ); // AJAX handler for updating consent status. add_action( 'wp_ajax_activetrail_update_consent', array( $this, 'ajax_update_consent' ) ); add_action( 'wp_ajax_nopriv_activetrail_update_consent', array( $this, 'ajax_update_consent' ) ); } /** * Add consent field to registration form */ public function add_registration_consent_field() { $settings = ActiveTrail_Settings::get_all(); $show_marketing = ! empty( $settings['enable_registration_consent'] ); $show_tracking = ! empty( $settings['enable_tracking_consent'] ); if ( ! $show_marketing && ! $show_tracking ) { return; } // Prevent duplicate fields. static $field_added = false; if ( $field_added ) { return; } $field_added = true; if ( $show_marketing ) { // Use custom consent text from settings, or fall back to translated default. $consent_text = ! empty( $settings['consent_text'] ) ? $settings['consent_text'] : esc_html__( 'I agree to receive marketing emails', 'activetrail-for-woocommerce' ); // When "Marketing consent checked by default" is enabled, render the checkbox as checked. $checked_by_default = ! empty( $settings['consent_checked_by_default'] ); ?>

prepare_user_data( $user ); // Always set subscriptionStatus explicitly based on consent. $user_data['subscriptionStatus'] = true; // Since we know consent is given here // Make sure API client has the correct API key. $api_key = ActiveTrail_Settings::get( 'api_key' ); if ( ! empty( $api_key ) ) { $this->api_client->set_api_key( $api_key ); } // Send to ActiveTrail - wrap in try/catch to prevent form submission errors. try { $result = $this->api_client->import_customer_from_user( $user_data ); if ( $result['success'] ) { // Mark user as synced. update_user_meta( $user_id, '_activetrail_synced', 'yes' ); update_user_meta( $user_id, '_activetrail_sync_date', current_time( 'mysql' ) ); $this->log( 'User consent updated and synced successfully: ' . $user->user_email ); } else { $this->log( 'Failed to sync user after consent update ' . $user->user_email . ': ' . $result['message'], 'error' ); } } catch ( Exception $api_e ) { // Log the API error but don't fail the form submission. $this->log( 'API error during consent update: ' . $api_e->getMessage(), 'error' ); } } } else { update_user_meta( $user_id, '_activetrail_registration_consent', 'no' ); // Add standard meta keys for compatibility with other plugins. update_user_meta( $user_id, 'activetrail_optin', 'no' ); update_user_meta( $user_id, 'activetrail_subscribe', '0' ); } // Tracking consent (CNIL): only persist when the feature is enabled, so we // never overwrite a stored choice while the checkbox is hidden. if ( ! empty( ActiveTrail_Settings::get( 'enable_tracking_consent' ) ) ) { $tracking_given = $this->is_tracking_consent_given( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WordPress account form handles nonce verification update_user_meta( $user_id, '_activetrail_tracking_consent', $tracking_given ? 'yes' : 'no' ); update_user_meta( $user_id, '_activetrail_tracking_consent_date', current_time( 'mysql' ) ); } } catch ( Exception $e ) { // Log the error but don't fail the form submission. $this->log( 'Error during account consent update: ' . $e->getMessage(), 'error' ); } } /** * Determine whether tracking consent was given in posted data. * * Covers both the registration/account field name and the checkout field names. * * @param array $data Posted data. * @return bool */ private function is_tracking_consent_given( $data ) { if ( isset( $data['activetrail_tracking_registration_consent'] ) && sanitize_text_field( wp_unslash( $data['activetrail_tracking_registration_consent'] ) ) ) { return true; } if ( class_exists( 'ActiveTrail_Consent_Helper' ) && is_array( $data ) ) { return ActiveTrail_Consent_Helper::has_tracking_consent_from_data( $data ); } return false; } /** * Validate registration consent * * @param string $username Username. * @param string $email Email. * @param WP_Error $errors Errors object. */ public function validate_registration_consent( $username, $email, $errors ) { $settings = ActiveTrail_Settings::get_all(); if ( ! isset( $settings['enable_registration_consent'] ) || ! $settings['enable_registration_consent'] ) { return; } if ( empty( $settings['consent_required'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Registration form context $consent = isset( $_POST['activetrail_registration_consent'] ) ? sanitize_text_field( wp_unslash( $_POST['activetrail_registration_consent'] ) ) : ''; if ( empty( $consent ) ) { $errors->add( 'activetrail_consent_required', __( 'You must approve marketing emails in order to continue.', 'activetrail-for-woocommerce' ) ); } } /** * Save registration consent * * @param int $customer_id Customer ID. * @param array $new_customer_data Customer data. * @param string $password_generated Whether password was generated. */ public function save_registration_consent( $customer_id, $new_customer_data, $password_generated ) { try { // Save consent information. // Check registration form consent first. $has_consent = false; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WordPress registration form handles nonce verification if ( isset( $_POST['activetrail_registration_consent'] ) && sanitize_text_field( wp_unslash( $_POST['activetrail_registration_consent'] ) ) ) { $has_consent = true; } // Registration during checkout: consent may come from checkout checkbox (billing_activetrail_consent / activetrail_consent). if ( ! $has_consent && ! empty( $_POST ) && class_exists( 'ActiveTrail_Consent_Helper' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Checkout form context $has_consent = ActiveTrail_Consent_Helper::has_consent_from_data( $_POST ); } if ( $has_consent ) { update_user_meta( $customer_id, '_activetrail_registration_consent', 'yes' ); update_user_meta( $customer_id, '_activetrail_consent', 'yes' ); update_user_meta( $customer_id, '_activetrail_consent_date', current_time( 'mysql' ) ); // Add standard meta keys for compatibility with other plugins. update_user_meta( $customer_id, 'activetrail_optin', 'yes' ); update_user_meta( $customer_id, 'activetrail_subscribe', '1' ); // Note: We don't need to sync here as handle_user_registration will do it. // This is just to record the consent status. } else { update_user_meta( $customer_id, '_activetrail_registration_consent', 'no' ); update_user_meta( $customer_id, '_activetrail_consent', 'no' ); // Add standard meta keys for compatibility with other plugins. update_user_meta( $customer_id, 'activetrail_optin', 'no' ); update_user_meta( $customer_id, 'activetrail_subscribe', '0' ); } // Tracking consent (CNIL): always store an explicit yes/no when the feature // is enabled, from either the registration field or the checkout checkbox. if ( ! empty( ActiveTrail_Settings::get( 'enable_tracking_consent' ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Registration/checkout form context $tracking_given = $this->is_tracking_consent_given( $_POST ); update_user_meta( $customer_id, '_activetrail_tracking_consent', $tracking_given ? 'yes' : 'no' ); update_user_meta( $customer_id, '_activetrail_tracking_consent_date', current_time( 'mysql' ) ); } // Registration during checkout: copy billing fields from POST to user meta // so that prepare_user_data (in handle_user_registration) can send phone, street, zip to ActiveTrail. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Checkout form context if ( ! empty( $_POST['billing_phone'] ) ) { update_user_meta( $customer_id, 'billing_phone', sanitize_text_field( wp_unslash( $_POST['billing_phone'] ) ) ); } if ( ! empty( $_POST['billing_address_1'] ) ) { update_user_meta( $customer_id, 'billing_address_1', sanitize_text_field( wp_unslash( $_POST['billing_address_1'] ) ) ); } if ( isset( $_POST['billing_address_2'] ) ) { update_user_meta( $customer_id, 'billing_address_2', sanitize_text_field( wp_unslash( $_POST['billing_address_2'] ) ) ); } if ( ! empty( $_POST['billing_city'] ) ) { update_user_meta( $customer_id, 'billing_city', sanitize_text_field( wp_unslash( $_POST['billing_city'] ) ) ); } if ( ! empty( $_POST['billing_postcode'] ) ) { update_user_meta( $customer_id, 'billing_postcode', sanitize_text_field( wp_unslash( $_POST['billing_postcode'] ) ) ); } } catch ( Exception $e ) { // Log the error but don't fail the registration. $this->log( 'Error during registration consent save: ' . $e->getMessage(), 'error' ); } } /** * Handle user registration to ActiveTrail * * @param int $customer_id Customer ID. * @param array $new_customer_data Customer data. * @param string $password_generated Whether password was generated. */ public function handle_user_registration( $customer_id, $new_customer_data, $password_generated ) { try { // Get settings. $settings = ActiveTrail_Settings::get_all(); // Check if we should sync this user. if ( ! $this->should_sync_user( $customer_id ) ) { return; } // Get user data. $user = get_user_by( 'id', $customer_id ); if ( ! $user ) { return; } // CRITICAL: Check if there's already a guest contact with this email. // If user registered during checkout, a guest contact might already exist. // We should not create a duplicate - the existing guest contact will be used. // and the order will link to it via the registered external ID when synced. try { $api_key = ActiveTrail_Settings::get( 'api_key' ); if ( ! empty( $api_key ) ) { $this->api_client->set_api_key( $api_key ); } // Check if contact already exists (could be guest contact created during cart/checkout). $existing_contact_check = $this->api_client->get_customer_by_email( $user->user_email ); if ( ! is_wp_error( $existing_contact_check ) && ! empty( $existing_contact_check['exists'] ) ) { // Contact already exists (likely a guest contact from checkout). data may be array of contacts. $data = $existing_contact_check['data']; $first = ( is_array( $data ) && isset( $data[0] ) ) ? $data[0] : $data; $existing_external_id = isset( $first['externalId'] ) ? $first['externalId'] : 'unknown'; $this->log( 'User registered but contact already exists in ActiveTrail (likely from checkout): ' . $existing_external_id . ' - skipping duplicate creation', 'info' ); // Mark user as synced even though we didn't create new contact. // The existing guest contact will be used for orders. update_user_meta( $customer_id, '_activetrail_synced', 'yes' ); update_user_meta( $customer_id, '_activetrail_sync_date', current_time( 'mysql' ) ); update_user_meta( $customer_id, '_activetrail_existing_contact_id', $existing_external_id ); return; // Exit early - don't create duplicate } } catch ( Exception $check_e ) { // If check fails, continue with normal registration process. $this->log( 'Could not check for existing contact during registration: ' . $check_e->getMessage(), 'debug' ); } // Prepare user data for ActiveTrail. $user_data = $this->prepare_user_data( $user ); // Make sure API client has the correct API key. $api_key = ActiveTrail_Settings::get( 'api_key' ); if ( ! empty( $api_key ) ) { $this->api_client->set_api_key( $api_key ); } // Send to ActiveTrail - wrap in try/catch to prevent registration errors. try { $result = $this->api_client->import_customer_from_user( $user_data ); if ( $result['success'] ) { // Mark user as synced. update_user_meta( $customer_id, '_activetrail_synced', 'yes' ); update_user_meta( $customer_id, '_activetrail_sync_date', current_time( 'mysql' ) ); $this->log( 'User synced successfully: ' . $user->user_email ); } else { $this->log( 'Failed to sync user ' . $user->user_email . ': ' . $result['message'], 'error' ); } } catch ( Exception $api_e ) { // Log the API error but don't fail the registration. $this->log( 'API error during user registration sync: ' . $api_e->getMessage(), 'error' ); } } catch ( Exception $e ) { // Log the error but don't let it bubble up to the user interface. $this->log( 'Error during user registration sync: ' . $e->getMessage(), 'error' ); } } /** * Check if user should be synced to ActiveTrail * * @param int $customer_id Customer ID. * @return bool */ private function should_sync_user( $customer_id ) { // Always sync users to ActiveTrail. // The subscriptionStatus will be set based on consent in prepare_user_data. return true; } /** * Prepare user data for ActiveTrail API * * @param WP_User $user User object. * @return array */ private function prepare_user_data( $user ) { // Get settings. $settings = ActiveTrail_Settings::get_all(); // Get user meta data. $first_name = get_user_meta( $user->ID, 'first_name', true ); $last_name = get_user_meta( $user->ID, 'last_name', true ); $phone = get_user_meta( $user->ID, 'billing_phone', true ); // Get address data from user meta. $address_1 = get_user_meta( $user->ID, 'billing_address_1', true ); $address_2 = get_user_meta( $user->ID, 'billing_address_2', true ); $city = get_user_meta( $user->ID, 'billing_city', true ); $postcode = get_user_meta( $user->ID, 'billing_postcode', true ); // Combine address lines. $address = trim( $address_1 . ' ' . $address_2 ); // Get consent information from both registration and checkout consent. $reg_consent = get_user_meta( $user->ID, '_activetrail_registration_consent', true ); $checkout_consent = get_user_meta( $user->ID, '_activetrail_consent', true ); $has_consent = ( $reg_consent === 'yes' || $checkout_consent === 'yes' ); $subscriber_status = $has_consent; // Tracking consent (CNIL) - always sent as a boolean. $tracking_consent = ( get_user_meta( $user->ID, '_activetrail_tracking_consent', true ) === 'yes' ); // Format data according to what the API client expects. $user_data = array( 'email' => $user->user_email, 'first_name' => $first_name ? $first_name : '', 'last_name' => $last_name ? $last_name : '', 'phone' => $phone ? $phone : '', 'address' => $address ? $address : '', 'street' => $address ? $address : '', // Map to street for API 'city' => $city ? $city : '', 'zip_code' => $postcode ? $postcode : '', 'postcode' => $postcode ? $postcode : '', // Also include as postcode for compatibility 'consent' => $has_consent, 'subscriptionStatus' => $subscriber_status, // Always set explicitly 'tracking_consent' => $tracking_consent, // CNIL tracking consent, always boolean 'registration_date' => $user->user_registered, 'source' => 'woocommerce_registration', 'group_id' => isset( $settings['group_id'] ) ? $settings['group_id'] : '', // Add required fields for the API client. 'externalId' => activetrail_external_id_for_contact( $user->ID, $user->user_email ), 'externalName' => ACTIVETRAIL_EXTERNAL_NAME, ); $this->log( 'Prepared user data: ' . wp_json_encode( $user_data ), 'debug' ); return $user_data; } /** * Log message * * @param string $message Message to log. * @param string $level Log level. */ private function log( $message, $level = 'info' ) { if ( class_exists( 'ActiveTrail_Logger' ) ) { ActiveTrail_Logger::get_instance()->log( $message, $level ); } } /** * AJAX handler for updating consent status */ public function ajax_update_consent() { try { // Verify nonce. $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : ''; if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'activetrail_nonce' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'activetrail-for-woocommerce' ), ) ); return; } // Check if user is logged in. if ( ! is_user_logged_in() ) { wp_send_json_error( array( 'message' => esc_html__( 'You must be logged in to update consent status', 'activetrail-for-woocommerce' ), ) ); return; } // Get current user. $user_id = get_current_user_id(); if ( ! $user_id ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid user', 'activetrail-for-woocommerce' ), ) ); return; } // Get consent status from request. $consent = isset( $_POST['consent'] ) ? (bool) sanitize_text_field( wp_unslash( $_POST['consent'] ) ) : false; // Update user meta. update_user_meta( $user_id, '_activetrail_registration_consent', $consent ? 'yes' : 'no' ); // Add standard meta keys for compatibility with other plugins. update_user_meta( $user_id, 'activetrail_optin', $consent ? 'yes' : 'no' ); update_user_meta( $user_id, 'activetrail_subscribe', $consent ? '1' : '0' ); if ( $consent ) { update_user_meta( $user_id, '_activetrail_consent_date', current_time( 'mysql' ) ); } // If consent is given, sync user to ActiveTrail. if ( $consent ) { $user = get_user_by( 'id', $user_id ); if ( $user ) { // Prepare user data for ActiveTrail. $user_data = $this->prepare_user_data( $user ); // Make sure API client has the correct API key. $api_key = ActiveTrail_Settings::get( 'api_key' ); if ( ! empty( $api_key ) ) { $this->api_client->set_api_key( $api_key ); } // Send to ActiveTrail - wrap in try/catch to prevent AJAX errors. try { $result = $this->api_client->import_customer_from_user( $user_data ); if ( $result['success'] ) { // Mark user as synced. update_user_meta( $user_id, '_activetrail_synced', 'yes' ); update_user_meta( $user_id, '_activetrail_sync_date', current_time( 'mysql' ) ); $this->log( 'User consent updated and synced successfully: ' . $user->user_email ); wp_send_json_success( array( 'message' => esc_html__( 'Consent updated and synced successfully', 'activetrail-for-woocommerce' ), 'synced' => true, ) ); return; } else { $this->log( 'Failed to sync user after consent update ' . $user->user_email . ': ' . $result['message'], 'error' ); wp_send_json_success( array( 'message' => esc_html__( 'Consent updated but sync failed', 'activetrail-for-woocommerce' ), 'synced' => false, 'error' => $result['message'], ) ); return; } } catch ( Exception $api_e ) { // Log the API error but still return success for the consent update. $this->log( 'API error during AJAX consent update: ' . $api_e->getMessage(), 'error' ); wp_send_json_success( array( 'message' => esc_html__( 'Consent updated but sync failed', 'activetrail-for-woocommerce' ), 'synced' => false, 'error' => $api_e->getMessage(), ) ); return; } } } // If we got here, consent was updated but no sync was needed. wp_send_json_success( array( 'message' => esc_html__( 'Consent updated successfully', 'activetrail-for-woocommerce' ), 'synced' => false, ) ); } catch ( Exception $e ) { // Log the error. $this->log( 'Error during consent update: ' . $e->getMessage(), 'error' ); wp_send_json_error( array( 'message' => esc_html__( 'An error occurred while updating consent', 'activetrail-for-woocommerce' ), 'error' => $e->getMessage(), ) ); } } }