Changeset 3489862
- Timestamp:
- 03/24/2026 10:29:45 AM (4 days ago)
- Location:
- cartflush-autoclear-cart-for-inactive-users/trunk
- Files:
-
- 6 edited
-
assets/css/admin.css (modified) (3 diffs)
-
cartflush-autoclear-cart-for-inactive-users.php (modified) (2 diffs)
-
includes/admin/class-cartflush-admin.php (modified) (19 diffs)
-
includes/class-cartflush-plugin.php (modified) (3 diffs)
-
includes/class-cartflush-rules.php (modified) (8 diffs)
-
readme.txt (modified) (8 diffs)
Legend:
- Unmodified
- Added
- Removed
-
cartflush-autoclear-cart-for-inactive-users/trunk/assets/css/admin.css
r3488606 r3489862 256 256 } 257 257 258 .cartflush-toggle { 259 display: inline-flex; 260 align-items: center; 261 gap: 10px; 262 padding: 12px 14px; 263 border: 1px solid var(--cf-border); 264 border-radius: 14px; 265 background: var(--cf-surface-soft); 266 color: var(--cf-title); 267 font-weight: 600; 268 } 269 270 .cartflush-toggle input[type="checkbox"] { 271 margin: 0; 272 } 273 258 274 .cartflush-rule-grid, 259 275 .cartflush-summary { … … 506 522 } 507 523 524 .cartflush-tool-actions { 525 display: flex; 526 flex-wrap: wrap; 527 gap: 10px; 528 } 529 508 530 .cartflush-upload-field { 509 531 display: grid; … … 606 628 } 607 629 630 .cartflush-tool-actions { 631 flex-direction: column; 632 } 633 608 634 .cartflush-rule-table__action, 609 635 .cartflush-row-action { -
cartflush-autoclear-cart-for-inactive-users/trunk/cartflush-autoclear-cart-for-inactive-users.php
r3488606 r3489862 4 4 * Plugin URI: https://wordpress.org/plugins/cartflush-autoclear-cart-for-inactive-users/ 5 5 * Description: Automatically clears WooCommerce carts after inactivity with configurable default timeouts, import/export tools, and rule-based exclusions. 6 * Version: 2. 1.06 * Version: 2.2.0 7 7 * Requires at least: 5.8 8 8 * Requires PHP: 7.4 … … 21 21 } 22 22 23 define( 'CARTFLUSH_VERSION', '2. 1.0' );23 define( 'CARTFLUSH_VERSION', '2.2.0' ); 24 24 define( 'CARTFLUSH_FILE', __FILE__ ); 25 25 define( 'CARTFLUSH_PATH', plugin_dir_path( __FILE__ ) ); -
cartflush-autoclear-cart-for-inactive-users/trunk/includes/admin/class-cartflush-admin.php
r3488606 r3489862 28 28 add_action( 'admin_post_cartflush_import_csv', [ $this, 'handle_csv_import' ] ); 29 29 add_action( 'admin_post_cartflush_export_json', [ $this, 'handle_json_export' ] ); 30 add_action( 'admin_post_cartflush_download_csv_sample', [ $this, 'handle_csv_sample_download' ] ); 30 31 add_filter( 'option_page_capability_cartflush_settings_group', [ $this, 'settings_capability' ] ); 31 32 } … … 37 38 public function register_settings() { 38 39 register_setting( 'cartflush_settings_group', 'cartflush_expiration_time', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 30 ] ); 40 register_setting( 'cartflush_settings_group', 'cartflush_enable_warning_notice', [ 'type' => 'string', 'sanitize_callback' => [ $this, 'sanitize_yes_no_option' ], 'default' => 'no' ] ); 41 register_setting( 'cartflush_settings_group', 'cartflush_warning_notice_minutes', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 5 ] ); 39 42 register_setting( 'cartflush_settings_group', CartFlush_Rules::OPTION_NAME, [ 'type' => 'array', 'sanitize_callback' => [ $this, 'sanitize_rules_option' ], 'default' => $this->rules->get_default_rules() ] ); 40 43 add_settings_section( 'cartflush_main', __( 'Timeout Settings', 'cartflush' ), [ $this, 'render_main_section' ], 'cartflush-settings' ); 41 44 add_settings_field( 'cartflush_expiration_time', __( 'Default cart expiration', 'cartflush' ), [ $this, 'render_expiration_field' ], 'cartflush-settings', 'cartflush_main' ); 45 add_settings_field( 'cartflush_enable_warning_notice', __( 'Expiry warning notice', 'cartflush' ), [ $this, 'render_warning_toggle_field' ], 'cartflush-settings', 'cartflush_main' ); 46 add_settings_field( 'cartflush_warning_notice_minutes', __( 'Warning threshold', 'cartflush' ), [ $this, 'render_warning_minutes_field' ], 'cartflush-settings', 'cartflush_main' ); 42 47 } 43 48 44 49 public function sanitize_rules_option( $value ) { 50 $this->store_duplicate_rule_warnings( is_array( $value ) ? $value : [] ); 45 51 return $this->rules->normalize_rules_data( is_array( $value ) ? $this->prepare_rules_for_storage( $value ) : $value ); 46 52 } … … 69 75 } 70 76 77 public function render_warning_toggle_field() { 78 $value = get_option( 'cartflush_enable_warning_notice', 'no' ); 79 ?> 80 <label class="cartflush-toggle"> 81 <input type="hidden" name="cartflush_enable_warning_notice" value="no"> 82 <input type="checkbox" name="cartflush_enable_warning_notice" value="yes" <?php checked( $value, 'yes' ); ?>> 83 <span><?php esc_html_e( 'Show a shopper notice before CartFlush clears the cart.', 'cartflush' ); ?></span> 84 </label> 85 <p class="description"><?php esc_html_e( 'The warning appears on the cart and checkout pages shortly before the timeout is reached.', 'cartflush' ); ?></p> 86 <?php 87 } 88 89 public function render_warning_minutes_field() { 90 $value = (int) get_option( 'cartflush_warning_notice_minutes', 5 ); 91 echo '<label class="cartflush-field"><input type="number" min="1" step="1" class="small-text" name="cartflush_warning_notice_minutes" value="' . esc_attr( $value ) . '"> <span>' . esc_html__( 'minutes before expiry', 'cartflush' ) . '</span></label>'; 92 echo '<p class="description">' . esc_html__( 'Choose how soon before expiry the shopper warning notice appears.', 'cartflush' ) . '</p>'; 93 } 94 71 95 public function render_admin_notices() { 72 96 if ( ! isset( $_GET['page'] ) || 'cartflush-settings' !== sanitize_key( wp_unslash( $_GET['page'] ) ) ) { … … 81 105 if ( ! empty( $_GET['cartflush_error'] ) ) { 82 106 echo '<div class="notice notice-error"><p>' . esc_html( wp_unslash( $_GET['cartflush_error'] ) ) . '</p></div>'; 107 } 108 $warnings = get_transient( 'cartflush_admin_duplicate_warnings_' . get_current_user_id() ); 109 if ( is_array( $warnings ) && ! empty( $warnings ) ) { 110 delete_transient( 'cartflush_admin_duplicate_warnings_' . get_current_user_id() ); 111 echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html__( 'Some duplicate rules were detected. CartFlush kept the last saved value for each duplicate key.', 'cartflush' ) . '</p><ul>'; 112 foreach ( $warnings as $warning ) { 113 echo '<li>' . esc_html( $warning ) . '</li>'; 114 } 115 echo '</ul></div>'; 83 116 } 84 117 } … … 97 130 $default_timeout = isset( $data['cartflush_expiration_time'] ) ? absint( $data['cartflush_expiration_time'] ) : get_option( 'cartflush_expiration_time', 30 ); 98 131 $rules = isset( $data['import_rules'] ) ? $data['import_rules'] : $data; 132 $warning_enabled = isset( $data['cartflush_enable_warning_notice'] ) ? $this->sanitize_yes_no_option( $data['cartflush_enable_warning_notice'] ) : get_option( 'cartflush_enable_warning_notice', 'no' ); 133 $warning_minutes = isset( $data['cartflush_warning_notice_minutes'] ) ? absint( $data['cartflush_warning_notice_minutes'] ) : get_option( 'cartflush_warning_notice_minutes', 5 ); 99 134 update_option( 'cartflush_expiration_time', max( 1, $default_timeout ) ); 135 update_option( 'cartflush_enable_warning_notice', $warning_enabled ); 136 update_option( 'cartflush_warning_notice_minutes', max( 1, $warning_minutes ) ); 100 137 update_option( CartFlush_Rules::OPTION_NAME, $this->rules->normalize_rules_data( $rules ) ); 101 138 $this->redirect_with_message( __( 'JSON settings imported successfully.', 'cartflush' ) ); … … 141 178 } 142 179 break; 180 case 'cart_value': 181 $range = $this->parse_cart_value_key( $key ); 182 if ( $range && $timeout > 0 ) { 183 $rules['cart_value_rules'][] = [ 184 'minimum' => $range['minimum'], 185 'maximum' => $range['maximum'], 186 'timeout' => $timeout, 187 ]; 188 } 189 break; 143 190 case 'product_rule': 144 191 if ( absint( $key ) > 0 && $timeout > 0 ) { … … 178 225 } 179 226 227 public function handle_csv_sample_download() { 228 $this->assert_admin_permissions(); 229 check_admin_referer( 'cartflush_download_csv_sample' ); 230 nocache_headers(); 231 header( 'Content-Type: text/csv; charset=utf-8' ); 232 header( 'Content-Disposition: attachment; filename=cartflush-sample-rules.csv' ); 233 234 $handle = fopen( 'php://output', 'w' ); 235 236 if ( ! $handle ) { 237 exit; 238 } 239 240 fputcsv( $handle, [ 'type', 'key', 'timeout_minutes' ] ); 241 fputcsv( $handle, [ 'customer_type', 'guest', '20' ] ); 242 fputcsv( $handle, [ 'role', 'customer', '30' ] ); 243 fputcsv( $handle, [ 'cart_value', '100+', '90' ] ); 244 fputcsv( $handle, [ 'product_rule', '321', '10' ] ); 245 fputcsv( $handle, [ 'category', 'flash-sale', '15' ] ); 246 fputcsv( $handle, [ 'tag', 'seasonal', '25' ] ); 247 fputcsv( $handle, [ 'excluded_role', 'wholesale_customer', '' ] ); 248 fputcsv( $handle, [ 'excluded_product', '123', '' ] ); 249 fputcsv( $handle, [ 'excluded_category', 'high-ticket', '' ] ); 250 fputcsv( $handle, [ 'excluded_tag', 'fragile', '' ] ); 251 fclose( $handle ); 252 exit; 253 } 254 180 255 public function handle_json_export() { 181 256 $this->assert_admin_permissions(); 182 257 check_admin_referer( 'cartflush_export_json' ); 183 $payload = [ 'plugin' => 'CartFlush', 'version' => CARTFLUSH_VERSION, 'exported_at' => gmdate( 'c' ), 'cartflush_expiration_time' => (int) get_option( 'cartflush_expiration_time', 30 ), ' import_rules' => $this->rules->get_rules_option() ];258 $payload = [ 'plugin' => 'CartFlush', 'version' => CARTFLUSH_VERSION, 'exported_at' => gmdate( 'c' ), 'cartflush_expiration_time' => (int) get_option( 'cartflush_expiration_time', 30 ), 'cartflush_enable_warning_notice' => get_option( 'cartflush_enable_warning_notice', 'no' ), 'cartflush_warning_notice_minutes' => (int) get_option( 'cartflush_warning_notice_minutes', 5 ), 'import_rules' => $this->rules->get_rules_option() ]; 184 259 nocache_headers(); 185 260 header( 'Content-Type: application/json; charset=utf-8' ); … … 220 295 <ul class="cartflush-bullet-list"> 221 296 <li><?php esc_html_e( 'Customer type and role timeouts', 'cartflush' ); ?></li> 222 <li><?php esc_html_e( ' Product, category, and tag rules', 'cartflush' ); ?></li>297 <li><?php esc_html_e( 'Cart value, product, category, and tag rules', 'cartflush' ); ?></li> 223 298 <li><?php esc_html_e( 'Role, product, category, and tag exclusions', 'cartflush' ); ?></li> 224 299 </ul> … … 253 328 $this->render_rule_card( __( 'Customer Type Rules', 'cartflush' ), __( 'Set separate timeouts for guest and logged-in customers.', 'cartflush' ), 'customer-type', __( 'Add Rule', 'cartflush' ), [ __( 'Customer Type', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['customer_type_rules'] ), __( 'Use this when guest carts and account carts should expire differently.', 'cartflush' ), function() use ( $rules, $customer_types ) { $this->render_customer_type_rows( $rules['customer_type_rules'], $customer_types ); } ); 254 329 $this->render_rule_card( __( 'Role Rules', 'cartflush' ), __( 'Override the default timeout for specific user roles.', 'cartflush' ), 'role-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Role', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['role_rules'] ), __( 'Perfect for wholesale, shop manager, or VIP-specific behavior.', 'cartflush' ), function() use ( $rules, $roles ) { $this->render_map_timeout_rows( 'role_rules', 'role', $rules['role_rules'], $roles, __( 'Select a role', 'cartflush' ) ); } ); 330 $this->render_rule_card( __( 'Cart Value Rules', 'cartflush' ), __( 'Match carts by subtotal range to give larger or smaller orders more time.', 'cartflush' ), 'cart-value-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Min Value', 'cartflush' ), __( 'Max Value', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['cart_value_rules'] ), __( 'Leave the maximum value empty to create an open-ended range like 100+.', 'cartflush' ), function() use ( $rules ) { $this->render_cart_value_rows( 'cart_value_rules', $rules['cart_value_rules'] ); } ); 255 331 $this->render_rule_card( __( 'Product Rules', 'cartflush' ), __( 'Set a timeout for a single WooCommerce product ID.', 'cartflush' ), 'product-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Product ID', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['product_rules'] ), __( 'Use product-level rules when one item needs a special cart lifetime.', 'cartflush' ), function() use ( $rules ) { $this->render_product_timeout_rows( 'product_rules', $rules['product_rules'] ); } ); 256 332 $this->render_rule_card( __( 'Category Rules', 'cartflush' ), __( 'Apply timeouts using WooCommerce product categories.', 'cartflush' ), 'category-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Category', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['category_rules'] ), __( 'Great for grouping timeouts across collections instead of product by product.', 'cartflush' ), function() use ( $rules, $categories ) { $this->render_map_timeout_rows( 'category_rules', 'slug', $rules['category_rules'], $categories, __( 'Select a category', 'cartflush' ) ); } ); … … 296 372 </div> 297 373 298 <div class="cartflush-sidecard">299 <div class="cartflush-sidecard__header">300 <span class="cartflush-chip"><?php esc_html_e( 'CSV Import', 'cartflush' ); ?></span>301 <strong><?php esc_html_e( 'Bulk Rules', 'cartflush' ); ?></strong>302 </div>303 <p><?php esc_html_e( 'Upload a CSV using the headers type, key, timeout_minutes. Exclusion rows can keep timeout_minutes empty .', 'cartflush' ); ?></p>374 <div class="cartflush-sidecard"> 375 <div class="cartflush-sidecard__header"> 376 <span class="cartflush-chip"><?php esc_html_e( 'CSV Import', 'cartflush' ); ?></span> 377 <strong><?php esc_html_e( 'Bulk Rules', 'cartflush' ); ?></strong> 378 </div> 379 <p><?php esc_html_e( 'Upload a CSV using the headers type, key, timeout_minutes. Exclusion rows can keep timeout_minutes empty, and cart value ranges can use formats like 100+ or 50-200.', 'cartflush' ); ?></p> 304 380 <div class="cartflush-code-list"> 305 381 <code>customer_type</code> 306 382 <code>role</code> 383 <code>cart_value</code> 307 384 <code>product_rule</code> 308 385 <code>category</code> … … 316 393 <input type="file" name="cartflush_csv_file" accept=".csv,text/csv" required> 317 394 </label> 318 <?php submit_button( __( 'Import CSV', 'cartflush' ), 'secondary', 'submit', false ); ?> 395 <div class="cartflush-tool-actions"> 396 <?php submit_button( __( 'Import CSV', 'cartflush' ), 'secondary', 'submit', false ); ?> 397 <a class="button button-secondary" href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=cartflush_download_csv_sample' ), 'cartflush_download_csv_sample' ) ); ?>"><?php esc_html_e( 'Download Sample', 'cartflush' ); ?></a> 398 </div> 319 399 </form> 320 400 </div> … … 402 482 <script type="text/html" id="tmpl-cartflush-customer-type"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[customer_type_rules][{{index}}][type]', '', $customer_types, __( 'Select customer type', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[customer_type_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 403 483 <script type="text/html" id="tmpl-cartflush-role-rule"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[role_rules][{{index}}][role]', '', $roles, __( 'Select a role', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[role_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 484 <script type="text/html" id="tmpl-cartflush-cart-value-rule"><tr><td><input type="number" min="0" step="0.01" class="small-text" name="cartflush_import_rules[cart_value_rules][{{index}}][minimum]" value=""></td><td><input type="number" min="0" step="0.01" class="small-text" name="cartflush_import_rules[cart_value_rules][{{index}}][maximum]" value=""></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[cart_value_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 404 485 <script type="text/html" id="tmpl-cartflush-product-rule"><tr><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[product_rules][{{index}}][product_id]" value=""></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[product_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 405 486 <script type="text/html" id="tmpl-cartflush-category-rule"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[category_rules][{{index}}][slug]', '', $categories, __( 'Select a category', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[category_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> … … 427 508 } 428 509 510 private function render_cart_value_rows( $group, $items ) { 511 if ( empty( $items ) ) { echo $this->get_cart_value_row( $group, 0, '', '', '' ); return; } // phpcs:ignore 512 foreach ( array_values( $items ) as $index => $item ) { 513 $minimum = isset( $item['minimum'] ) ? $item['minimum'] : ''; 514 $maximum = isset( $item['maximum'] ) ? $item['maximum'] : ''; 515 $timeout = isset( $item['timeout'] ) ? $item['timeout'] : ''; 516 echo $this->get_cart_value_row( $group, $index, $minimum, $maximum, $timeout ); // phpcs:ignore 517 } 518 } 519 429 520 private function render_simple_select_rows( $group, $field, $items, $options, $placeholder ) { 430 521 if ( empty( $items ) ) { echo $this->get_simple_select_row( $group, 0, $field, '', $options, $placeholder ); return; } // phpcs:ignore … … 443 534 private function get_number_timeout_row( $group, $index, $value, $timeout ) { 444 535 ob_start(); ?><tr><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][product_id]' ); ?>" value="<?php echo esc_attr( $value ); ?>"></td><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][timeout]' ); ?>" value="<?php echo esc_attr( $timeout ); ?>"></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr><?php return (string) ob_get_clean(); 536 } 537 538 private function get_cart_value_row( $group, $index, $minimum, $maximum, $timeout ) { 539 ob_start(); ?><tr><td><input type="number" min="0" step="0.01" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][minimum]' ); ?>" value="<?php echo esc_attr( $minimum ); ?>"></td><td><input type="number" min="0" step="0.01" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][maximum]' ); ?>" value="<?php echo esc_attr( $maximum ); ?>"></td><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][timeout]' ); ?>" value="<?php echo esc_attr( $timeout ); ?>"></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr><?php return (string) ob_get_clean(); 445 540 } 446 541 … … 467 562 'customer_type_rules' => $this->prepare_timeout_rows( isset( $value['customer_type_rules'] ) ? $value['customer_type_rules'] : [], 'type', 'sanitize_key' ), 468 563 'role_rules' => $this->prepare_timeout_rows( isset( $value['role_rules'] ) ? $value['role_rules'] : [], 'role', 'sanitize_key' ), 564 'cart_value_rules' => $this->prepare_cart_value_rows( isset( $value['cart_value_rules'] ) ? $value['cart_value_rules'] : [] ), 469 565 'product_rules' => $this->prepare_integer_timeout_rows( isset( $value['product_rules'] ) ? $value['product_rules'] : [], 'product_id' ), 470 566 'category_rules' => $this->prepare_timeout_rows( isset( $value['category_rules'] ) ? $value['category_rules'] : [], 'slug', 'sanitize_title' ), … … 485 581 } 486 582 583 private function prepare_cart_value_rows( $rows ) { 584 $prepared = []; if ( ! is_array( $rows ) ) { return $prepared; } foreach ( $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $minimum = isset( $row['minimum'] ) ? (float) wc_format_decimal( $row['minimum'] ) : 0; $maximum = isset( $row['maximum'] ) && '' !== (string) $row['maximum'] ? (float) wc_format_decimal( $row['maximum'] ) : 0; $timeout = isset( $row['timeout'] ) ? absint( $row['timeout'] ) : 0; if ( $timeout > 0 && $minimum >= 0 && ( 0 === $maximum || $maximum >= $minimum ) ) { $prepared[] = [ 'minimum' => $minimum, 'maximum' => $maximum, 'timeout' => $timeout ]; } } return $prepared; 585 } 586 487 587 private function prepare_string_list_rows( $rows, $field, $sanitizer ) { 488 588 $prepared = []; if ( ! is_array( $rows ) ) { return $prepared; } foreach ( $rows as $row ) { $item = is_array( $row ) && isset( $row[ $field ] ) ? call_user_func( $sanitizer, $row[ $field ] ) : ''; if ( $item ) { $prepared[] = $item; } } return array_values( array_unique( $prepared ) ); … … 496 596 return [ 497 597 [ 'label' => __( 'Default Timeout', 'cartflush' ), 'value' => (int) get_option( 'cartflush_expiration_time', 30 ), 'meta' => __( 'minutes', 'cartflush' ) ], 498 [ 'label' => __( 'Timeout Rules', 'cartflush' ), 'value' => count( $rules['customer_type_rules'] ) + count( $rules['role_rules'] ) + count( $rules[' product_rules'] ) + count( $rules['category_rules'] ) + count( $rules['tag_rules'] ), 'meta' => __( 'active', 'cartflush' ) ],598 [ 'label' => __( 'Timeout Rules', 'cartflush' ), 'value' => count( $rules['customer_type_rules'] ) + count( $rules['role_rules'] ) + count( $rules['cart_value_rules'] ) + count( $rules['product_rules'] ) + count( $rules['category_rules'] ) + count( $rules['tag_rules'] ), 'meta' => __( 'active', 'cartflush' ) ], 499 599 [ 'label' => __( 'Exclusions', 'cartflush' ), 'value' => count( $rules['excluded_roles'] ) + count( $rules['excluded_products'] ) + count( $rules['excluded_categories'] ) + count( $rules['excluded_tags'] ), 'meta' => __( 'guards', 'cartflush' ) ], 500 [ 'label' => __( 'Import Modes', 'cartflush' ), 'value' => 'CSV + JSON', 'meta' => __( ' ready', 'cartflush' ) ],600 [ 'label' => __( 'Import Modes', 'cartflush' ), 'value' => 'CSV + JSON', 'meta' => __( 'plus sample', 'cartflush' ) ], 501 601 ]; 502 602 } … … 506 606 [ 'label' => __( 'Customer Type Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['customer_type_rules'] ) ], 507 607 [ 'label' => __( 'Role Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['role_rules'] ) ], 608 [ 'label' => __( 'Cart Value Rules', 'cartflush' ), 'value' => $this->format_cart_value_list( $rules['cart_value_rules'] ) ], 508 609 [ 'label' => __( 'Product Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['product_rules'] ) ], 509 610 [ 'label' => __( 'Category Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['category_rules'] ) ], … … 530 631 } 531 632 633 private function format_cart_value_list( $items ) { 634 if ( empty( $items ) ) { return __( 'None saved yet.', 'cartflush' ); } 635 $formatted = []; 636 foreach ( $items as $item ) { 637 $minimum = isset( $item['minimum'] ) ? (float) $item['minimum'] : 0; 638 $maximum = isset( $item['maximum'] ) ? (float) $item['maximum'] : 0; 639 $timeout = isset( $item['timeout'] ) ? absint( $item['timeout'] ) : 0; 640 $range = $maximum > 0 ? $minimum . '-' . $maximum : $minimum . '+'; 641 $formatted[] = sprintf( __( '%1$s: %2$d min', 'cartflush' ), $range, $timeout ); 642 } 643 return implode( ', ', $formatted ); 644 } 645 532 646 private function format_simple_list( $items ) { 533 647 return empty( $items ) ? __( 'None saved yet.', 'cartflush' ) : implode( ', ', array_map( 'strval', $items ) ); 648 } 649 650 private function sanitize_yes_no_option( $value ) { 651 return 'yes' === $value ? 'yes' : 'no'; 652 } 653 654 private function store_duplicate_rule_warnings( $value ) { 655 $warnings = []; 656 657 $this->collect_duplicate_timeout_warnings( $warnings, isset( $value['customer_type_rules'] ) ? $value['customer_type_rules'] : [], 'type', __( 'Customer type rules', 'cartflush' ) ); 658 $this->collect_duplicate_timeout_warnings( $warnings, isset( $value['role_rules'] ) ? $value['role_rules'] : [], 'role', __( 'Role rules', 'cartflush' ) ); 659 $this->collect_duplicate_cart_value_warnings( $warnings, isset( $value['cart_value_rules'] ) ? $value['cart_value_rules'] : [] ); 660 $this->collect_duplicate_timeout_warnings( $warnings, isset( $value['product_rules'] ) ? $value['product_rules'] : [], 'product_id', __( 'Product rules', 'cartflush' ) ); 661 $this->collect_duplicate_timeout_warnings( $warnings, isset( $value['category_rules'] ) ? $value['category_rules'] : [], 'slug', __( 'Category rules', 'cartflush' ) ); 662 $this->collect_duplicate_timeout_warnings( $warnings, isset( $value['tag_rules'] ) ? $value['tag_rules'] : [], 'slug', __( 'Tag rules', 'cartflush' ) ); 663 $this->collect_duplicate_simple_warnings( $warnings, isset( $value['excluded_roles'] ) ? $value['excluded_roles'] : [], 'role', __( 'Excluded roles', 'cartflush' ) ); 664 $this->collect_duplicate_simple_warnings( $warnings, isset( $value['excluded_products'] ) ? $value['excluded_products'] : [], 'product_id', __( 'Excluded products', 'cartflush' ) ); 665 $this->collect_duplicate_simple_warnings( $warnings, isset( $value['excluded_categories'] ) ? $value['excluded_categories'] : [], 'slug', __( 'Excluded categories', 'cartflush' ) ); 666 $this->collect_duplicate_simple_warnings( $warnings, isset( $value['excluded_tags'] ) ? $value['excluded_tags'] : [], 'slug', __( 'Excluded tags', 'cartflush' ) ); 667 668 if ( ! empty( $warnings ) ) { 669 set_transient( 'cartflush_admin_duplicate_warnings_' . get_current_user_id(), array_values( array_unique( $warnings ) ), MINUTE_IN_SECONDS ); 670 return; 671 } 672 673 delete_transient( 'cartflush_admin_duplicate_warnings_' . get_current_user_id() ); 674 } 675 676 private function collect_duplicate_timeout_warnings( &$warnings, $rows, $field, $label ) { 677 if ( ! is_array( $rows ) ) { 678 return; 679 } 680 681 $seen = []; 682 683 foreach ( $rows as $row ) { 684 if ( ! is_array( $row ) || empty( $row[ $field ] ) ) { 685 continue; 686 } 687 688 $key = sanitize_text_field( (string) $row[ $field ] ); 689 690 if ( isset( $seen[ $key ] ) ) { 691 $warnings[] = sprintf( __( '%1$s contains a duplicate entry for %2$s.', 'cartflush' ), $label, $key ); 692 } 693 694 $seen[ $key ] = true; 695 } 696 } 697 698 private function collect_duplicate_simple_warnings( &$warnings, $rows, $field, $label ) { 699 $this->collect_duplicate_timeout_warnings( $warnings, $rows, $field, $label ); 700 } 701 702 private function collect_duplicate_cart_value_warnings( &$warnings, $rows ) { 703 if ( ! is_array( $rows ) ) { 704 return; 705 } 706 707 $seen = []; 708 709 foreach ( $rows as $row ) { 710 if ( ! is_array( $row ) || ! isset( $row['minimum'] ) ) { 711 continue; 712 } 713 714 $key = (string) $row['minimum'] . '|' . ( isset( $row['maximum'] ) ? (string) $row['maximum'] : '' ); 715 716 if ( isset( $seen[ $key ] ) ) { 717 $warnings[] = sprintf( __( 'Cart value rules contains a duplicate range for %s.', 'cartflush' ), str_replace( '|', ' - ', $key ) ); 718 } 719 720 $seen[ $key ] = true; 721 } 722 } 723 724 private function parse_cart_value_key( $key ) { 725 $key = trim( (string) $key ); 726 727 if ( preg_match( '/^([0-9]+(?:\.[0-9]+)?)\+$/', $key, $matches ) ) { 728 return [ 729 'minimum' => (float) $matches[1], 730 'maximum' => 0, 731 ]; 732 } 733 734 if ( preg_match( '/^([0-9]+(?:\.[0-9]+)?)\-([0-9]+(?:\.[0-9]+)?)$/', $key, $matches ) ) { 735 $minimum = (float) $matches[1]; 736 $maximum = (float) $matches[2]; 737 738 if ( $maximum < $minimum ) { 739 return false; 740 } 741 742 return [ 743 'minimum' => $minimum, 744 'maximum' => $maximum, 745 ]; 746 } 747 748 return false; 534 749 } 535 750 -
cartflush-autoclear-cart-for-inactive-users/trunk/includes/class-cartflush-plugin.php
r3484352 r3489862 31 31 add_action( 'plugins_loaded', [ $this, 'load_textdomain' ] ); 32 32 add_action( 'init', [ $this, 'maybe_clear_cart' ] ); 33 add_action( 'wp', [ $this, 'maybe_add_warning_notice' ] ); 33 34 add_action( 'woocommerce_before_cart', [ $this, 'store_last_activity_time' ] ); 34 35 add_action( 'woocommerce_before_checkout_form', [ $this, 'store_last_activity_time' ] ); … … 37 38 if ( is_admin() ) { 38 39 $this->admin = new CartFlush_Admin( $this->rules ); 40 add_action( 'woocommerce_product_options_general_product_data', [ $this, 'render_product_timeout_field' ] ); 41 add_action( 'woocommerce_process_product_meta', [ $this, 'save_product_timeout_field' ] ); 39 42 } 40 43 } … … 95 98 } 96 99 } 100 101 /** 102 * Show a warning before the cart is about to be cleared. 103 * 104 * @return void 105 */ 106 public function maybe_add_warning_notice() { 107 if ( is_admin() || ! function_exists( 'WC' ) || ! WC()->session || ! WC()->cart ) { 108 return; 109 } 110 111 if ( ! is_cart() && ! is_checkout() ) { 112 return; 113 } 114 115 if ( 'yes' !== get_option( 'cartflush_enable_warning_notice', 'no' ) ) { 116 return; 117 } 118 119 if ( $this->rules->cart_has_excluded_items() ) { 120 return; 121 } 122 123 $last_activity = absint( WC()->session->get( 'cart_last_activity' ) ); 124 $expire_minutes = absint( $this->rules->get_cart_expiration_minutes() ); 125 $warning_threshold = absint( get_option( 'cartflush_warning_notice_minutes', 5 ) ); 126 127 if ( ! $last_activity || $expire_minutes <= 0 || $warning_threshold <= 0 ) { 128 return; 129 } 130 131 $remaining_seconds = ( $expire_minutes * 60 ) - ( time() - $last_activity ); 132 133 if ( $remaining_seconds <= 0 || $remaining_seconds > ( $warning_threshold * 60 ) ) { 134 return; 135 } 136 137 if ( wc_has_notice( __( 'Your cart will expire soon due to inactivity. Continue shopping or update your cart to keep it active.', 'cartflush' ), 'notice' ) ) { 138 return; 139 } 140 141 wc_add_notice( __( 'Your cart will expire soon due to inactivity. Continue shopping or update your cart to keep it active.', 'cartflush' ), 'notice' ); 142 } 143 144 /** 145 * Render product-level timeout override field. 146 * 147 * @return void 148 */ 149 public function render_product_timeout_field() { 150 echo '<div class="options_group">'; 151 woocommerce_wp_text_input( 152 [ 153 'id' => CartFlush_Rules::PRODUCT_TIMEOUT_META_KEY, 154 'label' => __( 'CartFlush timeout', 'cartflush' ), 155 'description' => __( 'Optional timeout in minutes for this product. Leave empty to use global CartFlush rules.', 'cartflush' ), 156 'desc_tip' => true, 157 'type' => 'number', 158 'custom_attributes' => [ 159 'min' => '1', 160 'step' => '1', 161 ], 162 'value' => get_post_meta( get_the_ID(), CartFlush_Rules::PRODUCT_TIMEOUT_META_KEY, true ), 163 ] 164 ); 165 echo '</div>'; 166 } 167 168 /** 169 * Save product-level timeout override. 170 * 171 * @param int $post_id Product ID. 172 * @return void 173 */ 174 public function save_product_timeout_field( $post_id ) { 175 $value = isset( $_POST[ CartFlush_Rules::PRODUCT_TIMEOUT_META_KEY ] ) ? absint( wp_unslash( $_POST[ CartFlush_Rules::PRODUCT_TIMEOUT_META_KEY ] ) ) : 0; 176 177 if ( $value > 0 ) { 178 update_post_meta( $post_id, CartFlush_Rules::PRODUCT_TIMEOUT_META_KEY, $value ); 179 return; 180 } 181 182 delete_post_meta( $post_id, CartFlush_Rules::PRODUCT_TIMEOUT_META_KEY ); 183 } 97 184 } -
cartflush-autoclear-cart-for-inactive-users/trunk/includes/class-cartflush-rules.php
r3488606 r3489862 13 13 14 14 const OPTION_NAME = 'cartflush_import_rules'; 15 const PRODUCT_TIMEOUT_META_KEY = '_cartflush_timeout_override'; 15 16 16 17 /** … … 37 38 'customer_type_rules' => $this->normalize_customer_type_rules( $rules['customer_type_rules'] ), 38 39 'role_rules' => $this->normalize_timeout_map( $rules['role_rules'], 'sanitize_key' ), 40 'cart_value_rules' => $this->normalize_cart_value_rules( $rules['cart_value_rules'] ), 39 41 'category_rules' => $this->normalize_timeout_map( $rules['category_rules'], 'sanitize_title' ), 40 42 'tag_rules' => $this->normalize_timeout_map( $rules['tag_rules'], 'sanitize_title' ), … … 62 64 $cart_items = WC()->cart->get_cart(); 63 65 $is_guest = ! ( $user instanceof WP_User ) || 0 === (int) $user->ID; 66 $cart_total = $this->get_cart_subtotal(); 64 67 65 68 if ( $is_guest && isset( $rules['customer_type_rules']['guest'] ) ) { … … 81 84 } 82 85 86 foreach ( $rules['cart_value_rules'] as $rule ) { 87 $minimum = isset( $rule['minimum'] ) ? (float) $rule['minimum'] : 0; 88 $maximum = isset( $rule['maximum'] ) ? (float) $rule['maximum'] : 0; 89 $timeout = isset( $rule['timeout'] ) ? absint( $rule['timeout'] ) : 0; 90 91 if ( $timeout <= 0 || $cart_total < $minimum ) { 92 continue; 93 } 94 95 if ( $maximum > 0 && $cart_total > $maximum ) { 96 continue; 97 } 98 99 $timeouts[] = $timeout; 100 } 101 83 102 foreach ( $cart_items as $cart_item ) { 84 103 $product_id = isset( $cart_item['product_id'] ) ? absint( $cart_item['product_id'] ) : 0; … … 86 105 if ( ! $product_id ) { 87 106 continue; 107 } 108 109 $product_timeout = $this->get_product_timeout_override( $product_id ); 110 111 if ( $product_timeout > 0 ) { 112 $timeouts[] = $product_timeout; 88 113 } 89 114 … … 168 193 'customer_type_rules' => [], 169 194 'role_rules' => [], 195 'cart_value_rules' => [], 170 196 'category_rules' => [], 171 197 'tag_rules' => [], … … 205 231 206 232 /** 233 * Normalize cart value rules. 234 * 235 * @param mixed $rules Cart value rules. 236 * @return array<int, array<string, float|int>> 237 */ 238 private function normalize_cart_value_rules( $rules ) { 239 $normalized = []; 240 241 if ( ! is_array( $rules ) ) { 242 return $normalized; 243 } 244 245 foreach ( $rules as $rule ) { 246 if ( ! is_array( $rule ) ) { 247 continue; 248 } 249 250 $minimum = isset( $rule['minimum'] ) ? wc_format_decimal( $rule['minimum'] ) : 0; 251 $maximum = isset( $rule['maximum'] ) ? wc_format_decimal( $rule['maximum'] ) : 0; 252 $timeout = isset( $rule['timeout'] ) ? absint( $rule['timeout'] ) : 0; 253 254 $minimum = max( 0, (float) $minimum ); 255 $maximum = max( 0, (float) $maximum ); 256 257 if ( $timeout <= 0 ) { 258 continue; 259 } 260 261 if ( $maximum > 0 && $maximum < $minimum ) { 262 continue; 263 } 264 265 $normalized[] = [ 266 'minimum' => $minimum, 267 'maximum' => $maximum, 268 'timeout' => $timeout, 269 ]; 270 } 271 272 return $normalized; 273 } 274 275 /** 207 276 * Normalize a generic timeout map. 208 277 * … … 334 403 return array_values( array_unique( array_filter( $slugs ) ) ); 335 404 } 405 406 /** 407 * Get a product-level timeout override. 408 * 409 * @param int $product_id Product ID. 410 * @return int 411 */ 412 public function get_product_timeout_override( $product_id ) { 413 return absint( get_post_meta( absint( $product_id ), self::PRODUCT_TIMEOUT_META_KEY, true ) ); 414 } 415 416 /** 417 * Resolve the current cart subtotal for value rules. 418 * 419 * @return float 420 */ 421 private function get_cart_subtotal() { 422 if ( ! function_exists( 'WC' ) || ! WC()->cart ) { 423 return 0; 424 } 425 426 $subtotal = WC()->cart->get_subtotal(); 427 428 if ( '' === $subtotal || null === $subtotal ) { 429 return 0; 430 } 431 432 return (float) wc_format_decimal( $subtotal ); 433 } 336 434 } -
cartflush-autoclear-cart-for-inactive-users/trunk/readme.txt
r3488606 r3489862 6 6 Tested up to: 6.8 7 7 Requires PHP: 7.4 8 Stable tag: 2. 1.08 Stable tag: 2.2.0 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 29 29 * Improve WooCommerce session performance 30 30 * Apply smarter rules based on customers and products 31 * Protect higher-value carts with subtotal-based timeout rules 31 32 * Manage rules visually from the WooCommerce settings page 32 33 * Import or export rules for fast setup and migration … … 68 69 69 70 This makes it easy to create shorter or longer expiration windows for special items, campaigns, or collections. 71 72 === Cart Value Rules === 73 74 Create timeout rules based on cart subtotal ranges. 75 76 Examples: 77 78 * 0-49.99 - 20 minutes 79 * 50-199.99 - 45 minutes 80 * 200+ - 120 minutes 81 82 This is especially useful for giving high-value carts more time before they are cleared. 83 84 === Per-Product Timeout Override === 85 86 Add a CartFlush timeout directly on the WooCommerce product edit screen for products that need a custom timeout without relying on a global rule. 87 88 === Pre-Clear Warning Notice === 89 90 Optionally show a notice on the cart and checkout pages shortly before CartFlush clears the cart due to inactivity. 70 91 71 92 === Smart Timeout Logic === … … 90 111 * customer_type 91 112 * role 113 * cart_value 92 114 * product_rule 93 115 * category … … 98 120 * excluded_tag 99 121 122 CartFlush also includes a downloadable sample CSV from the settings page to help merchants get started faster. 123 100 124 === JSON Import and Export === 101 125 … … 138 162 * role 139 163 * product_rule 164 * cart_value 140 165 * category 141 166 * tag … … 149 174 `customer_type,guest,20` 150 175 `role,customer,30` 176 `cart_value,100+,90` 151 177 `product_rule,321,10` 152 178 `category,flash-sale,15` … … 191 217 192 218 == Changelog == 219 220 = 2.2.0 = 221 222 * Added cart value timeout rules 223 * Added product-level timeout overrides in the product editor 224 * Added optional pre-clear cart warning notices 225 * Expanded CSV import with cart value ranges and added downloadable sample CSV templates 226 * Added duplicate rule detection warnings in admin 193 227 194 228 = 2.1.0 =
Note: See TracChangeset
for help on using the changeset viewer.