get_privacy_policy_content(); wp_add_privacy_policy_content( 'Maple Carts', $content ); } /** * Get privacy policy suggested content. * * @return string */ private function get_privacy_policy_content() { $retention_days = Maple_Carts::get_option( 'delete_after_days', 90 ); return sprintf( '

%s

%s

%s

%s

%s

%s

%s

%s

%s

%s

%s

%s

', __( 'Abandoned Cart Recovery', 'maple-carts' ), __( 'We use abandoned cart recovery technology to help remind you about items left in your shopping cart. This section explains what data we collect and how we use it.', 'maple-carts' ), __( 'What data we collect', 'maple-carts' ), __( 'When you begin the checkout process, we may collect:', 'maple-carts' ), __( 'Email address', 'maple-carts' ), __( 'Name and phone number', 'maple-carts' ), __( 'Billing address', 'maple-carts' ), __( 'Cart contents and total', 'maple-carts' ), __( 'IP address', 'maple-carts' ), __( 'Browser information', 'maple-carts' ), __( 'Why we collect this data', 'maple-carts' ), __( 'We collect this data to send you reminder emails if you leave items in your cart without completing your purchase. These emails may include a discount code to encourage you to complete your order.', 'maple-carts' ), __( 'How long we retain your data', 'maple-carts' ), sprintf( /* translators: %d: number of days */ __( 'Abandoned cart data is automatically deleted after %d days. You can request immediate deletion at any time.', 'maple-carts' ), $retention_days ), __( 'Your rights', 'maple-carts' ), __( 'You can unsubscribe from abandoned cart emails at any time by clicking the unsubscribe link in any email. You can also request a copy of your data or request deletion through our privacy tools.', 'maple-carts' ), __( 'Third-party sharing', 'maple-carts' ), __( 'We do not share your abandoned cart data with any third parties. Emails are sent through our own email system or your configured SMTP provider.', 'maple-carts' ) ); } /** * Register data exporter. * * @param array $exporters Existing exporters. * @return array */ public function register_data_exporter( $exporters ) { $exporters['maple-carts'] = [ 'exporter_friendly_name' => __( 'Maple Carts Abandoned Cart Data', 'maple-carts' ), 'callback' => [ $this, 'export_personal_data' ], ]; return $exporters; } /** * Register data eraser. * * @param array $erasers Existing erasers. * @return array */ public function register_data_eraser( $erasers ) { $erasers['maple-carts'] = [ 'eraser_friendly_name' => __( 'Maple Carts Abandoned Cart Data', 'maple-carts' ), 'callback' => [ $this, 'erase_personal_data' ], ]; return $erasers; } /** * Export personal data for a user. * * @param string $email_address Email address. * @param int $page Page number. * @return array */ public function export_personal_data( $email_address, $page = 1 ) { global $wpdb; $export_items = []; $table = $wpdb->prefix . MAPLE_CARTS_TABLE; $per_page = 10; $offset = ( $page - 1 ) * $per_page; $carts = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE email = %s LIMIT %d OFFSET %d", $email_address, $per_page, $offset ) ); foreach ( $carts as $cart ) { $cart_contents = maybe_unserialize( $cart->cart_contents ); $product_names = []; if ( is_array( $cart_contents ) ) { foreach ( $cart_contents as $item ) { $product_names[] = $item['name'] ?? 'Unknown Product'; } } $data = [ [ 'name' => __( 'Email', 'maple-carts' ), 'value' => $cart->email, ], [ 'name' => __( 'Name', 'maple-carts' ), 'value' => $cart->customer_name ?: __( 'Not provided', 'maple-carts' ), ], [ 'name' => __( 'Phone', 'maple-carts' ), 'value' => $cart->customer_phone ?: __( 'Not provided', 'maple-carts' ), ], [ 'name' => __( 'Cart Contents', 'maple-carts' ), 'value' => implode( ', ', $product_names ), ], [ 'name' => __( 'Cart Total', 'maple-carts' ), 'value' => wc_price( $cart->cart_total, [ 'currency' => $cart->currency ] ), ], [ 'name' => __( 'Status', 'maple-carts' ), 'value' => ucfirst( $cart->status ), ], [ 'name' => __( 'IP Address', 'maple-carts' ), 'value' => $cart->ip_address ?: __( 'Not recorded', 'maple-carts' ), ], [ 'name' => __( 'Created', 'maple-carts' ), 'value' => $cart->created_at, ], [ 'name' => __( 'Unsubscribed', 'maple-carts' ), 'value' => $cart->unsubscribed ? __( 'Yes', 'maple-carts' ) : __( 'No', 'maple-carts' ), ], ]; // Add billing data if present. $billing_data = maybe_unserialize( $cart->billing_data ); if ( ! empty( $billing_data ) ) { $address_parts = array_filter( [ $billing_data['address_1'] ?? '', $billing_data['address_2'] ?? '', $billing_data['city'] ?? '', $billing_data['state'] ?? '', $billing_data['postcode'] ?? '', $billing_data['country'] ?? '', ] ); if ( ! empty( $address_parts ) ) { $data[] = [ 'name' => __( 'Billing Address', 'maple-carts' ), 'value' => implode( ', ', $address_parts ), ]; } } $export_items[] = [ 'group_id' => 'maple-carts', 'group_label' => __( 'Abandoned Cart Data', 'maple-carts' ), 'group_description' => __( 'Cart data collected during checkout.', 'maple-carts' ), 'item_id' => 'cart-' . $cart->id, 'data' => $data, ]; } // Check if there are more records. $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE email = %s", $email_address ) ); $done = ( $page * $per_page ) >= $total; return [ 'data' => $export_items, 'done' => $done, ]; } /** * Erase personal data for a user. * * @param string $email_address Email address. * @param int $page Page number. * @return array */ public function erase_personal_data( $email_address, $page = 1 ) { global $wpdb; $table = $wpdb->prefix . MAPLE_CARTS_TABLE; $log_table = $wpdb->prefix . MAPLE_CARTS_EMAIL_LOG_TABLE; $per_page = 50; $items_removed = 0; $items_retained = 0; // Get carts to delete. $carts = $wpdb->get_results( $wpdb->prepare( "SELECT id FROM {$table} WHERE email = %s LIMIT %d", $email_address, $per_page ) ); foreach ( $carts as $cart ) { // Delete email logs first. $wpdb->delete( $log_table, [ 'cart_id' => $cart->id ] ); // Delete cart. $deleted = $wpdb->delete( $table, [ 'id' => $cart->id ] ); if ( $deleted ) { $items_removed++; } else { $items_retained++; } } // Check if there are more records. $remaining = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE email = %s", $email_address ) ); $done = ( $remaining == 0 ); return [ 'items_removed' => $items_removed, 'items_retained' => $items_retained, 'messages' => [], 'done' => $done, ]; } /** * Add consent checkbox to checkout. */ public function add_consent_checkbox() { $consent_text = Maple_Carts::get_option( 'consent_text', __( 'I agree to receive cart reminder emails if I don\'t complete my purchase.', 'maple-carts' ) ); woocommerce_form_field( 'maple_carts_consent', [ 'type' => 'checkbox', 'class' => [ 'form-row maple-carts-consent' ], 'label' => esc_html( $consent_text ), 'required' => false, ] ); } /** * Validate consent checkbox. */ public function validate_consent_checkbox() { // If consent is required and not given, we don't track the cart. // This is handled in the tracking class. } /** * Check if user has given consent. * * @return bool */ public static function has_consent() { // If consent is not required, always return true. if ( 'yes' !== Maple_Carts::get_option( 'require_consent', 'no' ) ) { return true; } // Check if consent checkbox was checked. if ( isset( $_POST['maple_carts_consent'] ) ) { return true; } // Check session for previous consent. if ( WC()->session && WC()->session->get( 'maple_carts_consent' ) ) { return true; } return false; } /** * Record consent. */ public static function record_consent() { if ( WC()->session ) { WC()->session->set( 'maple_carts_consent', true ); WC()->session->set( 'maple_carts_consent_time', current_time( 'mysql' ) ); } } /** * Handle data deletion request from unsubscribe page. */ public function handle_data_deletion_request() { if ( ! isset( $_GET['maple_delete_data'] ) ) { return; } $token = sanitize_text_field( wp_unslash( $_GET['maple_delete_data'] ) ); // Verify signed token (same as unsubscribe). $tracking = Maple_Carts_Tracking::instance(); $data = $this->verify_deletion_token( $token ); if ( ! $data || empty( $data['sid'] ) ) { wc_add_notice( __( 'This data deletion link has expired or is invalid.', 'maple-carts' ), 'error' ); wp_safe_redirect( wc_get_page_permalink( 'shop' ) ); exit; } $session_id = sanitize_text_field( $data['sid'] ); $cart = Maple_Carts_DB::get_cart_by_session( $session_id ); if ( $cart ) { // Delete all data for this cart. Maple_Carts_DB::delete_cart( $cart->id ); wc_add_notice( __( 'Your cart data has been permanently deleted.', 'maple-carts' ), 'success' ); } wp_safe_redirect( wc_get_page_permalink( 'shop' ) ); exit; } /** * Verify deletion token (reuses tracking token verification). * * @param string $token Token to verify. * @return array|false */ private function verify_deletion_token( $token ) { $decoded = base64_decode( $token, true ); if ( ! $decoded || strpos( $decoded, '|' ) === false ) { return false; } $parts = explode( '|', $decoded, 2 ); if ( count( $parts ) !== 2 ) { return false; } list( $payload, $signature ) = $parts; // Verify HMAC signature. $secret = wp_salt( 'auth' ) . 'maple_carts'; $expected_signature = hash_hmac( 'sha256', $payload, $secret ); if ( ! hash_equals( $expected_signature, $signature ) ) { return false; } $data = json_decode( $payload, true ); if ( ! $data ) { return false; } // Token valid for 90 days. if ( isset( $data['t'] ) && ( time() - $data['t'] ) > ( 90 * DAY_IN_SECONDS ) ) { return false; } return $data; } /** * Generate data deletion URL. * * @param string $session_id Session ID. * @return string */ public static function generate_deletion_url( $session_id ) { $data = [ 'sid' => $session_id, 't' => time(), ]; $payload = wp_json_encode( $data ); $secret = wp_salt( 'auth' ) . 'maple_carts'; $signature = hash_hmac( 'sha256', $payload, $secret ); $token = base64_encode( $payload . '|' . $signature ); return add_query_arg( 'maple_delete_data', $token, home_url() ); } /** * Get anonymized data (for when full deletion isn't required). * * @param int $cart_id Cart ID. */ public static function anonymize_cart( $cart_id ) { global $wpdb; $table = $wpdb->prefix . MAPLE_CARTS_TABLE; $wpdb->update( $table, [ 'email' => 'deleted@anonymized.invalid', 'customer_name' => 'Anonymized', 'customer_phone' => null, 'billing_data' => null, 'ip_address' => '0.0.0.0', 'user_agent' => null, ], [ 'id' => $cart_id ] ); } }