Changeset 3734660
- Timestamp:
- 10/08/2026 01:12:03 PM (2 weeks ago)
- Location:
- cartexa-smart-cart-for-woocommerce/trunk
- Files:
-
- 1 added
- 11 edited
-
admin/class-cartexa-admin-panel.php (modified) (9 diffs)
-
cartexa-smart-cart.php (modified) (2 diffs)
-
frontend/css/cartexa-modules.css (modified) (1 diff)
-
frontend/js/cartexa.js (modified) (5 diffs)
-
frontend/templates/cart-drawer.php (modified) (1 diff)
-
frontend/templates/cart-item.php (modified) (1 diff)
-
includes/class-cartexa-ajax.php (modified) (5 diffs)
-
includes/class-cartexa-assets.php (modified) (5 diffs)
-
includes/class-cartexa-settings.php (modified) (4 diffs)
-
includes/modules/class-cartexa-module-loader.php (modified) (2 diffs)
-
includes/modules/class-cartexa-module-save-for-later.php (added)
-
readme.txt (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
-
cartexa-smart-cart-for-woocommerce/trunk/admin/class-cartexa-admin-panel.php
r3690135 r3734660 67 67 'group' => __( 'Conversion', 'cartexa-smart-cart-for-woocommerce' ), 68 68 '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' ), 72 74 ), 73 75 ), … … 274 276 'floating' => $this->section_floating_cart(), 275 277 'shipping-bar' => $this->section_shipping_bar(), 278 'coupon-box' => $this->section_coupon_box(), 279 'save-for-later' => $this->section_save_for_later(), 276 280 'trust' => $this->section_trust_badges(), 277 281 'scarcity' => $this->section_scarcity(), … … 291 295 'side-cart' => __( 'Side Cart', 'cartexa-smart-cart-for-woocommerce' ), 292 296 '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' ), 295 301 'scarcity' => __( 'Stock Scarcity Messages', 'cartexa-smart-cart-for-woocommerce' ), 296 302 'display' => __( 'Display Settings', 'cartexa-smart-cart-for-woocommerce' ), … … 310 316 'floating' => __( 'A persistent floating button that opens the cart.', 'cartexa-smart-cart-for-woocommerce' ), 311 317 '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' ), 312 320 'trust' => __( 'Reassurance badges to reduce checkout hesitation.', 'cartexa-smart-cart-for-woocommerce' ), 313 321 'scarcity' => __( 'Show low-stock warnings to create urgency.', 'cartexa-smart-cart-for-woocommerce' ), … … 408 416 $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' ) ); 409 417 $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' ) ); 410 456 ?> 411 457 </div> … … 839 885 } 840 886 841 // ─── Section: Export / Import───────────────────────────────887 // ─── Section: Export / Import ───────────────────────────────── 842 888 843 889 private function section_export_import(): void { … … 907 953 </div> 908 954 </div> 909 910 955 <?php 911 956 // Export/Import JS logic is loaded via cartexa-admin.js … … 969 1014 } 970 1015 971 private function get_nav_icon( string $name ): string {1016 private function get_nav_icon( string $name ): string { 972 1017 $icons = array( 973 1018 '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>', 974 1019 '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>', 975 1020 '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>', 976 1023 '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>', 977 1024 '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>', … … 979 1026 '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>', 980 1027 '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>', 982 1029 '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>', 983 1030 '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 2 2 /** 3 3 * Plugin Name: Cartexa – Smart Cart for WooCommerce 4 * Plugin URI: https://w ebtend.net/cartexa5 * 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. 04 * 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 7 7 * Author: WebTend 8 8 * Author URI: https://webtend.net/ … … 27 27 28 28 // ── Constants ────────────────────────────────────────────────────────────── 29 define( 'CARTEXA_VERSION', '1.0. 0' );29 define( 'CARTEXA_VERSION', '1.0.1' ); 30 30 define( 'CARTEXA_FILE', __FILE__ ); 31 31 define( 'CARTEXA_PATH', plugin_dir_path( __FILE__ ) ); -
cartexa-smart-cart-for-woocommerce/trunk/frontend/css/cartexa-modules.css
r3690135 r3734660 386 386 .cartexa-snackbar--bottom-left { left: 12px; bottom: 72px; } 387 387 } 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 595 595 } ); 596 596 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)' ); 603 637 if ( cartexaIsBlockCartPage() || cartexaIsBlockCheckoutPage() ) { 604 638 refreshCart(); 605 639 return; 606 640 } 607 608 641 const trigger = cfg.drawerTrigger || 'both'; 609 642 if ( trigger === 'add_to_cart' || trigger === 'both' ) { … … 614 647 } ); 615 648 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)' ); 620 651 refreshCart(); 621 652 } ); … … 678 709 * - Products that require page refresh (e.g. subscriptions). 679 710 */ 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 */ 680 732 function wireSingleProductForm() { 681 733 // If WC is set to redirect to cart page, don't intercept. … … 688 740 if ( ! form ) return; 689 741 690 // Skip if already handled by WC Blocks or variation JS.691 742 const addToCartBtn = qs( '.single_add_to_cart_button', form ); 692 743 if ( ! addToCartBtn ) return; 693 744 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 702 745 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. 704 748 if ( addToCartBtn.classList.contains( 'disabled' ) ) return; 705 749 750 // Also respect WC's own variation-disabled body class. 751 if ( document.body.classList.contains( 'woocommerce-variation-add-to-cart-disabled' ) ) return; 752 706 753 e.preventDefault(); 707 754 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; 711 768 btn.classList.add( 'loading' ); 712 769 btn.setAttribute( 'disabled', 'disabled' ); 713 770 714 771 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, 718 775 headers: { 'X-Requested-With': 'XMLHttpRequest' }, 719 776 } ); 720 777 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; 727 783 } 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 728 820 } 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 ); 731 822 form.submit(); 732 823 } finally { … … 1312 1403 log( 'Tab focus refresh: active (threshold ' + STALE_MS / 1000 + 's)' ); 1313 1404 } )(); 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 140 140 <?php 141 141 /** 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 /** 142 160 * Hook: cartexa_drawer_before_footer 143 161 * Pro: Cross-sell / upsell block renders here. -
cartexa-smart-cart-for-woocommerce/trunk/frontend/templates/cart-item.php
r3690135 r3734660 157 157 </span> 158 158 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 ?> 159 176 </div><!-- /.cartexa-cart-item__qty-row --> 160 177 -
cartexa-smart-cart-for-woocommerce/trunk/includes/class-cartexa-ajax.php
r3690135 r3734660 69 69 add_action( 'wp_ajax_cartexa_refresh_nonce', array( $this, 'handle_refresh_nonce' ) ); 70 70 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 } 71 79 } 72 80 … … 108 116 * qty = 0 removes the item. 109 117 * 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 * 110 137 * @return void 111 138 */ … … 127 154 } 128 155 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 ) { 132 160 wp_send_json_error( 133 161 array( 'message' => esc_html__( 'Cart item not found.', 'cartexa-smart-cart-for-woocommerce' ) ) 134 162 ); 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 } 135 178 } 136 179 137 180 if ( 0 === $qty ) { 138 181 $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 ); 147 215 } 148 216 … … 232 300 } 233 301 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 ); 237 308 238 309 $items[] = array( … … 264 335 ); 265 336 } 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 } 266 400 } -
cartexa-smart-cart-for-woocommerce/trunk/includes/class-cartexa-assets.php
r3690135 r3734660 80 80 } 81 81 82 // Global load setting — user opted in to load everywhere. 82 83 if ( Settings::fetch_bool( 'load_assets_globally' ) ) { 83 84 return true; 84 85 } 85 86 87 // Standard WooCommerce pages — always load here. 86 88 if ( is_woocommerce() || is_cart() || is_checkout() || is_account_page() ) { 87 89 return true; … … 89 91 90 92 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 ) { 91 109 return true; 92 110 } … … 96 114 * 97 115 * @since 1.0.0 98 * @param bool $should_load Default false .116 * @param bool $should_load Default false for pages not matching above. 99 117 */ 100 118 return (bool) apply_filters( 'cartexa_load_assets', false ); … … 210 228 private function any_module_active(): bool { 211 229 $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', 214 233 ); 215 234 foreach ( $module_keys as $key ) { … … 354 373 ), 355 374 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 356 394 ); 357 395 -
cartexa-smart-cart-for-woocommerce/trunk/includes/class-cartexa-settings.php
r3690135 r3734660 3 3 * Settings Manager — MVP v1.0 4 4 * 5 * Single source of truth for all 12Cartexa feature options.5 * Single source of truth for all Cartexa feature options. 6 6 * 7 7 * Feature groups: … … 183 183 'enable_undo_remove' => 'yes', 184 184 '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' => '', 185 195 ); 186 196 } … … 294 304 // Undo Remove. 295 305 'enable_undo_remove', 306 // Save for Later. 307 'enable_save_for_later', 308 // Coupon Box. 309 'enable_coupon_box', 296 310 ); 297 311 foreach ( $toggles as $key ) { … … 348 362 'scarcity_message', 349 363 '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', 350 366 ); 351 367 foreach ( $text_keys as $key ) { -
cartexa-smart-cart-for-woocommerce/trunk/includes/modules/class-cartexa-module-loader.php
r3690135 r3734660 3 3 * Module Loader. 4 4 * 5 * Central registry that boots all 11feature modules5 * Central registry that boots all active feature modules 6 6 * (Side Cart is handled by the core Frontend class). 7 7 * … … 78 78 Toast::instance(); 79 79 80 // v1.0.1: Save for Later. 81 \Cartexa\Module_Save_For_Later::instance()->hooks(); 82 80 83 /** 81 84 * Fires after all modules have been booted. -
cartexa-smart-cart-for-woocommerce/trunk/readme.txt
r3690135 r3734660 1 1 === Cartexa – Smart Cart for WooCommerce === 2 2 Contributors: webtendhq,devhasib 3 Tags: woocommerce , cart, side cart, ajaxcart, mini cart3 Tags: woocommerce cart, side cart, ajax cart, sliding cart, mini cart 4 4 Requires at least: 6.3 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.0. 07 Stable tag: 1.0.1 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 11 11 WC tested up to: 9.9 12 12 13 Smart AJAX cart for WooCommerce — side cart drawer, floating cart, free shipping bar, trust badges, scarcity messages & more. Zero jQuery.13 AJAX side cart for WooCommerce — sliding cart drawer, floating cart, free shipping bar, coupon box, save for later & more. No jQuery dependency. Beginner-friendly. 14 14 15 15 == Description == … … 17 17 **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. 18 18 19 8 independent modules. One lightweight plugin. Works with any theme — zero jQuery, zero page reloads. 19 8 independent modules. One lightweight plugin. Works with any theme — No jQuery dependency, zero page reloads. 20 21 🎥 **See Cartexa in Action:** 22 23 https://youtu.be/5X_ZIH_6OtM?si=dFmSGuFozEJiP_FU 24 25 📖 [View Documentation](https://webtend.net/docs/cartexa-doc.html) | 🌐 [WebTend](https://webtend.net/) 20 26 21 27 = 🛒 Cart Modules = … … 52 58 = ⚡ Performance = 53 59 54 * ** Zero jQuery** — pure vanilla ES6 throughout60 * **No jQuery dependency** — pure vanilla ES6 throughout 55 61 * **Module-level loading** — disabled modules register zero hooks and load zero assets 56 62 * **Smart enqueue** — CSS/JS load only on WooCommerce pages by default … … 117 123 == Changelog == 118 124 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 119 136 = 1.0.0 = 120 137 * Initial release. … … 126 143 * Cart Refresh on Tab Focus — auto-syncs cart state when switching browser tabs. 127 144 * 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. 129 146 * HPOS and Cart/Checkout Blocks compatibility declared. 130 147 * PSR-4 namespaced PHP under Cartexa\, 25+ hooks, theme template overrides. … … 135 152 == Upgrade Notice == 136 153 154 = 1.0.1 = 155 Adds Save for Later and free Coupon Box inside the drawer. Recommended update. 156 157 158 137 159 = 1.0.0 = 138 160 Initial release.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)