Plugin Directory

Changeset 3734660


Ignore:
Timestamp:
10/08/2026 01:12:03 PM (2 weeks ago)
Author:
webtendhq
Message:

updated v1.0.1

Location:
cartexa-smart-cart-for-woocommerce/trunk
Files:
1 added
11 edited

Legend:

Unmodified
Added
Removed
  • cartexa-smart-cart-for-woocommerce/trunk/admin/class-cartexa-admin-panel.php

    r3690135 r3734660  
    6767                                'group' => __( 'Conversion', 'cartexa-smart-cart-for-woocommerce' ),
    6868                                'items' => array(
    69                                         'shipping-bar' => array( 'label' => __( 'Free Shipping Bar',          'cartexa-smart-cart-for-woocommerce' ), 'icon' => 'shipping' ),
    70                                         'trust'        => array( 'label' => __( 'Trust Badges',               'cartexa-smart-cart-for-woocommerce' ), 'icon' => 'trust' ),
    71                                         'scarcity'     => array( 'label' => __( 'Stock Scarcity',             'cartexa-smart-cart-for-woocommerce' ), 'icon' => 'scarcity' ),
     69                                        'shipping-bar'   => array( 'label' => __( 'Shipping Bar',   'cartexa-smart-cart-for-woocommerce' ), 'icon' => 'shipping' ),
     70                                        'coupon-box'     => array( 'label' => __( 'Coupon Box',     'cartexa-smart-cart-for-woocommerce' ), 'icon' => 'coupon' ),
     71                                        'save-for-later' => array( 'label' => __( 'Save for Later', 'cartexa-smart-cart-for-woocommerce' ), 'icon' => 'save' ),
     72                                        'trust'          => array( 'label' => __( 'Trust Badges',   'cartexa-smart-cart-for-woocommerce' ), 'icon' => 'trust' ),
     73                                        'scarcity'       => array( 'label' => __( 'Stock Scarcity', 'cartexa-smart-cart-for-woocommerce' ), 'icon' => 'scarcity' ),
    7274                                ),
    7375                        ),
    … …  
    274276                        'floating'        => $this->section_floating_cart(),
    275277                        'shipping-bar'    => $this->section_shipping_bar(),
     278                        'coupon-box'      => $this->section_coupon_box(),
     279                        'save-for-later'  => $this->section_save_for_later(),
    276280                        'trust'           => $this->section_trust_badges(),
    277281                        'scarcity'        => $this->section_scarcity(),
    … …  
    291295                        'side-cart'       => __( 'Side Cart',                  'cartexa-smart-cart-for-woocommerce' ),
    292296                        'floating'        => __( 'Floating Cart',              'cartexa-smart-cart-for-woocommerce' ),
    293                         'shipping-bar'    => __( 'Free Shipping Bar',          'cartexa-smart-cart-for-woocommerce' ),
    294                         'trust'           => __( 'Trust Badges',               'cartexa-smart-cart-for-woocommerce' ),
     297                        'shipping-bar'    => __( 'Free Shipping Bar',  'cartexa-smart-cart-for-woocommerce' ),
     298                        'coupon-box'      => __( 'Coupon Box',         'cartexa-smart-cart-for-woocommerce' ),
     299                        'save-for-later'  => __( 'Save for Later',     'cartexa-smart-cart-for-woocommerce' ),
     300                        'trust'           => __( 'Trust Badges',       'cartexa-smart-cart-for-woocommerce' ),
    295301                        'scarcity'        => __( 'Stock Scarcity Messages',    'cartexa-smart-cart-for-woocommerce' ),
    296302                        'display'         => __( 'Display Settings',           'cartexa-smart-cart-for-woocommerce' ),
    … …  
    310316                        'floating'        => __( 'A persistent floating button that opens the cart.', 'cartexa-smart-cart-for-woocommerce' ),
    311317                        'shipping-bar'    => __( 'Show progress toward free shipping inside the drawer.', 'cartexa-smart-cart-for-woocommerce' ),
     318                        'coupon-box'      => __( 'Let customers apply coupon codes inside the drawer.', 'cartexa-smart-cart-for-woocommerce' ),
     319                        'save-for-later'  => __( 'Allow shoppers to save items and restore them anytime.', 'cartexa-smart-cart-for-woocommerce' ),
    312320                        'trust'           => __( 'Reassurance badges to reduce checkout hesitation.', 'cartexa-smart-cart-for-woocommerce' ),
    313321                        'scarcity'        => __( 'Show low-stock warnings to create urgency.', 'cartexa-smart-cart-for-woocommerce' ),
    … …  
    408416                                $this->row_text(   'shipping_bar_msg_reached',  __( 'Message after threshold',  'cartexa-smart-cart-for-woocommerce' ), __( '🎉 You\'ve unlocked free shipping!',    'cartexa-smart-cart-for-woocommerce' ) );
    409417                                $this->row_color(  'shipping_bar_color',        __( 'Bar fill color',            'cartexa-smart-cart-for-woocommerce' ), '#10b981' );
     418                                ?>
     419                        </div>
     420                </div>
     421                <?php
     422        }
     423
     424        // ─── Section: Coupon Box (v1.0.1) ──────────────────────────────────
     425
     426        private function section_coupon_box(): void {
     427                ?>
     428                <div class="cartly-section">
     429                        <div class="cartly-card">
     430                                <div class="cartly-card__head">
     431                                        <h2><?php esc_html_e( 'Coupon Box', 'cartexa-smart-cart-for-woocommerce' ); ?></h2>
     432                                </div>
     433                                <?php
     434                                $this->row_toggle( 'enable_coupon_box',      __( 'Enable coupon box',  'cartexa-smart-cart-for-woocommerce' ) );
     435                                $this->row_text(   'coupon_box_placeholder', __( 'Input placeholder',  'cartexa-smart-cart-for-woocommerce' ), __( 'Coupon code', 'cartexa-smart-cart-for-woocommerce' ) );
     436                                $this->row_text(   'coupon_box_btn_text',    __( 'Button label',       'cartexa-smart-cart-for-woocommerce' ), __( 'Apply',       'cartexa-smart-cart-for-woocommerce' ) );
     437                                ?>
     438                        </div>
     439                </div>
     440                <?php
     441        }
     442
     443        // ─── Section: Save for Later (v1.0.1) ───────────────────────────────
     444
     445        private function section_save_for_later(): void {
     446                ?>
     447                <div class="cartly-section">
     448                        <div class="cartly-card">
     449                                <div class="cartly-card__head">
     450                                        <h2><?php esc_html_e( 'Save for Later', 'cartexa-smart-cart-for-woocommerce' ); ?></h2>
     451                                </div>
     452                                <?php
     453                                $this->row_toggle( 'enable_save_for_later',   __( 'Enable Save for Later',    'cartexa-smart-cart-for-woocommerce' ) );
     454                                $this->row_text(   'save_for_later_btn_text', __( '"Save" button label',       'cartexa-smart-cart-for-woocommerce' ), __( 'Save for Later', 'cartexa-smart-cart-for-woocommerce' ) );
     455                                $this->row_text(   'restore_btn_text',        __( '"Move to Cart" label',      'cartexa-smart-cart-for-woocommerce' ), __( 'Move to Cart',   'cartexa-smart-cart-for-woocommerce' ) );
    410456                                ?>
    411457                        </div>
    … …  
    839885        }
    840886
    841                 // ─── Section: Export / Import ───────────────────────────────
     887        // ─── Section: Export / Import ─────────────────────────────────
    842888
    843889        private function section_export_import(): void {
    … …  
    907953                        </div>
    908954                </div>
    909 
    910955                <?php
    911956                // Export/Import JS logic is loaded via cartexa-admin.js
    … …  
    9691014        }
    9701015
    971                 private function get_nav_icon( string $name ): string {
     1016        private function get_nav_icon( string $name ): string {
    9721017                $icons = array(
    9731018                        'cart'     => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M16 10a4 4 0 01-8 0" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
    9741019                        'floating' => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.8"/><line x1="12" y1="8" x2="12" y2="16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><line x1="8" y1="12" x2="16" y2="12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
    9751020                        'bar'      => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><rect x="2" y="14" width="20" height="7" rx="1" stroke="currentColor" stroke-width="1.8"/><path d="M6 14V8a6 6 0 0112 0v6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
     1021                        'coupon'   => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><line x1="7" y1="7" x2="7.01" y2="7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
     1022                        'save'     => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
    9761023                        'shipping' => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><rect x="1" y="3" width="15" height="13" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="5.5" cy="18.5" r="2.5" stroke="currentColor" stroke-width="1.8"/><circle cx="18.5" cy="18.5" r="2.5" stroke="currentColor" stroke-width="1.8"/></svg>',
    9771024                        'trust'    => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
    … …  
    9791026                        'display'  => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.8"/><path d="M3 9h18M9 21V9" stroke="currentColor" stroke-width="1.8"/></svg>',
    9801027                        'icon'     => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.8"/><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/></svg>',
    981                 'nav'      => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><line x1="3" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
     1028                        'nav'      => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><line x1="3" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
    9821029                        'toast'    => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 01-3.46 0" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
    9831030                        'undo'     => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><polyline points="1 4 1 10 7 10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.51 15a9 9 0 102.13-9.36L1 10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
  • cartexa-smart-cart-for-woocommerce/trunk/cartexa-smart-cart.php

    r3690135 r3734660  
    22/**
    33 * Plugin Name:       Cartexa – Smart Cart for WooCommerce
    4  * Plugin URI:        https://webtend.net/cartexa
    5  * Description:       Modern AJAX side cart for WooCommerce — slide-in drawer, floating cart button, free shipping bar, trust badges, scarcity messages & more. Zero jQuery. Lightweight & conversion-focused.
    6  * Version:           1.0.0
     4 * Plugin URI:        https://wordpress.org/plugins/cartexa-smart-cart-for-woocommerce/
     5 * Description:       Modern AJAX side cart for WooCommerce — slide-in drawer, floating cart button, free shipping bar, trust badges, scarcity messages & more. No jQuery dependency. Lightweight & conversion-focused.
     6 * Version:           1.0.1
    77 * Author:            WebTend
    88 * Author URI:        https://webtend.net/
    … …  
    2727
    2828// ── Constants ──────────────────────────────────────────────────────────────
    29 define( 'CARTEXA_VERSION',  '1.0.0' );
     29define( 'CARTEXA_VERSION',  '1.0.1' );
    3030define( 'CARTEXA_FILE',     __FILE__ );
    3131define( 'CARTEXA_PATH',     plugin_dir_path( __FILE__ ) );
  • cartexa-smart-cart-for-woocommerce/trunk/frontend/css/cartexa-modules.css

    r3690135 r3734660  
    386386        .cartexa-snackbar--bottom-left  { left: 12px;  bottom: 72px; }
    387387}
     388
     389/* ============================================================
     390 * v1.0.1 — Save for Later
     391 * ============================================================ */
     392.cartexa-save-for-later-btn {
     393        display: inline-flex;
     394        align-items: center;
     395        gap: 4px;
     396        font-size: 11px;
     397        font-weight: 500;
     398        color: var(--cartexa-text-muted, #94a3b8);
     399        background: none;
     400        border: none;
     401        cursor: pointer;
     402        font-family: inherit;
     403        padding: 2px 0;
     404        transition: color 0.15s;
     405        line-height: 1;
     406}
     407.cartexa-save-for-later-btn:hover { color: var(--cartexa-accent, #7c3aed); }
     408.cartexa-save-for-later-btn:disabled { opacity: 0.5; cursor: not-allowed; }
     409
     410.cartexa-saved-items-wrap { margin: 0; }
     411
     412.cartexa-saved-items {
     413        border-top: 1px solid var(--cartexa-border, #f1f5f9);
     414        padding: 10px 0 2px;
     415}
     416
     417.cartexa-saved-items__head {
     418        display: flex;
     419        align-items: center;
     420        padding: 0 var(--cartexa-drawer-px, 20px) 8px;
     421}
     422
     423.cartexa-saved-items__title {
     424        font-size: 11px;
     425        font-weight: 700;
     426        color: var(--cartexa-text-muted, #64748b);
     427        text-transform: uppercase;
     428        letter-spacing: 0.07em;
     429        display: flex;
     430        align-items: center;
     431        gap: 6px;
     432}
     433
     434.cartexa-saved-items__count {
     435        background: var(--cartexa-border, #f1f5f9);
     436        color: var(--cartexa-text-muted, #64748b);
     437        border-radius: 20px;
     438        padding: 1px 7px;
     439        font-size: 10px;
     440        font-weight: 700;
     441}
     442
     443.cartexa-saved-items__list {
     444        list-style: none;
     445        padding: 0;
     446        margin: 0;
     447}
     448
     449.cartexa-saved-item {
     450        display: flex;
     451        gap: 12px;
     452        padding: 10px var(--cartexa-drawer-px, 20px);
     453        border-bottom: 1px solid var(--cartexa-border, #f8fafc);
     454        transition: background 0.12s;
     455}
     456.cartexa-saved-item:hover { background: var(--cartexa-bg-subtle, #fafafa); }
     457
     458.cartexa-saved-item__thumb {
     459        width: 50px;
     460        height: 50px;
     461        border-radius: 8px;
     462        overflow: hidden;
     463        flex-shrink: 0;
     464        background: var(--cartexa-border, #f8fafc);
     465}
     466.cartexa-saved-item__thumb img {
     467        width: 100%;
     468        height: 100%;
     469        object-fit: cover;
     470        display: block;
     471}
     472
     473.cartexa-saved-item__body {
     474        flex: 1;
     475        display: flex;
     476        flex-direction: column;
     477        gap: 2px;
     478        min-width: 0;
     479}
     480
     481.cartexa-saved-item__name {
     482        font-size: 13px;
     483        font-weight: 600;
     484        color: var(--cartexa-text, #1a1a2e);
     485        line-height: 1.3;
     486        white-space: nowrap;
     487        overflow: hidden;
     488        text-overflow: ellipsis;
     489}
     490
     491.cartexa-saved-item__attrs {
     492        font-size: 11px;
     493        color: var(--cartexa-text-muted, #94a3b8);
     494}
     495
     496.cartexa-saved-item__price {
     497        font-size: 13px;
     498        font-weight: 700;
     499        color: var(--cartexa-text, #1a1a2e);
     500}
     501
     502.cartexa-saved-item__actions {
     503        display: flex;
     504        align-items: center;
     505        gap: 10px;
     506        margin-top: 5px;
     507}
     508
     509.cartexa-saved-item__restore {
     510        font-size: 11px;
     511        font-weight: 700;
     512        color: var(--cartexa-accent, #7c3aed);
     513        background: var(--cartexa-accent-light, #f3eeff);
     514        border: none;
     515        border-radius: 5px;
     516        padding: 4px 10px;
     517        cursor: pointer;
     518        font-family: inherit;
     519        transition: background 0.15s, color 0.15s;
     520}
     521.cartexa-saved-item__restore:hover {
     522        background: var(--cartexa-accent, #7c3aed);
     523        color: #fff;
     524}
     525.cartexa-saved-item__restore:disabled { opacity: 0.5; cursor: not-allowed; }
     526
     527.cartexa-saved-item__remove {
     528        font-size: 11px;
     529        color: var(--cartexa-text-muted, #cbd5e1);
     530        background: none;
     531        border: none;
     532        cursor: pointer;
     533        font-family: inherit;
     534        padding: 0;
     535        transition: color 0.15s;
     536}
     537.cartexa-saved-item__remove:hover { color: var(--cartexa-danger, #ef4444); }
     538
     539/* ============================================================
     540 * v1.0.1 — Coupon Box
     541 * ============================================================ */
     542.cartexa-coupon-box {
     543        border: 1.5px solid var(--cartexa-border, #e2e8f0);
     544        border-radius: 10px;
     545        overflow: hidden;
     546        margin-bottom: 12px;
     547}
     548
     549.cartexa-coupon-box__toggle {
     550        width: 100%;
     551        display: flex;
     552        align-items: center;
     553        gap: 8px;
     554        padding: 10px 14px;
     555        background: transparent;
     556        border: none;
     557        cursor: pointer;
     558        font-family: inherit;
     559        font-size: 13px;
     560        font-weight: 600;
     561        color: var(--cartexa-accent, #7c3aed);
     562        text-align: left;
     563        transition: background 0.15s;
     564}
     565.cartexa-coupon-box__toggle:hover { background: var(--cartexa-accent-light, #f3eeff); }
     566
     567.cartexa-coupon-box__chevron {
     568        margin-left: auto;
     569        color: var(--cartexa-text-muted, #94a3b8);
     570        transition: transform 0.2s;
     571        flex-shrink: 0;
     572}
     573.cartexa-coupon-box__toggle[aria-expanded="true"] .cartexa-coupon-box__chevron {
     574        transform: rotate(180deg);
     575}
     576
     577.cartexa-coupon-box__body {
     578        padding: 10px 14px 14px;
     579        border-top: 1px solid var(--cartexa-border, #f1f5f9);
     580        background: var(--cartexa-accent-light, #faf5ff);
     581}
     582
     583.cartexa-coupon-box__row {
     584        display: flex;
     585        gap: 8px;
     586        margin-bottom: 8px;
     587}
     588
     589.cartexa-coupon-box__input {
     590        flex: 1;
     591        padding: 9px 12px;
     592        border: 1.5px solid var(--cartexa-accent, #7c3aed);
     593        border-radius: 8px;
     594        font-size: 13px;
     595        font-family: inherit;
     596        color: var(--cartexa-text, #1a1a2e);
     597        background: #fff;
     598        outline: none;
     599        min-width: 0;
     600        transition: box-shadow 0.15s;
     601}
     602.cartexa-coupon-box__input:focus { box-shadow: 0 0 0 3px rgba(124,58,237,.15); }
     603.cartexa-coupon-box__input::placeholder { color: #c4b5fd; }
     604
     605.cartexa-coupon-box__apply {
     606        padding: 9px 14px;
     607        background: var(--cartexa-accent, #7c3aed);
     608        color: #fff;
     609        border: none;
     610        border-radius: 8px;
     611        font-size: 13px;
     612        font-weight: 700;
     613        cursor: pointer;
     614        font-family: inherit;
     615        white-space: nowrap;
     616        flex-shrink: 0;
     617        transition: background 0.15s;
     618}
     619.cartexa-coupon-box__apply:hover   { background: var(--cartexa-accent-dark, #6d28d9); }
     620.cartexa-coupon-box__apply:disabled { opacity: 0.6; cursor: not-allowed; }
     621
     622.cartexa-coupon-box__msg {
     623        font-size: 12px;
     624        min-height: 16px;
     625        line-height: 1.4;
     626}
     627.cartexa-coupon-box__msg--ok  { color: #15803d; font-weight: 600; }
     628.cartexa-coupon-box__msg--err { color: #dc2626; }
     629
     630.cartexa-coupon-box__tags {
     631        list-style: none;
     632        padding: 0;
     633        margin: 6px 0 0;
     634        display: flex;
     635        flex-wrap: wrap;
     636        gap: 6px;
     637}
     638
     639.cartexa-coupon-tag {
     640        display: inline-flex;
     641        align-items: center;
     642        gap: 5px;
     643        background: #dcfce7;
     644        border: 1px solid #bbf7d0;
     645        border-radius: 6px;
     646        padding: 3px 8px;
     647}
     648.cartexa-coupon-tag__code {
     649        font-size: 11px;
     650        font-weight: 700;
     651        color: #15803d;
     652        letter-spacing: 0.05em;
     653}
     654.cartexa-coupon-tag__remove {
     655        background: none;
     656        border: none;
     657        cursor: pointer;
     658        color: #86efac;
     659        font-size: 14px;
     660        line-height: 1;
     661        padding: 0;
     662        font-family: inherit;
     663        transition: color 0.15s;
     664}
     665.cartexa-coupon-tag__remove:hover   { color: #dc2626; }
     666.cartexa-coupon-tag__remove:disabled { opacity: 0.5; cursor: not-allowed; }
  • cartexa-smart-cart-for-woocommerce/trunk/frontend/js/cartexa.js

    r3690135 r3734660  
    595595                } );
    596596
    597                 // ── WooCommerce: added_to_cart (classic jQuery event via body) ──
    598                 // WC core fires this on both classic themes AND Block themes.
    599                 on( document.body, 'added_to_cart', () => {
    600                         log( 'WC added_to_cart fired' );
    601 
    602                         // Level 2: Don't auto-open drawer on Block cart/checkout pages.
     597                // ── WooCommerce classic: added_to_cart / removed_from_cart ────
     598                //
     599                // FIX (Bug 2): WooCommerce fires these events via jQuery's
     600                // trigger() on document.body (e.g. `$( document.body ).trigger(
     601                // 'added_to_cart', [...] )`). jQuery triggers do NOT propagate to
     602                // native addEventListener listeners — only to handlers registered
     603                // with $.on() / $.bind(). So `on( document.body, 'added_to_cart' )`
     604                // never fires on shop/archive pages where WC's own AJAX runs.
     605                //
     606                // Fix: when jQuery is present, register a jQuery handler that
     607                // re-dispatches a native CustomEvent so our vanilla listener picks
     608                // it up. The native listener below then handles both paths.
     609                ( function installJQueryBridge() {
     610                        if ( window.jQuery ) {
     611                                window.jQuery( document.body )
     612                                        .on( 'added_to_cart',     function () {
     613                                                document.body.dispatchEvent(
     614                                                        new CustomEvent( 'cartexa:added_to_cart', { bubbles: false } )
     615                                                );
     616                                        } )
     617                                        .on( 'removed_from_cart', function () {
     618                                                document.body.dispatchEvent(
     619                                                        new CustomEvent( 'cartexa:removed_from_cart', { bubbles: false } )
     620                                                );
     621                                        } );
     622                                log( 'jQuery bridge installed for WC classic cart events' );
     623                        } else {
     624                                // No jQuery (headless / block-only themes): also listen to the
     625                                // native DOM event that our own wireSingleProductForm dispatches.
     626                                on( document.body, 'added_to_cart', function () {
     627                                        document.body.dispatchEvent(
     628                                                new CustomEvent( 'cartexa:added_to_cart', { bubbles: false } )
     629                                        );
     630                                } );
     631                        }
     632                } )();
     633
     634                // Unified native listener — handles both jQuery-bridge and block events.
     635                on( document.body, 'cartexa:added_to_cart', function () {
     636                        log( 'WC added_to_cart (classic)' );
    603637                        if ( cartexaIsBlockCartPage() || cartexaIsBlockCheckoutPage() ) {
    604638                                refreshCart();
    605639                                return;
    606640                        }
    607 
    608641                        const trigger = cfg.drawerTrigger || 'both';
    609642                        if ( trigger === 'add_to_cart' || trigger === 'both' ) {
    … …  
    614647                } );
    615648
    616                 // ── WooCommerce: removed_from_cart ────────────────────────
    617                 on( document.body, 'removed_from_cart', () => {
    618                         log( 'WC removed_from_cart fired' );
    619                         // Always refresh — no drawer open on remove.
     649                on( document.body, 'cartexa:removed_from_cart', function () {
     650                        log( 'WC removed_from_cart (classic)' );
    620651                        refreshCart();
    621652                } );
    … …  
    678709         * - Products that require page refresh (e.g. subscriptions).
    679710         */
     711        /**
     712         * Wire the single-product add-to-cart form for AJAX interception.
     713         *
     714         * Fixes vs original:
     715         *  1. Uses `new FormData(form, submitter)` so the submit button's
     716         *     name="add-to-cart" value="ID" is included in the request.
     717         *     Without the submitter argument WooCommerce never receives the
     718         *     product ID and silently ignores the POST, yet returns HTTP 200.
     719         *
     720         *  2. Checks the HTML response for WooCommerce error notices instead
     721         *     of trusting res.ok alone — WC always returns 200 even on failure.
     722         *
     723         *  3. Does NOT bail early on `.disabled` at wire-time.
     724         *     Variable products toggle that class on variation change; bailing
     725         *     at init time means the listener is never attached, so the very
     726         *     first valid variation click is missed. Instead we check `.disabled`
     727         *     inside the submit handler, at the moment of submission.
     728         *
     729         *  4. Falls back gracefully for products that require a page reload
     730         *     (subscriptions, bundles) — if WC redirects, we follow it.
     731         */
    680732        function wireSingleProductForm() {
    681733                // If WC is set to redirect to cart page, don't intercept.
    … …  
    688740                if ( ! form ) return;
    689741
    690                 // Skip if already handled by WC Blocks or variation JS.
    691742                const addToCartBtn = qs( '.single_add_to_cart_button', form );
    692743                if ( ! addToCartBtn ) return;
    693744
    694                 // Variable products handle their own AJAX — only intercept simple.
    695                 if (
    696                         addToCartBtn.classList.contains( 'disabled' ) ||
    697                         document.body.classList.contains( 'woocommerce-variation-add-to-cart-disabled' )
    698                 ) {
    699                         return;
    700                 }
    701 
    702745                on( form, 'submit', async function ( e ) {
    703                         // If button has 'disabled' class (variation not selected), let WC handle.
     746                        // At submit-time: if variation is not selected WC puts `.disabled`
     747                        // on the button — respect that and let WC handle the validation.
    704748                        if ( addToCartBtn.classList.contains( 'disabled' ) ) return;
    705749
     750                        // Also respect WC's own variation-disabled body class.
     751                        if ( document.body.classList.contains( 'woocommerce-variation-add-to-cart-disabled' ) ) return;
     752
    706753                        e.preventDefault();
    707754
    708                         const data  = new FormData( form );
    709                         const btn   = addToCartBtn;
    710 
     755                        // FIX 1: Pass `e.submitter ?? addToCartBtn` as the second argument
     756                        // to FormData so the button's name/value pair (add-to-cart=ID) is
     757                        // included. Without this, WooCommerce never knows which product to add.
     758                        const submitter = e.submitter || addToCartBtn;
     759                        const data      = new FormData( form, submitter );
     760
     761                        // Fallback for browsers that don't support FormData(form, submitter):
     762                        // manually append the button value if it's missing.
     763                        if ( ! data.has( 'add-to-cart' ) && submitter.name ) {
     764                                data.append( submitter.name, submitter.value );
     765                        }
     766
     767                        const btn = addToCartBtn;
    711768                        btn.classList.add( 'loading' );
    712769                        btn.setAttribute( 'disabled', 'disabled' );
    713770
    714771                        try {
    715                                 const res = await fetch( window.location.href, {
    716                                         method: 'POST',
    717                                         body: data,
     772                                const res  = await fetch( window.location.href, {
     773                                        method:  'POST',
     774                                        body:    data,
    718775                                        headers: { 'X-Requested-With': 'XMLHttpRequest' },
    719776                                } );
    720777
    721                                 if ( res.ok ) {
    722                                         // Trigger WC event so drawer opens.
    723                                         document.body.dispatchEvent(
    724                                                 new CustomEvent( 'added_to_cart', { bubbles: true, detail: {} } )
    725                                         );
    726                                         log( 'Single product add-to-cart intercepted successfully' );
     778                                if ( ! res.ok ) {
     779                                        // Network/server error — fall back to native submit.
     780                                        log( 'Single product ATC: non-OK response, falling back', res.status );
     781                                        form.submit();
     782                                        return;
    727783                                }
     784
     785                                // FIX 2: WooCommerce ALWAYS returns HTTP 200 even when the add
     786                                // fails (e.g. out of stock). Parse the HTML and look for WC
     787                                // error notices before declaring success.
     788                                const html    = await res.text();
     789                                const parser  = new DOMParser();
     790                                const doc     = parser.parseFromString( html, 'text/html' );
     791                                const notices = doc.querySelectorAll(
     792                                        '.woocommerce-error li, .woocommerce-notices-wrapper .woocommerce-error'
     793                                );
     794
     795                                if ( notices.length ) {
     796                                        // Surface the first error message to the user.
     797                                        const msg = notices[ 0 ].textContent.trim();
     798                                        log( 'Single product ATC: WC error —', msg );
     799                                        // Re-render native notices in page (WC handles the rest on fallback).
     800                                        form.submit();
     801                                        return;
     802                                }
     803
     804                                // WC sometimes redirects to /cart — detect via response URL.
     805                                if ( res.url && res.url !== window.location.href && res.url.indexOf( 'add-to-cart' ) === -1 ) {
     806                                        // A redirect happened (e.g. subscription product) — follow it.
     807                                        window.location.href = res.url;
     808                                        return;
     809                                }
     810
     811                                // Success — dispatch our internal event so drawer opens.
     812                                // We use 'cartexa:added_to_cart' (the same event the jQuery bridge
     813                                // re-emits) rather than the raw 'added_to_cart' so both paths go
     814                                // through the same unified handler above.
     815                                document.body.dispatchEvent(
     816                                        new CustomEvent( 'cartexa:added_to_cart', { bubbles: false } )
     817                                );
     818                                log( 'Single product add-to-cart: success' );
     819
    728820                        } catch ( err ) {
    729                                 log( 'Single product intercept error — falling back to native submit', err );
    730                                 // Fall back to native form submit on error.
     821                                log( 'Single product ATC: network error, falling back', err );
    731822                                form.submit();
    732823                        } finally {
    … …  
    13121403        log( 'Tab focus refresh: active (threshold ' + STALE_MS / 1000 + 's)' );
    13131404} )();
     1405
     1406/* ============================================================
     1407 * v1.0.1 — Save for Later · Coupon Box
     1408 * ============================================================ */
     1409( function () {
     1410        'use strict';
     1411
     1412        const cfg     = window.cartexaConfig || {};
     1413        const ajaxUrl = cfg.ajaxUrl || '';
     1414        const nonce   = cfg.nonce   || '';
     1415
     1416        function qs( sel, ctx )  { return ( ctx || document ).querySelector( sel ); }
     1417        function qsa( sel, ctx ) { return Array.from( ( ctx || document ).querySelectorAll( sel ) ); }
     1418
     1419        async function wpAjax( action, data ) {
     1420                const body = new FormData();
     1421                body.append( 'action', action );
     1422                body.append( 'nonce',  nonce );
     1423                Object.keys( data ).forEach( k => body.append( k, data[ k ] ) );
     1424                const res = await fetch( ajaxUrl, { method: 'POST', body } );
     1425                return res.json();
     1426        }
     1427
     1428        // ── SAVE FOR LATER ────────────────────────────────────────
     1429        const sflCfg = ( cfg.saveForLater && cfg.saveForLater.enabled ) ? cfg.saveForLater : null;
     1430
     1431        function updateSavedSection( html ) {
     1432                const wrap = qs( '.cartexa-saved-items-wrap' );
     1433                if ( ! wrap ) return;
     1434                wrap.innerHTML = html || '';
     1435                bindSavedBtns( wrap );
     1436        }
     1437
     1438        function bindSavedBtns( ctx ) {
     1439                qsa( '.cartexa-saved-item__restore', ctx ).forEach( btn => {
     1440                        btn.addEventListener( 'click', async function () {
     1441                                const key = this.dataset.saveKey;
     1442                                this.disabled = true;
     1443                                try {
     1444                                        const r = await wpAjax( 'cartexa_restore_saved', { save_key: key } );
     1445                                        if ( r.success ) {
     1446                                                updateSavedSection( r.data.saved );
     1447                                                document.dispatchEvent( new CustomEvent( 'cartexa:cart_updated' ) );
     1448                                        }
     1449                                } finally { this.disabled = false; }
     1450                        } );
     1451                } );
     1452
     1453                qsa( '.cartexa-saved-item__remove', ctx ).forEach( btn => {
     1454                        btn.addEventListener( 'click', async function () {
     1455                                const key  = this.dataset.saveKey;
     1456                                const item = this.closest( '.cartexa-saved-item' );
     1457                                if ( item ) { item.style.opacity = '0.4'; item.style.pointerEvents = 'none'; }
     1458                                try {
     1459                                        const r = await wpAjax( 'cartexa_remove_saved', { save_key: key } );
     1460                                        if ( r.success ) updateSavedSection( r.data.saved );
     1461                                } finally {
     1462                                        if ( item ) { item.style.opacity = ''; item.style.pointerEvents = ''; }
     1463                                }
     1464                        } );
     1465                } );
     1466        }
     1467
     1468        if ( sflCfg ) {
     1469                // Delegate "Save for Later" clicks on live cart items.
     1470                document.addEventListener( 'click', async function ( e ) {
     1471                        const btn = e.target.closest( '.cartexa-save-for-later-btn' );
     1472                        if ( ! btn ) return;
     1473                        const cartItemKey = btn.dataset.cartItemKey;
     1474                        if ( ! cartItemKey ) return;
     1475                        btn.disabled    = true;
     1476                        btn.textContent = '\u2026';
     1477                        try {
     1478                                const r = await wpAjax( 'cartexa_save_for_later', { cart_item_key: cartItemKey } );
     1479                                if ( r.success ) {
     1480                                        updateSavedSection( r.data.saved );
     1481                                        document.dispatchEvent( new CustomEvent( 'cartexa:cart_updated' ) );
     1482                                }
     1483                        } finally {
     1484                                btn.disabled    = false;
     1485                                btn.textContent = sflCfg.btnText || 'Save for Later';
     1486                        }
     1487                } );
     1488
     1489                // Bind events on saved section already in DOM on load.
     1490                const initWrap = qs( '.cartexa-saved-items-wrap' );
     1491                if ( initWrap ) bindSavedBtns( initWrap );
     1492        }
     1493
     1494        // ── COUPON BOX ────────────────────────────────────────────
     1495        const couponCfg = ( cfg.couponBox && cfg.couponBox.enabled ) ? cfg.couponBox : null;
     1496
     1497        if ( couponCfg ) {
     1498                document.addEventListener( 'DOMContentLoaded', function () {
     1499                        const footer = qs( '.cartexa-drawer__footer' );
     1500                        if ( ! footer || qs( '.cartexa-coupon-box' ) ) return;
     1501
     1502                        const box = document.createElement( 'div' );
     1503                        box.className = 'cartexa-coupon-box';
     1504                        box.innerHTML =
     1505                                '<button type="button" class="cartexa-coupon-box__toggle" aria-expanded="false">' +
     1506                                        '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7" stroke-width="3" stroke-linecap="round"/></svg>' +
     1507                                        '<span>' + ( couponCfg.placeholder || 'Coupon code' ) + '</span>' +
     1508                                        '<svg class="cartexa-coupon-box__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>' +
     1509                                '</button>' +
     1510                                '<div class="cartexa-coupon-box__body" hidden>' +
     1511                                        '<div class="cartexa-coupon-box__row">' +
     1512                                                '<input type="text" class="cartexa-coupon-box__input" placeholder="' + ( couponCfg.placeholder || 'Coupon code' ) + '" autocomplete="off" aria-label="' + ( couponCfg.placeholder || 'Coupon code' ) + '" />' +
     1513                                                '<button type="button" class="cartexa-coupon-box__apply">' + ( couponCfg.btnText || 'Apply' ) + '</button>' +
     1514                                        '</div>' +
     1515                                        '<div class="cartexa-coupon-box__msg" role="status" aria-live="polite"></div>' +
     1516                                        '<ul class="cartexa-coupon-box__tags"></ul>' +
     1517                                '</div>';
     1518
     1519                        footer.prepend( box );
     1520
     1521                        const toggle   = qs( '.cartexa-coupon-box__toggle', box );
     1522                        const body     = qs( '.cartexa-coupon-box__body',   box );
     1523                        const input    = qs( '.cartexa-coupon-box__input',  box );
     1524                        const applyBtn = qs( '.cartexa-coupon-box__apply',  box );
     1525                        const msgEl    = qs( '.cartexa-coupon-box__msg',    box );
     1526                        const tagsEl   = qs( '.cartexa-coupon-box__tags',   box );
     1527
     1528                        toggle.addEventListener( 'click', function () {
     1529                                const open = body.hidden;
     1530                                body.hidden = ! open;
     1531                                toggle.setAttribute( 'aria-expanded', String( open ) );
     1532                                if ( open ) input.focus();
     1533                        } );
     1534
     1535                        applyBtn.addEventListener( 'click', applyCoupon );
     1536                        input.addEventListener( 'keydown', function ( e ) {
     1537                                if ( 'Enter' === e.key ) applyCoupon();
     1538                        } );
     1539
     1540                        async function applyCoupon() {
     1541                                const code = input.value.trim();
     1542                                if ( ! code ) return;
     1543                                applyBtn.disabled = true;
     1544                                msgEl.textContent = '\u2026';
     1545                                msgEl.className   = 'cartexa-coupon-box__msg';
     1546                                try {
     1547                                        const r = await wpAjax( 'cartexa_apply_coupon', { coupon_code: code } );
     1548                                        if ( r.success ) {
     1549                                                msgEl.className   = 'cartexa-coupon-box__msg cartexa-coupon-box__msg--ok';
     1550                                                msgEl.textContent = r.data.message;
     1551                                                input.value       = '';
     1552                                                renderTags( r.data.coupons || [] );
     1553                                                document.dispatchEvent( new CustomEvent( 'cartexa:cart_updated' ) );
     1554                                        } else {
     1555                                                msgEl.className   = 'cartexa-coupon-box__msg cartexa-coupon-box__msg--err';
     1556                                                msgEl.textContent = r.data.message;
     1557                                        }
     1558                                } catch {
     1559                                        msgEl.className   = 'cartexa-coupon-box__msg cartexa-coupon-box__msg--err';
     1560                                        msgEl.textContent = 'Request failed.';
     1561                                }
     1562                                applyBtn.disabled = false;
     1563                        }
     1564
     1565                        function renderTags( coupons ) {
     1566                                tagsEl.innerHTML = '';
     1567                                coupons.forEach( function ( code ) {
     1568                                        const li = document.createElement( 'li' );
     1569                                        li.className = 'cartexa-coupon-tag';
     1570                                        li.innerHTML =
     1571                                                '<span class="cartexa-coupon-tag__code">' + code.toUpperCase() + '</span>' +
     1572                                                '<button type="button" class="cartexa-coupon-tag__remove" data-coupon="' + code + '" aria-label="' + ( couponCfg.removeTitle || 'Remove coupon' ) + '">\u00d7</button>';
     1573                                        tagsEl.appendChild( li );
     1574                                } );
     1575
     1576                                qsa( '.cartexa-coupon-tag__remove', tagsEl ).forEach( function ( btn ) {
     1577                                        btn.addEventListener( 'click', async function () {
     1578                                                const code = this.dataset.coupon;
     1579                                                this.disabled = true;
     1580                                                const r = await wpAjax( 'cartexa_remove_coupon', { coupon_code: code } );
     1581                                                if ( r.success ) {
     1582                                                        msgEl.className   = 'cartexa-coupon-box__msg';
     1583                                                        msgEl.textContent = r.data.message;
     1584                                                        renderTags( r.data.coupons || [] );
     1585                                                        document.dispatchEvent( new CustomEvent( 'cartexa:cart_updated' ) );
     1586                                                }
     1587                                        } );
     1588                                } );
     1589                        }
     1590                } );
     1591        }
     1592
     1593        // ── QTY DIRECT INPUT VALIDATION ───────────────────────────
     1594        // Clamp value on input so +/- buttons and direct typing stay in sync.
     1595        document.addEventListener( 'input', function ( e ) {
     1596                const input = e.target.closest( '.cartexa-qty-input' );
     1597                if ( ! input ) return;
     1598                const min = parseInt( input.getAttribute( 'min' ) || '1', 10 );
     1599                const max = parseInt( input.getAttribute( 'max' ) || '999', 10 );
     1600                let   val = parseInt( input.value, 10 );
     1601                if ( isNaN( val ) || val < min ) { input.value = min; }
     1602                if ( val > max )                 { input.value = max; }
     1603        } );
     1604
     1605} )();
  • cartexa-smart-cart-for-woocommerce/trunk/frontend/templates/cart-drawer.php

    r3690135 r3734660  
    140140        <?php
    141141        /**
     142         * Save for Later section (v1.0.1).
     143         * Populated on load; swapped via AJAX on save/restore.
     144         */
     145        if ( \Cartexa\Settings::fetch_bool( 'enable_save_for_later' ) ) :
     146                ?>
     147                <div class="cartexa-saved-items-wrap">
     148                        <?php
     149                        if ( class_exists( '\\Cartexa\\Module_Save_For_Later' ) ) {
     150                                echo \Cartexa\Module_Save_For_Later::instance()->get_rendered_saved_list(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     151                        }
     152                        ?>
     153                </div>
     154                <?php
     155        endif;
     156        ?>
     157
     158        <?php
     159        /**
    142160         * Hook: cartexa_drawer_before_footer
    143161         * Pro: Cross-sell / upsell block renders here.
  • cartexa-smart-cart-for-woocommerce/trunk/frontend/templates/cart-item.php

    r3690135 r3734660  
    157157                        </span>
    158158
     159                        <?php
     160                        // Save for Later button — only when module is enabled.
     161                        if ( \Cartexa\Settings::fetch_bool( 'enable_save_for_later' ) ) :
     162                                $btn_text = \Cartexa\Settings::fetch( 'save_for_later_btn_text', '' )
     163                                        ?: __( 'Save for Later', 'cartexa-smart-cart-for-woocommerce' );
     164                                ?>
     165                                <button
     166                                        type="button"
     167                                        class="cartexa-save-for-later-btn"
     168                                        data-cart-item-key="<?php echo esc_attr( $cart_item_key ); ?>"
     169                                        aria-label="<?php esc_attr_e( 'Save this item for later', 'cartexa-smart-cart-for-woocommerce' ); ?>"
     170                                >
     171                                        <?php echo esc_html( $btn_text ); ?>
     172                                </button>
     173                                <?php
     174                        endif;
     175                        ?>
    159176                </div><!-- /.cartexa-cart-item__qty-row -->
    160177
  • cartexa-smart-cart-for-woocommerce/trunk/includes/class-cartexa-ajax.php

    r3690135 r3734660  
    6969                add_action( 'wp_ajax_cartexa_refresh_nonce',        array( $this, 'handle_refresh_nonce' ) );
    7070                add_action( 'wp_ajax_nopriv_cartexa_refresh_nonce', array( $this, 'handle_refresh_nonce' ) );
     71
     72                // Coupon Box (v1.0.1).
     73                if ( Settings::fetch_bool( 'enable_coupon_box' ) && wc_coupons_enabled() ) {
     74                        add_action( 'wp_ajax_cartexa_apply_coupon',         array( $this, 'handle_apply_coupon' ) );
     75                        add_action( 'wp_ajax_nopriv_cartexa_apply_coupon',  array( $this, 'handle_apply_coupon' ) );
     76                        add_action( 'wp_ajax_cartexa_remove_coupon',        array( $this, 'handle_remove_coupon' ) );
     77                        add_action( 'wp_ajax_nopriv_cartexa_remove_coupon', array( $this, 'handle_remove_coupon' ) );
     78                }
    7179        }
    7280
    … …  
    108116         * qty = 0 removes the item.
    109117         *
     118         * FIX (Bug 4): The original code called set_quantity() directly without
     119         * validating the requested quantity against:
     120         *   a) Stock limits — set_quantity() itself does NOT check stock; it just
     121         *      updates the session. Over-purchasing becomes possible silently.
     122         *   b) Product max purchase quantity — get_max_purchase_quantity() returns
     123         *      -1 for unlimited, 1 for sold-individually, and the actual limit for
     124         *      products with max quantity set. The old max_qty calculation used
     125         *      get_stock_quantity() which ignores sold-individually and backorders.
     126         *   c) WooCommerce cart validation filters — third-party plugins (e.g.
     127         *      membership/subscription plugins) hook woocommerce_update_cart_validation
     128         *      to block certain qty changes. These were bypassed entirely.
     129         *
     130         * Fix:
     131         *   1. Compute the real max via get_max_purchase_quantity() which handles
     132         *      sold-individually (-1 stock), backorders, and explicit max limits.
     133         *   2. Clamp the requested qty client-side error before touching the cart.
     134         *   3. Run apply_filters('woocommerce_update_cart_validation') so third-party
     135         *      plugins can block the update, identical to what WC's own cart page does.
     136         *
    110137         * @return void
    111138         */
    … …  
    127154                }
    128155
    129                 $cart = WC()->cart;
    130 
    131                 if ( ! $cart->get_cart_item( $cart_item_key ) ) {
     156                $cart      = WC()->cart;
     157                $cart_item = $cart->get_cart_item( $cart_item_key );
     158
     159                if ( ! $cart_item ) {
    132160                        wp_send_json_error(
    133161                                array( 'message' => esc_html__( 'Cart item not found.', 'cartexa-smart-cart-for-woocommerce' ) )
    134162                        );
     163                }
     164
     165                // ── FIX 4a: Real max qty via get_max_purchase_quantity() ────────────
     166                // This respects sold_individually (returns 1), backorders (no cap if
     167                // backorders are allowed), and explicit max purchase qty limits.
     168                // It returns -1 when there is no upper limit.
     169                $product = $cart_item['data'] instanceof \WC_Product
     170                        ? $cart_item['data']
     171                        : wc_get_product( $cart_item['product_id'] );
     172
     173                if ( $product && $product->exists() ) {
     174                        $max_qty = $product->get_max_purchase_quantity(); // -1 = unlimited.
     175                        if ( $max_qty > 0 && $qty > $max_qty ) {
     176                                $qty = $max_qty;
     177                        }
    135178                }
    136179
    137180                if ( 0 === $qty ) {
    138181                        $cart->remove_cart_item( $cart_item_key );
    139                 } else {
    140                         $updated = $cart->set_quantity( $cart_item_key, $qty, true );
    141 
    142                         if ( false === $updated ) {
    143                                 wp_send_json_error(
    144                                         array( 'message' => esc_html__( 'Could not update quantity. Please check stock availability.', 'cartexa-smart-cart-for-woocommerce' ) )
    145                                 );
    146                         }
     182                        $cart->calculate_totals();
     183                        wp_send_json_success( $this->build_cart_payload() );
     184                        return;
     185                }
     186
     187                // ── FIX 4b: Run WooCommerce cart validation filters ──────────────────
     188                // woocommerce_update_cart_validation lets third-party plugins (membership,
     189                // subscriptions, min/max qty plugins) block the update — exactly what the
     190                // WC native cart page does before calling set_quantity().
     191                $passed = apply_filters(
     192                        'woocommerce_update_cart_validation',
     193                        true,
     194                        $cart_item_key,
     195                        $cart_item,
     196                        $qty
     197                );
     198
     199                if ( ! $passed ) {
     200                        // Collect the WC error notices the filter may have added.
     201                        $notices = wc_get_notices( 'error' );
     202                        wc_clear_notices();
     203                        $msg = ! empty( $notices )
     204                                ? wp_strip_all_tags( $notices[0]['notice'] )
     205                                : esc_html__( 'Quantity update not allowed.', 'cartexa-smart-cart-for-woocommerce' );
     206                        wp_send_json_error( array( 'message' => $msg ) );
     207                }
     208
     209                $updated = $cart->set_quantity( $cart_item_key, $qty, true );
     210
     211                if ( false === $updated ) {
     212                        wp_send_json_error(
     213                                array( 'message' => esc_html__( 'Could not update quantity. Please check stock availability.', 'cartexa-smart-cart-for-woocommerce' ) )
     214                        );
    147215                }
    148216
    … …  
    232300                        }
    233301
    234                         $max_qty = $product->managing_stock()
    235                                 ? max( 1, (int) $product->get_stock_quantity() )
    236                                 : 99;
     302                        // FIX (Bug 4): Use get_max_purchase_quantity() instead of raw
     303                        // get_stock_quantity(). This correctly returns 1 for sold-individually
     304                        // products, respects backorders (no cap), and picks up any explicit
     305                        // max quantity set by third-party plugins. Returns -1 = unlimited.
     306                        $raw_max = $product->get_max_purchase_quantity();
     307                        $max_qty = ( -1 === $raw_max ) ? 9999 : max( 1, (int) $raw_max );
    237308
    238309                        $items[] = array(
    … …  
    264335                );
    265336        }
     337
     338        // ─── Coupon Box (v1.0.1) ─────────────────────────────────────
     339
     340        /**
     341         * Apply a WooCommerce coupon code to the cart.
     342         * POST params: nonce, coupon_code
     343         */
     344        public function handle_apply_coupon(): void {
     345                check_ajax_referer( 'cartexa_nonce', 'nonce' );
     346
     347                $coupon_code = sanitize_text_field( wp_unslash( $_POST['coupon_code'] ?? '' ) );
     348                if ( '' === $coupon_code ) {
     349                        wp_send_json_error( array( 'message' => __( 'Please enter a coupon code.', 'cartexa-smart-cart-for-woocommerce' ) ) );
     350                }
     351
     352                $applied = WC()->cart->apply_coupon( $coupon_code );
     353                if ( $applied ) {
     354                        WC()->cart->calculate_totals();
     355                        wp_send_json_success( array(
     356                                'message' => sprintf(
     357                                        /* translators: %s: coupon code */
     358                                        esc_html__( 'Coupon "%s" applied!', 'cartexa-smart-cart-for-woocommerce' ),
     359                                        esc_html( strtoupper( $coupon_code ) )
     360                                ),
     361                                'coupons'  => array_values( WC()->cart->get_applied_coupons() ),
     362                                'discount' => WC()->cart->get_discount_total(),
     363                                'total'    => WC()->cart->get_total( 'edit' ),
     364                        ) );
     365                } else {
     366                        $notices = wc_get_notices( 'error' );
     367                        wc_clear_notices();
     368                        $msg = ! empty( $notices )
     369                                ? wp_strip_all_tags( $notices[0]['notice'] )
     370                                : __( 'Invalid coupon code.', 'cartexa-smart-cart-for-woocommerce' );
     371                        wp_send_json_error( array( 'message' => $msg ) );
     372                }
     373        }
     374
     375        /**
     376         * Remove an applied coupon from the cart.
     377         * POST params: nonce, coupon_code
     378         */
     379        public function handle_remove_coupon(): void {
     380                check_ajax_referer( 'cartexa_nonce', 'nonce' );
     381
     382                $coupon_code = sanitize_text_field( wp_unslash( $_POST['coupon_code'] ?? '' ) );
     383                if ( '' === $coupon_code ) {
     384                        wp_send_json_error( array( 'message' => __( 'No coupon specified.', 'cartexa-smart-cart-for-woocommerce' ) ) );
     385                }
     386
     387                WC()->cart->remove_coupon( $coupon_code );
     388                WC()->cart->calculate_totals();
     389
     390                wp_send_json_success( array(
     391                        'message' => sprintf(
     392                                /* translators: %s: coupon code */
     393                                esc_html__( 'Coupon "%s" removed.', 'cartexa-smart-cart-for-woocommerce' ),
     394                                esc_html( strtoupper( $coupon_code ) )
     395                        ),
     396                        'coupons' => array_values( WC()->cart->get_applied_coupons() ),
     397                        'total'   => WC()->cart->get_total( 'edit' ),
     398                ) );
     399        }
    266400}
  • cartexa-smart-cart-for-woocommerce/trunk/includes/class-cartexa-assets.php

    r3690135 r3734660  
    8080                }
    8181
     82                // Global load setting — user opted in to load everywhere.
    8283                if ( Settings::fetch_bool( 'load_assets_globally' ) ) {
    8384                        return true;
    8485                }
    8586
     87                // Standard WooCommerce pages — always load here.
    8688                if ( is_woocommerce() || is_cart() || is_checkout() || is_account_page() ) {
    8789                        return true;
    … …  
    8991
    9092                if ( is_front_page() || is_shop() ) {
     93                        return true;
     94                }
     95
     96                // FIX (Bug 3): Modules that render HTML on every page type also need
     97                // their CSS/JS. Without this check, a Nav Menu Cart icon or FAB on a
     98                // blog post or static page would render unstyled markup with dead JS.
     99                //
     100                // Force-load whenever any of these "always-on" modules is active:
     101                //   • Floating Cart (FAB) — rendered via wp_footer on all pages.
     102                //   • Nav Menu Cart Icon — injected into menus on all pages.
     103                //   • Mini Cart shortcode / widget — can appear on any page.
     104                if (
     105                        Settings::fetch_bool( 'enable_floating_cart' ) ||
     106                        Settings::fetch_bool( 'enable_nav_menu' ) ||
     107                        Settings::fetch_bool( 'enable_mini_cart' )
     108                ) {
    91109                        return true;
    92110                }
    … …  
    96114                 *
    97115                 * @since 1.0.0
    98                  * @param bool $should_load Default false.
     116                 * @param bool $should_load Default false for pages not matching above.
    99117                 */
    100118                return (bool) apply_filters( 'cartexa_load_assets', false );
    … …  
    210228        private function any_module_active(): bool {
    211229                $module_keys = array(
    212                         'enable_floating_cart', 'enable_shipping_bar',
    213                         'enable_trust_badges',  'enable_scarcity',
     230                        'enable_floating_cart',  'enable_shipping_bar',
     231                        'enable_trust_badges',   'enable_scarcity',
     232                        'enable_save_for_later', 'enable_coupon_box',
    214233                );
    215234                foreach ( $module_keys as $key ) {
    … …  
    354373                        ),
    355374
     375                        // Save for Later (v1.0.1).
     376                        'saveForLater'     => array(
     377                                'enabled'     => $s->get_bool( 'enable_save_for_later' ),
     378                                'btnText'     => sanitize_text_field( $s->get_string( 'save_for_later_btn_text', '' ) )
     379                                                                        ?: __( 'Save for Later', 'cartexa-smart-cart-for-woocommerce' ),
     380                                'restoreText' => sanitize_text_field( $s->get_string( 'restore_btn_text', '' ) )
     381                                                                        ?: __( 'Move to Cart', 'cartexa-smart-cart-for-woocommerce' ),
     382                        ),
     383
     384                        // Coupon Box (v1.0.1).
     385                        'couponBox'        => array(
     386                                'enabled'      => $s->get_bool( 'enable_coupon_box' ) && wc_coupons_enabled(),
     387                                'placeholder'  => sanitize_text_field( $s->get_string( 'coupon_box_placeholder', '' ) )
     388                                                                        ?: __( 'Coupon code', 'cartexa-smart-cart-for-woocommerce' ),
     389                                'btnText'      => sanitize_text_field( $s->get_string( 'coupon_box_btn_text', '' ) )
     390                                                                        ?: __( 'Apply', 'cartexa-smart-cart-for-woocommerce' ),
     391                                'removeTitle'  => __( 'Remove coupon', 'cartexa-smart-cart-for-woocommerce' ),
     392                        ),
     393
    356394                );
    357395
  • cartexa-smart-cart-for-woocommerce/trunk/includes/class-cartexa-settings.php

    r3690135 r3734660  
    33 * Settings Manager — MVP v1.0
    44 *
    5  * Single source of truth for all 12 Cartexa feature options.
     5 * Single source of truth for all Cartexa feature options.
    66 *
    77 * Feature groups:
    … …  
    183183                        'enable_undo_remove'        => 'yes',
    184184                        'undo_remove_duration'      => '5',
     185
     186                        // ── Feature: Save for Later (v1.0.1) ────────────────
     187                        'enable_save_for_later'     => 'yes',
     188                        'save_for_later_btn_text'   => '',
     189                        'restore_btn_text'          => '',
     190
     191                        // ── Feature: Coupon Box (v1.0.1) ─────────────────────
     192                        'enable_coupon_box'         => 'yes',
     193                        'coupon_box_placeholder'    => '',
     194                        'coupon_box_btn_text'       => '',
    185195                );
    186196        }
    … …  
    294304                        // Undo Remove.
    295305                        'enable_undo_remove',
     306                        // Save for Later.
     307                        'enable_save_for_later',
     308                        // Coupon Box.
     309                        'enable_coupon_box',
    296310                );
    297311                foreach ( $toggles as $key ) {
    … …  
    348362                        'scarcity_message',
    349363                        'trust_custom_text_1', 'trust_custom_text_2',
     364                        'save_for_later_btn_text', 'restore_btn_text',
     365                        'coupon_box_placeholder', 'coupon_box_btn_text',
    350366                );
    351367                foreach ( $text_keys as $key ) {
  • cartexa-smart-cart-for-woocommerce/trunk/includes/modules/class-cartexa-module-loader.php

    r3690135 r3734660  
    33 * Module Loader.
    44 *
    5  * Central registry that boots all 11 feature modules
     5 * Central registry that boots all active feature modules
    66 * (Side Cart is handled by the core Frontend class).
    77 *
    … …  
    7878                Toast::instance();
    7979
     80                // v1.0.1: Save for Later.
     81                \Cartexa\Module_Save_For_Later::instance()->hooks();
     82
    8083                /**
    8184                 * Fires after all modules have been booted.
  • cartexa-smart-cart-for-woocommerce/trunk/readme.txt

    r3690135 r3734660  
    11=== Cartexa – Smart Cart for WooCommerce ===
    22Contributors: webtendhq,devhasib
    3 Tags: woocommerce, cart, side cart, ajax cart, mini cart
     3Tags: woocommerce cart, side cart, ajax cart, sliding cart, mini cart
    44Requires at least: 6.3
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.0.0
     7Stable tag: 1.0.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1111WC tested up to: 9.9
    1212
    13 Smart AJAX cart for WooCommerce — side cart drawer, floating cart, free shipping bar, trust badges, scarcity messages & more. Zero jQuery.
     13AJAX side cart for WooCommerce — sliding cart drawer, floating cart, free shipping bar, coupon box, save for later & more. No jQuery dependency. Beginner-friendly.
    1414
    1515== Description ==
    … …  
    1717**Cartexa – Smart Cart** replaces the default WooCommerce cart redirect with a smooth, instant AJAX experience that keeps shoppers on the page and moving toward checkout.
    1818
    19 8 independent modules. One lightweight plugin. Works with any theme — zero jQuery, zero page reloads.
     198 independent modules. One lightweight plugin. Works with any theme — No jQuery dependency, zero page reloads.
     20
     21🎥 **See Cartexa in Action:**
     22
     23https://youtu.be/5X_ZIH_6OtM?si=dFmSGuFozEJiP_FU
     24
     25📖 [View Documentation](https://webtend.net/docs/cartexa-doc.html) | 🌐 [WebTend](https://webtend.net/)
    2026
    2127= 🛒 Cart Modules =
    … …  
    5258= ⚡ Performance =
    5359
    54 * **Zero jQuery** — pure vanilla ES6 throughout
     60* **No jQuery dependency** — pure vanilla ES6 throughout
    5561* **Module-level loading** — disabled modules register zero hooks and load zero assets
    5662* **Smart enqueue** — CSS/JS load only on WooCommerce pages by default
    … …  
    117123== Changelog ==
    118124
     125= 1.0.1 =
     126* New: Save for Later — move cart items to a saved list and restore anytime
     127* New: Coupon Box — apply and remove WooCommerce coupon codes inside the drawer (free)
     128* Fix: Single product add-to-cart — `FormData(form, submitter)` now used so `add-to-cart=ID` is included in POST; WC error notices checked in response HTML (HTTP 200 alone is not sufficient)
     129* Fix: Variable products — listener no longer wired at init based on `.disabled` class; drawer correctly intercepts first valid variation selection
     130* Fix: Shop/archive pages — jQuery bridge installed so WooCommerce `$(body).trigger('added_to_cart')` reaches vanilla JS listeners; drawer opens on all page types
     131* Fix: Blog/page assets — force-load when Floating Cart, Nav Menu, or Shortcode module is active, preventing unstyled markup
     132* Fix: Server-side qty validation uses `get_max_purchase_quantity()` (respects sold-individually, backorders, third-party limits) and runs `woocommerce_update_cart_validation` filter before `set_quantity()`
     133* Tweak: Plugin name and tags updated for better WP.org search visibility
     134* Tweak: "Zero jQuery" corrected to "No jQuery dependency" — jQuery bridge used when present
     135
    119136= 1.0.0 =
    120137* Initial release.
    … …  
    126143* Cart Refresh on Tab Focus — auto-syncs cart state when switching browser tabs.
    127144* WooCommerce Blocks support — Level 1 (Store API events) and Level 2 (Block page detection).
    128 * Zero jQuery — pure vanilla ES6 throughout.
     145* No jQuery dependency — pure vanilla ES6 throughout.
    129146* HPOS and Cart/Checkout Blocks compatibility declared.
    130147* PSR-4 namespaced PHP under Cartexa\, 25+ hooks, theme template overrides.
    … …  
    135152== Upgrade Notice ==
    136153
     154= 1.0.1 =
     155Adds Save for Later and free Coupon Box inside the drawer. Recommended update.
     156
     157
     158
    137159= 1.0.0 =
    138160Initial release.
Note: See TracChangeset for help on using the changeset viewer.