Plugin Directory

Changeset 3489862


Ignore:
Timestamp:
03/24/2026 10:29:45 AM (4 days ago)
Author:
wprashed
Message:

2.2.0

  • Added cart value timeout rules
  • Added product-level timeout overrides in the product editor
  • Added optional pre-clear cart warning notices
  • Expanded CSV import with cart value ranges and added downloadable sample CSV templates
  • Added duplicate rule detection warnings in admin
Location:
cartflush-autoclear-cart-for-inactive-users/trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • cartflush-autoclear-cart-for-inactive-users/trunk/assets/css/admin.css

    r3488606 r3489862  
    256256}
    257257
     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
    258274.cartflush-rule-grid,
    259275.cartflush-summary {
     
    506522}
    507523
     524.cartflush-tool-actions {
     525    display: flex;
     526    flex-wrap: wrap;
     527    gap: 10px;
     528}
     529
    508530.cartflush-upload-field {
    509531    display: grid;
     
    606628    }
    607629
     630    .cartflush-tool-actions {
     631        flex-direction: column;
     632    }
     633
    608634    .cartflush-rule-table__action,
    609635    .cartflush-row-action {
  • cartflush-autoclear-cart-for-inactive-users/trunk/cartflush-autoclear-cart-for-inactive-users.php

    r3488606 r3489862  
    44 * Plugin URI: https://wordpress.org/plugins/cartflush-autoclear-cart-for-inactive-users/
    55 * Description: Automatically clears WooCommerce carts after inactivity with configurable default timeouts, import/export tools, and rule-based exclusions.
    6  * Version: 2.1.0
     6 * Version: 2.2.0
    77 * Requires at least: 5.8
    88 * Requires PHP: 7.4
     
    2121}
    2222
    23 define( 'CARTFLUSH_VERSION', '2.1.0' );
     23define( 'CARTFLUSH_VERSION', '2.2.0' );
    2424define( 'CARTFLUSH_FILE', __FILE__ );
    2525define( 'CARTFLUSH_PATH', plugin_dir_path( __FILE__ ) );
  • cartflush-autoclear-cart-for-inactive-users/trunk/includes/admin/class-cartflush-admin.php

    r3488606 r3489862  
    2828        add_action( 'admin_post_cartflush_import_csv', [ $this, 'handle_csv_import' ] );
    2929        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' ] );
    3031        add_filter( 'option_page_capability_cartflush_settings_group', [ $this, 'settings_capability' ] );
    3132    }
     
    3738    public function register_settings() {
    3839        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 ] );
    3942        register_setting( 'cartflush_settings_group', CartFlush_Rules::OPTION_NAME, [ 'type' => 'array', 'sanitize_callback' => [ $this, 'sanitize_rules_option' ], 'default' => $this->rules->get_default_rules() ] );
    4043        add_settings_section( 'cartflush_main', __( 'Timeout Settings', 'cartflush' ), [ $this, 'render_main_section' ], 'cartflush-settings' );
    4144        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' );
    4247    }
    4348
    4449    public function sanitize_rules_option( $value ) {
     50        $this->store_duplicate_rule_warnings( is_array( $value ) ? $value : [] );
    4551        return $this->rules->normalize_rules_data( is_array( $value ) ? $this->prepare_rules_for_storage( $value ) : $value );
    4652    }
     
    6975    }
    7076
     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
    7195    public function render_admin_notices() {
    7296        if ( ! isset( $_GET['page'] ) || 'cartflush-settings' !== sanitize_key( wp_unslash( $_GET['page'] ) ) ) {
     
    81105        if ( ! empty( $_GET['cartflush_error'] ) ) {
    82106            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>';
    83116        }
    84117    }
     
    97130        $default_timeout = isset( $data['cartflush_expiration_time'] ) ? absint( $data['cartflush_expiration_time'] ) : get_option( 'cartflush_expiration_time', 30 );
    98131        $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 );
    99134        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 ) );
    100137        update_option( CartFlush_Rules::OPTION_NAME, $this->rules->normalize_rules_data( $rules ) );
    101138        $this->redirect_with_message( __( 'JSON settings imported successfully.', 'cartflush' ) );
     
    141178                    }
    142179                    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;
    143190                case 'product_rule':
    144191                    if ( absint( $key ) > 0 && $timeout > 0 ) {
     
    178225    }
    179226
     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
    180255    public function handle_json_export() {
    181256        $this->assert_admin_permissions();
    182257        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() ];
    184259        nocache_headers();
    185260        header( 'Content-Type: application/json; charset=utf-8' );
     
    220295                            <ul class="cartflush-bullet-list">
    221296                                <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>
    223298                                <li><?php esc_html_e( 'Role, product, category, and tag exclusions', 'cartflush' ); ?></li>
    224299                            </ul>
     
    253328                                    $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 ); } );
    254329                                    $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'] ); } );
    255331                                    $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'] ); } );
    256332                                    $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' ) ); } );
     
    296372                            </div>
    297373
    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>
    304380                                <div class="cartflush-code-list">
    305381                                    <code>customer_type</code>
    306382                                    <code>role</code>
     383                                    <code>cart_value</code>
    307384                                    <code>product_rule</code>
    308385                                    <code>category</code>
     
    316393                                        <input type="file" name="cartflush_csv_file" accept=".csv,text/csv" required>
    317394                                    </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>
    319399                                </form>
    320400                            </div>
     
    402482        <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>
    403483        <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>
    404485        <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>
    405486        <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>
     
    427508    }
    428509
     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
    429520    private function render_simple_select_rows( $group, $field, $items, $options, $placeholder ) {
    430521        if ( empty( $items ) ) { echo $this->get_simple_select_row( $group, 0, $field, '', $options, $placeholder ); return; } // phpcs:ignore
     
    443534    private function get_number_timeout_row( $group, $index, $value, $timeout ) {
    444535        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();
    445540    }
    446541
     
    467562            'customer_type_rules' => $this->prepare_timeout_rows( isset( $value['customer_type_rules'] ) ? $value['customer_type_rules'] : [], 'type', 'sanitize_key' ),
    468563            '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'] : [] ),
    469565            'product_rules'       => $this->prepare_integer_timeout_rows( isset( $value['product_rules'] ) ? $value['product_rules'] : [], 'product_id' ),
    470566            'category_rules'      => $this->prepare_timeout_rows( isset( $value['category_rules'] ) ? $value['category_rules'] : [], 'slug', 'sanitize_title' ),
     
    485581    }
    486582
     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
    487587    private function prepare_string_list_rows( $rows, $field, $sanitizer ) {
    488588        $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 ) );
     
    496596        return [
    497597            [ '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' ) ],
    499599            [ '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' ) ],
    501601        ];
    502602    }
     
    506606            [ 'label' => __( 'Customer Type Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['customer_type_rules'] ) ],
    507607            [ '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'] ) ],
    508609            [ 'label' => __( 'Product Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['product_rules'] ) ],
    509610            [ 'label' => __( 'Category Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['category_rules'] ) ],
     
    530631    }
    531632
     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
    532646    private function format_simple_list( $items ) {
    533647        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;
    534749    }
    535750
  • cartflush-autoclear-cart-for-inactive-users/trunk/includes/class-cartflush-plugin.php

    r3484352 r3489862  
    3131        add_action( 'plugins_loaded', [ $this, 'load_textdomain' ] );
    3232        add_action( 'init', [ $this, 'maybe_clear_cart' ] );
     33        add_action( 'wp', [ $this, 'maybe_add_warning_notice' ] );
    3334        add_action( 'woocommerce_before_cart', [ $this, 'store_last_activity_time' ] );
    3435        add_action( 'woocommerce_before_checkout_form', [ $this, 'store_last_activity_time' ] );
     
    3738        if ( is_admin() ) {
    3839            $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' ] );
    3942        }
    4043    }
     
    9598        }
    9699    }
     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    }
    97184}
  • cartflush-autoclear-cart-for-inactive-users/trunk/includes/class-cartflush-rules.php

    r3488606 r3489862  
    1313
    1414    const OPTION_NAME = 'cartflush_import_rules';
     15    const PRODUCT_TIMEOUT_META_KEY = '_cartflush_timeout_override';
    1516
    1617    /**
     
    3738            'customer_type_rules' => $this->normalize_customer_type_rules( $rules['customer_type_rules'] ),
    3839            'role_rules'          => $this->normalize_timeout_map( $rules['role_rules'], 'sanitize_key' ),
     40            'cart_value_rules'    => $this->normalize_cart_value_rules( $rules['cart_value_rules'] ),
    3941            'category_rules'      => $this->normalize_timeout_map( $rules['category_rules'], 'sanitize_title' ),
    4042            'tag_rules'           => $this->normalize_timeout_map( $rules['tag_rules'], 'sanitize_title' ),
     
    6264        $cart_items = WC()->cart->get_cart();
    6365        $is_guest   = ! ( $user instanceof WP_User ) || 0 === (int) $user->ID;
     66        $cart_total = $this->get_cart_subtotal();
    6467
    6568        if ( $is_guest && isset( $rules['customer_type_rules']['guest'] ) ) {
     
    8184        }
    8285
     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
    83102        foreach ( $cart_items as $cart_item ) {
    84103            $product_id = isset( $cart_item['product_id'] ) ? absint( $cart_item['product_id'] ) : 0;
     
    86105            if ( ! $product_id ) {
    87106                continue;
     107            }
     108
     109            $product_timeout = $this->get_product_timeout_override( $product_id );
     110
     111            if ( $product_timeout > 0 ) {
     112                $timeouts[] = $product_timeout;
    88113            }
    89114
     
    168193            'customer_type_rules' => [],
    169194            'role_rules'          => [],
     195            'cart_value_rules'    => [],
    170196            'category_rules'      => [],
    171197            'tag_rules'           => [],
     
    205231
    206232    /**
     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    /**
    207276     * Normalize a generic timeout map.
    208277     *
     
    334403        return array_values( array_unique( array_filter( $slugs ) ) );
    335404    }
     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    }
    336434}
  • cartflush-autoclear-cart-for-inactive-users/trunk/readme.txt

    r3488606 r3489862  
    66Tested up to: 6.8
    77Requires PHP: 7.4
    8 Stable tag: 2.1.0
     8Stable tag: 2.2.0
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    2929* Improve WooCommerce session performance
    3030* Apply smarter rules based on customers and products
     31* Protect higher-value carts with subtotal-based timeout rules
    3132* Manage rules visually from the WooCommerce settings page
    3233* Import or export rules for fast setup and migration
     
    6869
    6970This makes it easy to create shorter or longer expiration windows for special items, campaigns, or collections.
     71
     72=== Cart Value Rules ===
     73
     74Create timeout rules based on cart subtotal ranges.
     75
     76Examples:
     77
     78* 0-49.99 - 20 minutes
     79* 50-199.99 - 45 minutes
     80* 200+ - 120 minutes
     81
     82This is especially useful for giving high-value carts more time before they are cleared.
     83
     84=== Per-Product Timeout Override ===
     85
     86Add 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
     90Optionally show a notice on the cart and checkout pages shortly before CartFlush clears the cart due to inactivity.
    7091
    7192=== Smart Timeout Logic ===
     
    90111* customer_type
    91112* role
     113* cart_value
    92114* product_rule
    93115* category
     
    98120* excluded_tag
    99121
     122CartFlush also includes a downloadable sample CSV from the settings page to help merchants get started faster.
     123
    100124=== JSON Import and Export ===
    101125
     
    138162* role
    139163* product_rule
     164* cart_value
    140165* category
    141166* tag
     
    149174`customer_type,guest,20`
    150175`role,customer,30`
     176`cart_value,100+,90`
    151177`product_rule,321,10`
    152178`category,flash-sale,15`
     
    191217
    192218== 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
    193227
    194228= 2.1.0 =
Note: See TracChangeset for help on using the changeset viewer.