Changeset 3488606
- Timestamp:
- 03/23/2026 06:39:37 AM (5 days ago)
- Location:
- cartflush-autoclear-cart-for-inactive-users
- Files:
-
- 29 added
- 5 edited
-
assets/Screenshot-1.png (added)
-
assets/Screenshot-2.png (added)
-
assets/Screenshot-3.png (added)
-
assets/Screenshot-4.png (added)
-
tags/2.1.0 (added)
-
tags/2.1.0/assets (added)
-
tags/2.1.0/assets/banner.png (added)
-
tags/2.1.0/assets/css (added)
-
tags/2.1.0/assets/css/admin.css (added)
-
tags/2.1.0/assets/js (added)
-
tags/2.1.0/assets/js/admin.js (added)
-
tags/2.1.0/assets/logo.png (added)
-
tags/2.1.0/blueprint.json (added)
-
tags/2.1.0/cartflush-autoclear-cart-for-inactive-users.php (added)
-
tags/2.1.0/docs (added)
-
tags/2.1.0/docs/wordpress-org-assets.md (added)
-
tags/2.1.0/includes (added)
-
tags/2.1.0/includes/admin (added)
-
tags/2.1.0/includes/admin/class-cartflush-admin.php (added)
-
tags/2.1.0/includes/class-cartflush-plugin.php (added)
-
tags/2.1.0/includes/class-cartflush-rules.php (added)
-
tags/2.1.0/languages (added)
-
tags/2.1.0/languages/cartflush.pot (added)
-
tags/2.1.0/readme.txt (added)
-
tags/2.1.0/uninstall.php (added)
-
trunk/assets/banner.png (added)
-
trunk/assets/css/admin.css (modified) (2 diffs)
-
trunk/assets/js (added)
-
trunk/assets/js/admin.js (added)
-
trunk/assets/logo.png (added)
-
trunk/cartflush-autoclear-cart-for-inactive-users.php (modified) (2 diffs)
-
trunk/includes/admin/class-cartflush-admin.php (modified) (5 diffs)
-
trunk/includes/class-cartflush-rules.php (modified) (7 diffs)
-
trunk/readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
cartflush-autoclear-cart-for-inactive-users/trunk/assets/css/admin.css
r3484352 r3488606 1 1 .cartflush-admin { 2 max-width: 1120px; 2 --cf-bg: #f1f5f9; 3 --cf-surface: #ffffff; 4 --cf-surface-soft: #f8fafc; 5 --cf-border: #e2e8f0; 6 --cf-border-strong: #cbd5e1; 7 --cf-text: #1e293b; 8 --cf-text-soft: #64748b; 9 --cf-title: #0f172a; 10 --cf-primary: #3b82f6; 11 --cf-primary-deep: #2563eb; 12 --cf-primary-soft: #eff6ff; 13 --cf-success: #0f766e; 14 --cf-danger: #dc2626; 15 --cf-shadow: 0 12px 30px rgba(15, 23, 42, 0.06); 16 max-width: 1340px; 17 padding-bottom: 32px; 18 } 19 20 .cartflush-admin::before { 21 content: ""; 22 position: fixed; 23 inset: 0; 24 z-index: -1; 25 background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%); 26 pointer-events: none; 27 } 28 29 .cartflush-shell { 30 display: grid; 31 gap: 22px; 32 } 33 34 .cartflush-hero, 35 .cartflush-panel, 36 .cartflush-sidecard, 37 .cartflush-summary-panel { 38 border: 1px solid var(--cf-border); 39 border-radius: 20px; 40 background: var(--cf-surface); 41 box-shadow: var(--cf-shadow); 3 42 } 4 43 5 44 .cartflush-hero { 45 display: grid; 46 grid-template-columns: minmax(0, 1.8fr) minmax(280px, 0.8fr); 47 gap: 24px; 48 padding: 28px 30px; 49 } 50 51 .cartflush-eyebrow, 52 .cartflush-chip { 53 display: inline-flex; 54 align-items: center; 55 padding: 6px 12px; 56 border-radius: 999px; 57 font-size: 11px; 58 font-weight: 700; 59 letter-spacing: 0.08em; 60 text-transform: uppercase; 61 } 62 63 .cartflush-eyebrow { 64 background: var(--cf-primary-soft); 65 color: var(--cf-primary-deep); 66 } 67 68 .cartflush-chip { 69 background: #f1f5f9; 70 color: var(--cf-text-soft); 71 } 72 73 .cartflush-chip--blue { 74 background: var(--cf-primary-soft); 75 color: var(--cf-primary-deep); 76 } 77 78 .cartflush-hero h1 { 79 margin: 14px 0 10px; 80 font-size: 34px; 81 line-height: 1.08; 82 letter-spacing: -0.03em; 83 color: var(--cf-title); 84 } 85 86 .cartflush-hero p, 87 .cartflush-rule-card p, 88 .cartflush-panel__intro p, 89 .cartflush-sidecard p, 90 .cartflush-savebar p, 91 .cartflush-summary p { 92 margin-top: 0; 93 color: var(--cf-text-soft); 94 line-height: 1.7; 95 } 96 97 .cartflush-hero__stats { 98 display: grid; 99 grid-template-columns: repeat(4, minmax(0, 1fr)); 100 gap: 14px; 101 margin-top: 24px; 102 } 103 104 .cartflush-stat { 105 padding: 16px 18px; 106 border: 1px solid var(--cf-border); 107 border-radius: 16px; 108 background: var(--cf-surface-soft); 109 } 110 111 .cartflush-stat__label, 112 .cartflush-stat__meta { 113 display: block; 114 font-size: 12px; 115 color: var(--cf-text-soft); 116 } 117 118 .cartflush-stat strong { 119 display: block; 120 margin: 8px 0 6px; 121 font-size: 28px; 122 line-height: 1; 123 letter-spacing: -0.04em; 124 color: var(--cf-title); 125 } 126 127 .cartflush-hero__rail { 128 display: grid; 129 align-content: start; 130 } 131 132 .cartflush-rail-card { 133 padding: 20px; 134 border: 1px solid var(--cf-border); 135 border-radius: 16px; 136 background: var(--cf-surface-soft); 137 } 138 139 .cartflush-rail-card h3 { 140 margin: 14px 0 8px; 141 font-size: 20px; 142 line-height: 1.25; 143 color: var(--cf-title); 144 } 145 146 .cartflush-rail-card p { 147 margin: 0; 148 } 149 150 .cartflush-bullet-list { 151 margin: 14px 0 0; 152 padding-left: 18px; 153 color: var(--cf-text-soft); 154 line-height: 1.8; 155 } 156 157 .cartflush-layout { 158 display: grid; 159 grid-template-columns: minmax(0, 1.9fr) minmax(300px, 0.9fr); 160 gap: 22px; 161 align-items: start; 162 } 163 164 .cartflush-main, 165 .cartflush-sidebar { 166 display: grid; 167 gap: 18px; 168 } 169 170 .cartflush-sidepanel { 171 display: grid; 172 gap: 16px; 173 } 174 175 .cartflush-sidepanel__intro { 176 padding: 8px 4px 0; 177 } 178 179 .cartflush-sidepanel__intro h2 { 180 margin: 10px 0 8px; 181 font-size: 24px; 182 line-height: 1.15; 183 letter-spacing: -0.02em; 184 color: var(--cf-title); 185 } 186 187 .cartflush-sidepanel__intro p { 188 margin: 0; 189 color: var(--cf-text-soft); 190 line-height: 1.7; 191 } 192 193 .cartflush-panel, 194 .cartflush-summary-panel { 195 padding: 28px; 196 } 197 198 .cartflush-panel__intro { 199 margin-bottom: 22px; 200 } 201 202 .cartflush-panel h2, 203 .cartflush-summary-panel h2 { 204 margin: 10px 0 8px; 205 font-size: 24px; 206 line-height: 1.15; 207 letter-spacing: -0.02em; 208 color: var(--cf-title); 209 } 210 211 .cartflush-settings-form { 212 display: grid; 213 gap: 20px; 214 } 215 216 .cartflush-settings-form .form-table { 217 margin: 0; 218 } 219 220 .cartflush-settings-form .form-table th { 221 padding-left: 0; 222 color: var(--cf-title); 223 font-size: 14px; 224 font-weight: 600; 225 } 226 227 .cartflush-settings-form .form-table td { 228 padding-right: 0; 229 } 230 231 .cartflush-field { 232 display: inline-flex; 233 align-items: center; 234 gap: 12px; 235 padding: 12px 14px; 236 border: 1px solid var(--cf-border); 237 border-radius: 14px; 238 background: var(--cf-surface-soft); 239 } 240 241 .cartflush-field input[type="number"] { 242 border: 0; 243 box-shadow: none; 244 background: transparent; 245 font-size: 20px; 246 font-weight: 700; 247 color: var(--cf-title); 248 } 249 250 .cartflush-field span { 251 font-size: 12px; 252 font-weight: 700; 253 color: var(--cf-text-soft); 254 letter-spacing: 0.08em; 255 text-transform: uppercase; 256 } 257 258 .cartflush-rule-grid, 259 .cartflush-summary { 260 display: grid; 261 grid-template-columns: repeat(2, minmax(0, 1fr)); 262 gap: 18px; 263 } 264 265 .cartflush-rule-card { 266 padding: 20px; 267 border: 1px solid var(--cf-border); 268 border-radius: 20px; 269 background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%); 270 } 271 272 .cartflush-rule-card__header { 6 273 display: flex; 7 274 justify-content: space-between; 8 275 align-items: flex-start; 9 gap: 24px; 10 margin: 24px 0; 11 padding: 28px 32px; 12 border-radius: 20px; 13 background: linear-gradient(135deg, #0f172a, #1d4ed8); 14 color: #fff; 15 box-shadow: 0 18px 40px rgba(15, 23, 42, 0.16); 16 } 17 18 .cartflush-hero h1 { 19 margin: 0 0 10px; 20 font-size: 30px; 21 line-height: 1.15; 22 color: #fff; 23 } 24 25 .cartflush-hero p { 26 margin: 0; 27 max-width: 720px; 28 font-size: 14px; 29 line-height: 1.7; 30 color: rgba(255, 255, 255, 0.88); 31 } 32 33 .cartflush-hero__meta { 34 display: flex; 35 flex-wrap: wrap; 36 gap: 10px; 37 } 38 39 .cartflush-hero__meta span { 40 display: inline-flex; 41 align-items: center; 42 padding: 8px 12px; 43 border: 1px solid rgba(255, 255, 255, 0.2); 44 border-radius: 999px; 45 background: rgba(255, 255, 255, 0.12); 46 font-size: 12px; 47 font-weight: 600; 48 letter-spacing: 0.02em; 49 } 50 51 .cartflush-grid { 52 display: grid; 53 grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); 54 gap: 20px; 55 margin-bottom: 20px; 56 } 57 58 .cartflush-card { 59 padding: 24px; 60 border: 1px solid #d0d7de; 61 border-radius: 18px; 62 background: #fff; 63 box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); 64 } 65 66 .cartflush-card--primary { 67 grid-column: span 2; 68 } 69 70 .cartflush-card--wide { 71 margin-top: 8px; 72 } 73 74 .cartflush-card h2 { 75 margin-top: 0; 76 margin-bottom: 10px; 77 font-size: 20px; 78 } 79 80 .cartflush-card h3 { 81 margin-top: 0; 82 margin-bottom: 8px; 83 font-size: 15px; 84 } 85 86 .cartflush-card p { 87 line-height: 1.65; 88 } 89 90 .cartflush-card form { 91 margin-top: 14px; 92 } 93 94 .cartflush-card input[type="file"] { 95 width: 100%; 96 padding: 10px; 97 border: 1px dashed #94a3b8; 98 border-radius: 12px; 99 background: #f8fafc; 100 } 101 102 .cartflush-card .button { 103 margin-top: 14px; 104 } 105 106 .cartflush-field { 276 gap: 14px; 277 margin-bottom: 18px; 278 } 279 280 .cartflush-rule-card__title { 281 display: grid; 282 gap: 8px; 283 } 284 285 .cartflush-rule-card__meta { 107 286 display: inline-flex; 108 287 align-items: center; 109 288 gap: 8px; 110 } 111 112 .cartflush-summary { 113 display: grid; 114 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); 289 width: fit-content; 290 padding: 6px 10px; 291 border: 1px solid var(--cf-border); 292 border-radius: 999px; 293 background: #fff; 294 } 295 296 .cartflush-rule-card__count { 297 font-size: 13px; 298 font-weight: 700; 299 color: var(--cf-title); 300 } 301 302 .cartflush-rule-card__label { 303 font-size: 11px; 304 font-weight: 700; 305 letter-spacing: 0.08em; 306 text-transform: uppercase; 307 color: var(--cf-text-soft); 308 } 309 310 .cartflush-rule-card h3, 311 .cartflush-sidecard h3, 312 .cartflush-summary h3 { 313 margin: 0 0 6px; 314 font-size: 18px; 315 line-height: 1.3; 316 color: var(--cf-title); 317 } 318 319 .cartflush-table-wrap { 320 overflow-x: auto; 321 padding: 2px 0; 322 } 323 324 .cartflush-rule-table { 325 width: 100%; 326 border: 0; 327 box-shadow: none; 328 background: transparent; 329 border-collapse: separate; 330 border-spacing: 0 10px; 331 } 332 333 .cartflush-rule-table thead th { 334 padding: 0 8px 2px 8px; 335 border: 0; 336 background: transparent; 337 font-size: 11px; 338 font-weight: 700; 339 text-transform: uppercase; 340 letter-spacing: 0.08em; 341 color: var(--cf-text-soft); 342 } 343 344 .cartflush-rule-table tbody tr, 345 .cartflush-rule-table.striped > tbody > :nth-child(odd), 346 .cartflush-rule-table.striped > tbody > :nth-child(even) { 347 background: transparent; 348 } 349 350 .cartflush-rule-table tbody td { 351 padding: 10px; 352 border-top: 1px solid var(--cf-border); 353 border-bottom: 1px solid var(--cf-border); 354 background: #fff; 355 vertical-align: middle; 356 } 357 358 .cartflush-rule-table tbody td:first-child { 359 border-left: 1px solid var(--cf-border); 360 border-radius: 14px 0 0 14px; 361 } 362 363 .cartflush-rule-table tbody td:last-child { 364 border-right: 1px solid var(--cf-border); 365 border-radius: 0 14px 14px 0; 366 } 367 368 .cartflush-rule-table select, 369 .cartflush-rule-table input[type="number"], 370 .cartflush-sidecard input[type="file"] { 371 width: 100%; 372 max-width: 100%; 373 min-height: 42px; 374 padding: 0 14px; 375 border: 1px solid var(--cf-border-strong); 376 border-radius: 12px; 377 background: #fff; 378 color: var(--cf-text); 379 } 380 381 .cartflush-sidecard input[type="file"] { 382 padding: 11px 14px; 383 background: #fff; 384 } 385 386 .cartflush-rule-table select:focus, 387 .cartflush-rule-table input[type="number"]:focus, 388 .cartflush-sidecard input[type="file"]:focus, 389 .cartflush-action:focus, 390 .cartflush-savebar__button:focus { 391 border-color: var(--cf-primary); 392 box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.14); 393 outline: 0; 394 } 395 396 .cartflush-rule-table__action, 397 .cartflush-row-action { 398 width: 96px; 399 text-align: right; 400 } 401 402 .cartflush-remove-row { 403 display: inline-flex; 404 align-items: center; 405 justify-content: center; 406 min-height: 34px; 407 padding: 0 10px; 408 border: 1px solid rgba(220, 38, 38, 0.18); 409 border-radius: 999px; 410 background: #fff; 411 color: var(--cf-danger); 412 font-weight: 600; 413 text-decoration: none; 414 } 415 416 .cartflush-remove-row:hover, 417 .cartflush-remove-row:focus { 418 border-color: rgba(220, 38, 38, 0.34); 419 background: #fef2f2; 420 color: #b91c1c; 421 } 422 423 .cartflush-rule-card__hint { 424 margin: 14px 0 0; 425 font-size: 13px; 426 color: var(--cf-text-soft); 427 } 428 429 .cartflush-action, 430 .cartflush-sidecard .button, 431 .cartflush-savebar__button { 432 border-radius: 12px; 433 box-shadow: none; 434 } 435 436 .cartflush-action { 437 margin-top: 0; 438 border-color: var(--cf-border); 439 background: var(--cf-surface); 440 color: var(--cf-title); 441 } 442 443 .cartflush-action:hover, 444 .cartflush-action:focus { 445 border-color: var(--cf-primary); 446 background: var(--cf-primary-soft); 447 color: var(--cf-primary-deep); 448 } 449 450 .cartflush-savebar { 451 display: flex; 452 justify-content: space-between; 453 align-items: center; 115 454 gap: 16px; 455 padding: 18px 20px; 456 border: 1px solid var(--cf-border); 457 border-radius: 18px; 458 background: var(--cf-surface-soft); 459 } 460 461 .cartflush-savebar strong { 462 display: block; 463 margin-bottom: 4px; 464 font-size: 18px; 465 color: var(--cf-title); 466 } 467 468 .cartflush-savebar__button { 469 margin: 0; 470 padding: 0 22px; 471 border-color: var(--cf-primary); 472 background: var(--cf-primary); 473 } 474 475 .cartflush-savebar__button:hover, 476 .cartflush-savebar__button:focus { 477 border-color: var(--cf-primary-deep); 478 background: var(--cf-primary-deep); 479 } 480 481 .cartflush-sidecard { 482 padding: 22px; 483 border-radius: 18px; 484 } 485 486 .cartflush-sidecard__header { 487 display: grid; 488 gap: 10px; 489 margin-bottom: 10px; 490 } 491 492 .cartflush-sidecard__header strong { 493 font-size: 20px; 494 line-height: 1.25; 495 color: var(--cf-title); 496 } 497 498 .cartflush-sidecard form { 499 display: grid; 500 gap: 12px; 501 margin-top: 14px; 502 } 503 504 .cartflush-sidebar form + form { 505 margin-top: 12px; 506 } 507 508 .cartflush-upload-field { 509 display: grid; 510 gap: 8px; 511 padding: 16px; 512 border: 1px dashed var(--cf-border-strong); 513 border-radius: 16px; 514 background: var(--cf-surface-soft); 515 } 516 517 .cartflush-upload-field > span { 518 font-size: 12px; 519 font-weight: 700; 520 letter-spacing: 0.08em; 521 text-transform: uppercase; 522 color: var(--cf-text-soft); 523 } 524 525 .cartflush-sidecard .button { 526 margin-top: 0; 527 justify-self: start; 528 } 529 530 .cartflush-sidecard .button-secondary { 531 border-color: var(--cf-primary); 532 background: #fff; 533 color: var(--cf-primary-deep); 534 } 535 536 .cartflush-sidecard .button-secondary:hover, 537 .cartflush-sidecard .button-secondary:focus { 538 border-color: var(--cf-primary-deep); 539 background: var(--cf-primary-soft); 540 color: var(--cf-primary-deep); 541 } 542 543 .cartflush-code-list { 544 display: flex; 545 flex-wrap: wrap; 546 gap: 8px; 547 margin-top: 14px; 548 } 549 550 .cartflush-code-list code { 551 display: inline-flex; 552 padding: 7px 10px; 553 border: 1px solid var(--cf-border); 554 border-radius: 999px; 555 background: var(--cf-surface-soft); 556 font-size: 12px; 557 color: var(--cf-text-soft); 558 } 559 560 .cartflush-summary-panel { 561 padding: 24px; 116 562 } 117 563 118 564 .cartflush-summary > div { 119 565 padding: 18px; 120 border-radius: 14px; 121 background: #f8fafc; 122 border: 1px solid #e2e8f0; 566 border: 1px solid var(--cf-border); 567 border-radius: 16px; 568 background: var(--cf-surface-soft); 569 } 570 571 .cartflush-summary h3 { 572 font-size: 16px; 123 573 } 124 574 … … 127 577 } 128 578 579 @media (max-width: 1180px) { 580 .cartflush-layout { 581 grid-template-columns: 1fr; 582 } 583 584 .cartflush-hero__stats { 585 grid-template-columns: repeat(2, minmax(0, 1fr)); 586 } 587 } 588 129 589 @media (max-width: 900px) { 130 .cartflush-hero { 590 .cartflush-hero, 591 .cartflush-rule-grid, 592 .cartflush-summary { 593 grid-template-columns: 1fr; 594 } 595 596 .cartflush-rule-card__header, 597 .cartflush-savebar { 131 598 flex-direction: column; 132 599 } 133 600 134 .cartflush-card--primary { 135 grid-column: span 1; 601 .cartflush-action, 602 .cartflush-savebar__button, 603 .cartflush-sidecard .button { 604 width: 100%; 605 justify-content: center; 136 606 } 137 } 607 608 .cartflush-rule-table__action, 609 .cartflush-row-action { 610 width: 100%; 611 text-align: left; 612 } 613 } -
cartflush-autoclear-cart-for-inactive-users/trunk/cartflush-autoclear-cart-for-inactive-users.php
r3484352 r3488606 4 4 * Plugin URI: https://wordpress.org/plugins/cartflush-autoclear-cart-for-inactive-users/ 5 5 * Description: Automatically clears WooCommerce carts after inactivity with configurable default timeouts, import/export tools, and rule-based exclusions. 6 * Version: 2. 0.06 * Version: 2.1.0 7 7 * Requires at least: 5.8 8 8 * Requires PHP: 7.4 … … 21 21 } 22 22 23 define( 'CARTFLUSH_VERSION', ' 1.2.1' );23 define( 'CARTFLUSH_VERSION', '2.1.0' ); 24 24 define( 'CARTFLUSH_FILE', __FILE__ ); 25 25 define( 'CARTFLUSH_PATH', plugin_dir_path( __FILE__ ) ); -
cartflush-autoclear-cart-for-inactive-users/trunk/includes/admin/class-cartflush-admin.php
r3484352 r3488606 19 19 private $rules; 20 20 21 /**22 * Constructor.23 *24 * @param CartFlush_Rules $rules Rules manager.25 */26 21 public function __construct( CartFlush_Rules $rules ) { 27 22 $this->rules = $rules; 28 29 23 add_action( 'admin_menu', [ $this, 'add_settings_page' ] ); 30 24 add_action( 'admin_init', [ $this, 'register_settings' ] ); … … 34 28 add_action( 'admin_post_cartflush_import_csv', [ $this, 'handle_csv_import' ] ); 35 29 add_action( 'admin_post_cartflush_export_json', [ $this, 'handle_json_export' ] ); 36 } 37 38 /** 39 * Add settings page under Settings. 40 * 41 * @return void 42 */ 30 add_filter( 'option_page_capability_cartflush_settings_group', [ $this, 'settings_capability' ] ); 31 } 32 43 33 public function add_settings_page() { 44 add_options_page( 45 __( 'CartFlush Settings', 'cartflush' ), 46 __( 'CartFlush', 'cartflush' ), 47 'manage_options', 48 'cartflush-settings', 49 [ $this, 'settings_page_html' ] 50 ); 51 } 52 53 /** 54 * Register plugin settings. 55 * 56 * @return void 57 */ 34 add_submenu_page( 'woocommerce', __( 'CartFlush Settings', 'cartflush' ), __( 'CartFlush', 'cartflush' ), $this->get_required_capability(), 'cartflush-settings', [ $this, 'settings_page_html' ] ); 35 } 36 58 37 public function register_settings() { 59 register_setting( 60 'cartflush_settings_group', 61 'cartflush_expiration_time', 62 [ 63 'type' => 'integer', 64 'sanitize_callback' => 'absint', 65 'default' => 30, 66 ] 67 ); 68 69 register_setting( 70 'cartflush_settings_group', 71 CartFlush_Rules::OPTION_NAME, 72 [ 73 'type' => 'array', 74 'sanitize_callback' => [ $this, 'sanitize_rules_option' ], 75 'default' => $this->rules->get_default_rules(), 76 ] 77 ); 78 79 add_settings_section( 80 'cartflush_main', 81 __( 'Timeout Settings', 'cartflush' ), 82 [ $this, 'render_main_section' ], 83 'cartflush-settings' 84 ); 85 86 add_settings_field( 87 'cartflush_expiration_time', 88 __( 'Default cart expiration', 'cartflush' ), 89 [ $this, 'render_expiration_field' ], 90 'cartflush-settings', 91 'cartflush_main' 92 ); 93 } 94 95 /** 96 * Sanitize imported rules. 97 * 98 * @param mixed $value Rules payload. 99 * @return array<string, mixed> 100 */ 38 register_setting( 'cartflush_settings_group', 'cartflush_expiration_time', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 30 ] ); 39 register_setting( 'cartflush_settings_group', CartFlush_Rules::OPTION_NAME, [ 'type' => 'array', 'sanitize_callback' => [ $this, 'sanitize_rules_option' ], 'default' => $this->rules->get_default_rules() ] ); 40 add_settings_section( 'cartflush_main', __( 'Timeout Settings', 'cartflush' ), [ $this, 'render_main_section' ], 'cartflush-settings' ); 41 add_settings_field( 'cartflush_expiration_time', __( 'Default cart expiration', 'cartflush' ), [ $this, 'render_expiration_field' ], 'cartflush-settings', 'cartflush_main' ); 42 } 43 101 44 public function sanitize_rules_option( $value ) { 102 return $this->rules->normalize_rules_data( $value ); 103 } 104 105 /** 106 * Enqueue admin assets on the plugin settings page. 107 * 108 * @param string $hook Current admin hook suffix. 109 * @return void 110 */ 45 return $this->rules->normalize_rules_data( is_array( $value ) ? $this->prepare_rules_for_storage( $value ) : $value ); 46 } 47 48 public function settings_capability() { 49 return $this->get_required_capability(); 50 } 51 111 52 public function enqueue_assets( $hook ) { 112 if ( ' settings_page_cartflush-settings' !== $hook ) {53 if ( 'woocommerce_page_cartflush-settings' !== $hook ) { 113 54 return; 114 55 } 115 56 116 wp_enqueue_style( 117 'cartflush-admin', 118 CARTFLUSH_URL . 'assets/css/admin.css', 119 [], 120 CARTFLUSH_VERSION 121 ); 122 } 123 124 /** 125 * Render settings section intro. 126 * 127 * @return void 128 */ 57 wp_enqueue_style( 'cartflush-admin', CARTFLUSH_URL . 'assets/css/admin.css', [], CARTFLUSH_VERSION ); 58 wp_enqueue_script( 'cartflush-admin', CARTFLUSH_URL . 'assets/js/admin.js', [], CARTFLUSH_VERSION, true ); 59 } 60 129 61 public function render_main_section() { 130 echo '<p>' . esc_html__( 'Set a default inactivity window. Imported role and category rules can override it for specific carts.', 'cartflush' ) . '</p>'; 131 } 132 133 /** 134 * Render expiration input. 135 * 136 * @return void 137 */ 62 echo '<p>' . esc_html__( 'Set the fallback timeout first, then add rule-based overrides and exclusions below.', 'cartflush' ) . '</p>'; 63 } 64 138 65 public function render_expiration_field() { 139 66 $value = (int) get_option( 'cartflush_expiration_time', 30 ); 140 echo '<label class="cartflush-field">'; 141 echo '<input type="number" min="1" step="1" class="small-text" name="cartflush_expiration_time" value="' . esc_attr( $value ) . '"> '; 142 echo '<span>' . esc_html__( 'minutes', 'cartflush' ) . '</span>'; 143 echo '</label>'; 144 echo '<p class="description">' . esc_html__( 'This fallback timeout is used when no imported rule matches the current customer or cart contents.', 'cartflush' ) . '</p>'; 145 } 146 147 /** 148 * Output admin notices. 149 * 150 * @return void 151 */ 67 echo '<label class="cartflush-field"><input type="number" min="1" step="1" class="small-text" name="cartflush_expiration_time" value="' . esc_attr( $value ) . '"> <span>' . esc_html__( 'minutes', 'cartflush' ) . '</span></label>'; 68 echo '<p class="description">' . esc_html__( 'Fallback timeout when no custom timeout rule matches.', 'cartflush' ) . '</p>'; 69 } 70 152 71 public function render_admin_notices() { 153 72 if ( ! isset( $_GET['page'] ) || 'cartflush-settings' !== sanitize_key( wp_unslash( $_GET['page'] ) ) ) { 154 73 return; 155 74 } 156 75 if ( ! empty( $_GET['settings-updated'] ) ) { 76 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved.', 'cartflush' ) . '</p></div>'; 77 } 157 78 if ( ! empty( $_GET['cartflush_notice'] ) ) { 158 79 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( wp_unslash( $_GET['cartflush_notice'] ) ) . '</p></div>'; 159 80 } 160 161 81 if ( ! empty( $_GET['cartflush_error'] ) ) { 162 82 echo '<div class="notice notice-error"><p>' . esc_html( wp_unslash( $_GET['cartflush_error'] ) ) . '</p></div>'; … … 164 84 } 165 85 166 /**167 * Import a full JSON settings file.168 *169 * @return void170 */171 86 public function handle_json_import() { 172 87 $this->assert_admin_permissions(); 173 88 check_admin_referer( 'cartflush_import_json' ); 174 175 89 if ( empty( $_FILES['cartflush_json_file']['tmp_name'] ) ) { 176 90 $this->redirect_with_message( '', __( 'Please choose a JSON file to import.', 'cartflush' ) ); 177 91 } 178 179 $raw = file_get_contents( $_FILES['cartflush_json_file']['tmp_name'] ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown 92 $raw = file_get_contents( $_FILES['cartflush_json_file']['tmp_name'] ); // phpcs:ignore 180 93 $data = json_decode( $raw, true ); 181 182 94 if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) { 183 95 $this->redirect_with_message( '', __( 'The uploaded JSON file is invalid.', 'cartflush' ) ); 184 96 } 185 186 97 $default_timeout = isset( $data['cartflush_expiration_time'] ) ? absint( $data['cartflush_expiration_time'] ) : get_option( 'cartflush_expiration_time', 30 ); 187 98 $rules = isset( $data['import_rules'] ) ? $data['import_rules'] : $data; 188 $normalized = $this->rules->normalize_rules_data( $rules );189 190 99 update_option( 'cartflush_expiration_time', max( 1, $default_timeout ) ); 191 update_option( CartFlush_Rules::OPTION_NAME, $normalized ); 192 100 update_option( CartFlush_Rules::OPTION_NAME, $this->rules->normalize_rules_data( $rules ) ); 193 101 $this->redirect_with_message( __( 'JSON settings imported successfully.', 'cartflush' ) ); 194 102 } 195 103 196 /**197 * Import timeout and exclusion rules from CSV.198 *199 * @return void200 */201 104 public function handle_csv_import() { 202 105 $this->assert_admin_permissions(); 203 106 check_admin_referer( 'cartflush_import_csv' ); 204 205 107 if ( empty( $_FILES['cartflush_csv_file']['tmp_name'] ) ) { 206 108 $this->redirect_with_message( '', __( 'Please choose a CSV file to import.', 'cartflush' ) ); 207 109 } 208 209 $handle = fopen( $_FILES['cartflush_csv_file']['tmp_name'], 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen,WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown 210 110 $handle = fopen( $_FILES['cartflush_csv_file']['tmp_name'], 'r' ); // phpcs:ignore 211 111 if ( ! $handle ) { 212 112 $this->redirect_with_message( '', __( 'The uploaded CSV file could not be read.', 'cartflush' ) ); 213 113 } 214 215 114 $header = fgetcsv( $handle ); 216 217 115 if ( ! is_array( $header ) ) { 218 fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose116 fclose( $handle ); // phpcs:ignore 219 117 $this->redirect_with_message( '', __( 'The CSV file is empty.', 'cartflush' ) ); 220 118 } 221 222 119 $header = array_map( 'sanitize_key', $header ); 223 120 $rules = $this->rules->get_rules_option(); 224 225 121 while ( false !== ( $row = fgetcsv( $handle ) ) ) { 226 $row = array_pad( $row, count( $header ), '' ); 227 $row = array_combine( $header, $row ); 228 122 $row = array_combine( $header, array_pad( $row, count( $header ), '' ) ); 229 123 if ( ! is_array( $row ) ) { 230 124 continue; 231 125 } 232 233 126 $type = isset( $row['type'] ) ? sanitize_key( $row['type'] ) : ''; 234 $key = isset( $row['key'] ) ? $row['key']: '';127 $key = isset( $row['key'] ) ? trim( (string) $row['key'] ) : ''; 235 128 $timeout = isset( $row['timeout_minutes'] ) ? absint( $row['timeout_minutes'] ) : 0; 236 237 if ( ! $type || '' === trim( $key ) ) { 129 if ( ! $type || '' === $key ) { 238 130 continue; 239 131 } 240 241 132 switch ( $type ) { 133 case 'customer_type': 134 if ( in_array( sanitize_key( $key ), [ 'guest', 'logged_in' ], true ) && $timeout > 0 ) { 135 $rules['customer_type_rules'][ sanitize_key( $key ) ] = $timeout; 136 } 137 break; 242 138 case 'role': 243 139 if ( $timeout > 0 ) { … … 245 141 } 246 142 break; 247 143 case 'product_rule': 144 if ( absint( $key ) > 0 && $timeout > 0 ) { 145 $rules['product_rules'][ absint( $key ) ] = $timeout; 146 } 147 break; 248 148 case 'category': 249 149 if ( $timeout > 0 ) { … … 251 151 } 252 152 break; 253 153 case 'tag': 154 if ( $timeout > 0 ) { 155 $rules['tag_rules'][ sanitize_title( $key ) ] = $timeout; 156 } 157 break; 158 case 'excluded_role': 159 $rules['excluded_roles'][] = sanitize_key( $key ); 160 break; 254 161 case 'excluded_product': 255 162 case 'product': 256 $product_id = absint( $key ); 257 if ( $product_id > 0 ) { 258 $rules['excluded_products'][] = $product_id; 163 if ( absint( $key ) > 0 ) { 164 $rules['excluded_products'][] = absint( $key ); 259 165 } 260 166 break; 261 262 167 case 'excluded_category': 263 168 $rules['excluded_categories'][] = sanitize_title( $key ); 264 169 break; 170 case 'excluded_tag': 171 $rules['excluded_tags'][] = sanitize_title( $key ); 172 break; 265 173 } 266 174 } 267 268 fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 269 175 fclose( $handle ); // phpcs:ignore 270 176 update_option( CartFlush_Rules::OPTION_NAME, $this->rules->normalize_rules_data( $rules ) ); 271 272 177 $this->redirect_with_message( __( 'CSV rules imported successfully.', 'cartflush' ) ); 273 178 } 274 179 275 /**276 * Export JSON settings.277 *278 * @return void279 */280 180 public function handle_json_export() { 281 181 $this->assert_admin_permissions(); 282 182 check_admin_referer( 'cartflush_export_json' ); 283 284 $payload = [ 285 'plugin' => 'CartFlush', 286 'version' => CARTFLUSH_VERSION, 287 'exported_at' => gmdate( 'c' ), 288 'cartflush_expiration_time' => (int) get_option( 'cartflush_expiration_time', 30 ), 289 'import_rules' => $this->rules->get_rules_option(), 290 ]; 291 183 $payload = [ 'plugin' => 'CartFlush', 'version' => CARTFLUSH_VERSION, 'exported_at' => gmdate( 'c' ), 'cartflush_expiration_time' => (int) get_option( 'cartflush_expiration_time', 30 ), 'import_rules' => $this->rules->get_rules_option() ]; 292 184 nocache_headers(); 293 185 header( 'Content-Type: application/json; charset=utf-8' ); 294 186 header( 'Content-Disposition: attachment; filename=cartflush-settings-' . gmdate( 'Y-m-d' ) . '.json' ); 295 296 187 echo wp_json_encode( $payload, JSON_PRETTY_PRINT ); 297 188 exit; 298 189 } 299 190 300 /**301 * Render the admin page.302 *303 * @return void304 */305 191 public function settings_page_html() { 306 $rules = $this->rules->get_rules_option(); 192 $rules = $this->rules->get_rules_option(); 193 $roles = $this->get_role_options(); 194 $customer_types = [ 'guest' => __( 'Guest', 'cartflush' ), 'logged_in' => __( 'Logged-in', 'cartflush' ) ]; 195 $categories = $this->get_taxonomy_options( 'product_cat' ); 196 $tags = $this->get_taxonomy_options( 'product_tag' ); 307 197 ?> 308 198 <div class="wrap cartflush-admin"> 309 <div class="cartflush-hero"> 310 <div> 311 <h1><?php esc_html_e( 'CartFlush Settings', 'cartflush' ); ?></h1> 312 <p><?php esc_html_e( 'Protect your WooCommerce experience with rule-based cart cleanup, import/export tools, and product-aware exclusions.', 'cartflush' ); ?></p> 199 <div class="cartflush-shell"> 200 <section class="cartflush-hero"> 201 <div class="cartflush-hero__content"> 202 <span class="cartflush-eyebrow"><?php esc_html_e( 'WooCommerce Settings', 'cartflush' ); ?></span> 203 <h1><?php esc_html_e( 'CartFlush', 'cartflush' ); ?></h1> 204 <p><?php esc_html_e( 'Configure automatic cart clearing with layered timeout rules, product conditions, and exclusions from one settings page.', 'cartflush' ); ?></p> 205 <div class="cartflush-hero__stats"> 206 <?php foreach ( $this->get_stats( $rules ) as $stat ) : ?> 207 <div class="cartflush-stat"> 208 <span class="cartflush-stat__label"><?php echo esc_html( $stat['label'] ); ?></span> 209 <strong><?php echo esc_html( $stat['value'] ); ?></strong> 210 <span class="cartflush-stat__meta"><?php echo esc_html( $stat['meta'] ); ?></span> 211 </div> 212 <?php endforeach; ?> 213 </div> 214 </div> 215 <div class="cartflush-hero__rail"> 216 <div class="cartflush-rail-card"> 217 <span class="cartflush-chip cartflush-chip--blue"><?php esc_html_e( 'How It Works', 'cartflush' ); ?></span> 218 <h3><?php esc_html_e( 'Build rules directly from this screen', 'cartflush' ); ?></h3> 219 <p><?php esc_html_e( 'Use a default timeout, layer rule-based overrides, then add exclusions for carts that should never be cleared automatically.', 'cartflush' ); ?></p> 220 <ul class="cartflush-bullet-list"> 221 <li><?php esc_html_e( 'Customer type and role timeouts', 'cartflush' ); ?></li> 222 <li><?php esc_html_e( 'Product, category, and tag rules', 'cartflush' ); ?></li> 223 <li><?php esc_html_e( 'Role, product, category, and tag exclusions', 'cartflush' ); ?></li> 224 </ul> 225 </div> 226 </div> 227 </section> 228 229 <div class="cartflush-layout"> 230 <main class="cartflush-main"> 231 <form method="post" action="options.php" class="cartflush-settings-form"> 232 <section class="cartflush-panel"> 233 <div class="cartflush-panel__intro"> 234 <div> 235 <span class="cartflush-chip"><?php esc_html_e( 'General', 'cartflush' ); ?></span> 236 <h2><?php esc_html_e( 'General Settings', 'cartflush' ); ?></h2> 237 <p><?php esc_html_e( 'Choose the fallback timeout used when no more specific rule applies.', 'cartflush' ); ?></p> 238 </div> 239 </div> 240 <?php settings_fields( 'cartflush_settings_group' ); do_settings_sections( 'cartflush-settings' ); ?> 241 </section> 242 243 <section class="cartflush-panel"> 244 <div class="cartflush-panel__intro"> 245 <div> 246 <span class="cartflush-chip"><?php esc_html_e( 'Timeout Rules', 'cartflush' ); ?></span> 247 <h2><?php esc_html_e( 'Rule-Based Timeouts', 'cartflush' ); ?></h2> 248 <p><?php esc_html_e( 'Apply shorter or longer cart expiration windows based on the customer or the products in the cart.', 'cartflush' ); ?></p> 249 </div> 250 </div> 251 <div class="cartflush-rule-grid"> 252 <?php 253 $this->render_rule_card( __( 'Customer Type Rules', 'cartflush' ), __( 'Set separate timeouts for guest and logged-in customers.', 'cartflush' ), 'customer-type', __( 'Add Rule', 'cartflush' ), [ __( 'Customer Type', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['customer_type_rules'] ), __( 'Use this when guest carts and account carts should expire differently.', 'cartflush' ), function() use ( $rules, $customer_types ) { $this->render_customer_type_rows( $rules['customer_type_rules'], $customer_types ); } ); 254 $this->render_rule_card( __( 'Role Rules', 'cartflush' ), __( 'Override the default timeout for specific user roles.', 'cartflush' ), 'role-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Role', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['role_rules'] ), __( 'Perfect for wholesale, shop manager, or VIP-specific behavior.', 'cartflush' ), function() use ( $rules, $roles ) { $this->render_map_timeout_rows( 'role_rules', 'role', $rules['role_rules'], $roles, __( 'Select a role', 'cartflush' ) ); } ); 255 $this->render_rule_card( __( 'Product Rules', 'cartflush' ), __( 'Set a timeout for a single WooCommerce product ID.', 'cartflush' ), 'product-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Product ID', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['product_rules'] ), __( 'Use product-level rules when one item needs a special cart lifetime.', 'cartflush' ), function() use ( $rules ) { $this->render_product_timeout_rows( 'product_rules', $rules['product_rules'] ); } ); 256 $this->render_rule_card( __( 'Category Rules', 'cartflush' ), __( 'Apply timeouts using WooCommerce product categories.', 'cartflush' ), 'category-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Category', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['category_rules'] ), __( 'Great for grouping timeouts across collections instead of product by product.', 'cartflush' ), function() use ( $rules, $categories ) { $this->render_map_timeout_rows( 'category_rules', 'slug', $rules['category_rules'], $categories, __( 'Select a category', 'cartflush' ) ); } ); 257 $this->render_rule_card( __( 'Tag Rules', 'cartflush' ), __( 'Apply timeouts using WooCommerce product tags.', 'cartflush' ), 'tag-rule', __( 'Add Rule', 'cartflush' ), [ __( 'Tag', 'cartflush' ), __( 'Timeout', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['tag_rules'] ), __( 'Helpful for campaign, seasonal, or merchandising tag-based rules.', 'cartflush' ), function() use ( $rules, $tags ) { $this->render_map_timeout_rows( 'tag_rules', 'slug', $rules['tag_rules'], $tags, __( 'Select a tag', 'cartflush' ) ); } ); 258 ?> 259 </div> 260 </section> 261 262 <section class="cartflush-panel"> 263 <div class="cartflush-panel__intro"> 264 <div> 265 <span class="cartflush-chip"><?php esc_html_e( 'Exclusions', 'cartflush' ); ?></span> 266 <h2><?php esc_html_e( 'Exclusion Rules', 'cartflush' ); ?></h2> 267 <p><?php esc_html_e( 'Skip the auto-clear behavior completely when a cart matches one of the following conditions.', 'cartflush' ); ?></p> 268 </div> 269 </div> 270 <div class="cartflush-rule-grid"> 271 <?php 272 $this->render_rule_card( __( 'Excluded Roles', 'cartflush' ), __( 'Exclude selected user roles from cart clearing.', 'cartflush' ), 'excluded-role', __( 'Add Exclusion', 'cartflush' ), [ __( 'Role', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['excluded_roles'] ), __( 'These roles will always bypass CartFlush regardless of timeout rules.', 'cartflush' ), function() use ( $rules, $roles ) { $this->render_simple_select_rows( 'excluded_roles', 'role', $rules['excluded_roles'], $roles, __( 'Select a role', 'cartflush' ) ); } ); 273 $this->render_rule_card( __( 'Excluded Products', 'cartflush' ), __( 'Exclude carts that contain any of these products.', 'cartflush' ), 'excluded-product', __( 'Add Exclusion', 'cartflush' ), [ __( 'Product ID', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['excluded_products'] ), __( 'Useful for subscriptions, booking items, or products needing longer retention.', 'cartflush' ), function() use ( $rules ) { $this->render_simple_number_rows( 'excluded_products', 'product_id', $rules['excluded_products'] ); } ); 274 $this->render_rule_card( __( 'Excluded Categories', 'cartflush' ), __( 'Exclude carts that contain products in these categories.', 'cartflush' ), 'excluded-category', __( 'Add Exclusion', 'cartflush' ), [ __( 'Category', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['excluded_categories'] ), __( 'Protect full product collections from the auto-clear workflow.', 'cartflush' ), function() use ( $rules, $categories ) { $this->render_simple_select_rows( 'excluded_categories', 'slug', $rules['excluded_categories'], $categories, __( 'Select a category', 'cartflush' ) ); } ); 275 $this->render_rule_card( __( 'Excluded Tags', 'cartflush' ), __( 'Exclude carts that contain products with these tags.', 'cartflush' ), 'excluded-tag', __( 'Add Exclusion', 'cartflush' ), [ __( 'Tag', 'cartflush' ), __( 'Remove', 'cartflush' ) ], count( $rules['excluded_tags'] ), __( 'Best for campaigns or fulfillment groups that should never be auto-cleared.', 'cartflush' ), function() use ( $rules, $tags ) { $this->render_simple_select_rows( 'excluded_tags', 'slug', $rules['excluded_tags'], $tags, __( 'Select a tag', 'cartflush' ) ); } ); 276 ?> 277 </div> 278 </section> 279 280 <div class="cartflush-savebar"> 281 <div> 282 <strong><?php esc_html_e( 'Save changes', 'cartflush' ); ?></strong> 283 <p><?php esc_html_e( 'Your new rules will be used immediately for future inactivity checks.', 'cartflush' ); ?></p> 284 </div> 285 <?php submit_button( __( 'Save Settings', 'cartflush' ), 'primary cartflush-savebar__button', 'submit', false ); ?> 286 </div> 287 </form> 288 </main> 289 290 <aside class="cartflush-sidebar"> 291 <div class="cartflush-sidepanel"> 292 <div class="cartflush-sidepanel__intro"> 293 <span class="cartflush-chip"><?php esc_html_e( 'Tools', 'cartflush' ); ?></span> 294 <h2><?php esc_html_e( 'Import & Export', 'cartflush' ); ?></h2> 295 <p><?php esc_html_e( 'Bring settings in from another store or create a backup before making large rule changes.', 'cartflush' ); ?></p> 296 </div> 297 298 <div class="cartflush-sidecard"> 299 <div class="cartflush-sidecard__header"> 300 <span class="cartflush-chip"><?php esc_html_e( 'CSV Import', 'cartflush' ); ?></span> 301 <strong><?php esc_html_e( 'Bulk Rules', 'cartflush' ); ?></strong> 302 </div> 303 <p><?php esc_html_e( 'Upload a CSV using the headers type, key, timeout_minutes. Exclusion rows can keep timeout_minutes empty.', 'cartflush' ); ?></p> 304 <div class="cartflush-code-list"> 305 <code>customer_type</code> 306 <code>role</code> 307 <code>product_rule</code> 308 <code>category</code> 309 <code>tag</code> 310 </div> 311 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data"> 312 <?php wp_nonce_field( 'cartflush_import_csv' ); ?> 313 <input type="hidden" name="action" value="cartflush_import_csv"> 314 <label class="cartflush-upload-field"> 315 <span><?php esc_html_e( 'Choose CSV file', 'cartflush' ); ?></span> 316 <input type="file" name="cartflush_csv_file" accept=".csv,text/csv" required> 317 </label> 318 <?php submit_button( __( 'Import CSV', 'cartflush' ), 'secondary', 'submit', false ); ?> 319 </form> 320 </div> 321 322 <div class="cartflush-sidecard"> 323 <div class="cartflush-sidecard__header"> 324 <span class="cartflush-chip"><?php esc_html_e( 'JSON Import', 'cartflush' ); ?></span> 325 <strong><?php esc_html_e( 'Move Settings', 'cartflush' ); ?></strong> 326 </div> 327 <p><?php esc_html_e( 'Import a full CartFlush configuration from another WooCommerce store.', 'cartflush' ); ?></p> 328 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data"> 329 <?php wp_nonce_field( 'cartflush_import_json' ); ?> 330 <input type="hidden" name="action" value="cartflush_import_json"> 331 <label class="cartflush-upload-field"> 332 <span><?php esc_html_e( 'Choose JSON file', 'cartflush' ); ?></span> 333 <input type="file" name="cartflush_json_file" accept=".json,application/json" required> 334 </label> 335 <?php submit_button( __( 'Import JSON', 'cartflush' ), 'secondary', 'submit', false ); ?> 336 </form> 337 </div> 338 339 <div class="cartflush-sidecard"> 340 <div class="cartflush-sidecard__header"> 341 <span class="cartflush-chip"><?php esc_html_e( 'JSON Export', 'cartflush' ); ?></span> 342 <strong><?php esc_html_e( 'Backup', 'cartflush' ); ?></strong> 343 </div> 344 <p><?php esc_html_e( 'Download the current timeout and every saved rule group as a portable JSON backup.', 'cartflush' ); ?></p> 345 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"> 346 <?php wp_nonce_field( 'cartflush_export_json' ); ?> 347 <input type="hidden" name="action" value="cartflush_export_json"> 348 <?php submit_button( __( 'Export JSON', 'cartflush' ), 'secondary', 'submit', false ); ?> 349 </form> 350 </div> 351 </div> 352 </aside> 313 353 </div> 314 <div class="cartflush-hero__meta"> 315 <span><?php esc_html_e( 'Version', 'cartflush' ); ?> <?php echo esc_html( CARTFLUSH_VERSION ); ?></span> 316 <span><?php esc_html_e( 'Settings', 'cartflush' ); ?></span> 317 </div> 354 355 <section class="cartflush-summary-panel"> 356 <div class="cartflush-panel__intro"> 357 <div> 358 <span class="cartflush-chip"><?php esc_html_e( 'Overview', 'cartflush' ); ?></span> 359 <h2><?php esc_html_e( 'Saved Configuration', 'cartflush' ); ?></h2> 360 </div> 361 </div> 362 <div class="cartflush-summary"> 363 <?php foreach ( $this->get_summary_items( $rules ) as $summary ) : ?> 364 <div><h3><?php echo esc_html( $summary['label'] ); ?></h3><p><?php echo esc_html( $summary['value'] ); ?></p></div> 365 <?php endforeach; ?> 366 </div> 367 </section> 318 368 </div> 319 369 320 <div class="cartflush-grid"> 321 <div class="cartflush-card cartflush-card--primary"> 322 <h2><?php esc_html_e( 'Default Timeout', 'cartflush' ); ?></h2> 323 <form method="post" action="options.php"> 324 <?php 325 settings_fields( 'cartflush_settings_group' ); 326 do_settings_sections( 'cartflush-settings' ); 327 submit_button( __( 'Save Settings', 'cartflush' ) ); 328 ?> 329 </form> 330 </div> 331 332 <div class="cartflush-card"> 333 <h2><?php esc_html_e( 'Import Full Settings', 'cartflush' ); ?></h2> 334 <p><?php esc_html_e( 'Upload a JSON export to move CartFlush settings between sites.', 'cartflush' ); ?></p> 335 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data"> 336 <?php wp_nonce_field( 'cartflush_import_json' ); ?> 337 <input type="hidden" name="action" value="cartflush_import_json"> 338 <input type="file" name="cartflush_json_file" accept=".json,application/json" required> 339 <?php submit_button( __( 'Import JSON', 'cartflush' ), 'secondary', 'submit', false ); ?> 340 </form> 341 </div> 342 343 <div class="cartflush-card"> 344 <h2><?php esc_html_e( 'Import Rules from CSV', 'cartflush' ); ?></h2> 345 <p><?php esc_html_e( 'Use type, key, and timeout_minutes columns to import role rules, category rules, or exclusions.', 'cartflush' ); ?></p> 346 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data"> 347 <?php wp_nonce_field( 'cartflush_import_csv' ); ?> 348 <input type="hidden" name="action" value="cartflush_import_csv"> 349 <input type="file" name="cartflush_csv_file" accept=".csv,text/csv" required> 350 <?php submit_button( __( 'Import CSV', 'cartflush' ), 'secondary', 'submit', false ); ?> 351 </form> 352 <p class="description"><code>role,customer,30</code> <code>category,subscription-box,10</code> <code>excluded_product,123,</code></p> 353 </div> 354 355 <div class="cartflush-card"> 356 <h2><?php esc_html_e( 'Export Settings', 'cartflush' ); ?></h2> 357 <p><?php esc_html_e( 'Download the current timeout, imported rules, and exclusions as a JSON snapshot.', 'cartflush' ); ?></p> 358 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"> 359 <?php wp_nonce_field( 'cartflush_export_json' ); ?> 360 <input type="hidden" name="action" value="cartflush_export_json"> 361 <?php submit_button( __( 'Export JSON', 'cartflush' ), 'secondary', 'submit', false ); ?> 362 </form> 363 </div> 364 </div> 365 366 <div class="cartflush-card cartflush-card--wide"> 367 <h2><?php esc_html_e( 'Imported Configuration Summary', 'cartflush' ); ?></h2> 368 <div class="cartflush-summary"> 369 <div> 370 <h3><?php esc_html_e( 'Role Rules', 'cartflush' ); ?></h3> 371 <p><?php echo esc_html( $this->format_assoc_list( $rules['role_rules'] ) ); ?></p> 372 </div> 373 <div> 374 <h3><?php esc_html_e( 'Category Rules', 'cartflush' ); ?></h3> 375 <p><?php echo esc_html( $this->format_assoc_list( $rules['category_rules'] ) ); ?></p> 376 </div> 377 <div> 378 <h3><?php esc_html_e( 'Excluded Product IDs', 'cartflush' ); ?></h3> 379 <p><?php echo esc_html( $this->format_simple_list( $rules['excluded_products'] ) ); ?></p> 380 </div> 381 <div> 382 <h3><?php esc_html_e( 'Excluded Category Slugs', 'cartflush' ); ?></h3> 383 <p><?php echo esc_html( $this->format_simple_list( $rules['excluded_categories'] ) ); ?></p> 384 </div> 385 </div> 386 </div> 370 <?php $this->render_templates( $roles, $customer_types, $categories, $tags ); ?> 387 371 </div> 388 372 <?php 389 373 } 390 374 391 /** 392 * Format key-value lists for display. 393 * 394 * @param array<string, int> $items Display items. 395 * @return string 396 */ 375 private function render_rule_card( $title, $description, $type, $button_label, $headers, $count, $hint, $renderer ) { 376 ?> 377 <div class="cartflush-rule-card"> 378 <div class="cartflush-rule-card__header"> 379 <div class="cartflush-rule-card__title"> 380 <div class="cartflush-rule-card__meta"> 381 <span class="cartflush-rule-card__count"><?php echo esc_html( absint( $count ) ); ?></span> 382 <span class="cartflush-rule-card__label"><?php esc_html_e( 'saved', 'cartflush' ); ?></span> 383 </div> 384 <h3><?php echo esc_html( $title ); ?></h3> 385 <p><?php echo esc_html( $description ); ?></p> 386 </div> 387 <button type="button" class="button cartflush-action" data-cartflush-add-row="<?php echo esc_attr( $type ); ?>"><?php echo esc_html( $button_label ); ?></button> 388 </div> 389 <div class="cartflush-table-wrap"> 390 <table class="widefat striped cartflush-rule-table"> 391 <thead><tr><?php foreach ( $headers as $index => $header ) : ?><th class="<?php echo esc_attr( $index === count( $headers ) - 1 ? 'cartflush-rule-table__action' : '' ); ?>"><?php echo esc_html( $header ); ?></th><?php endforeach; ?></tr></thead> 392 <tbody data-cartflush-rows="<?php echo esc_attr( $type ); ?>"><?php call_user_func( $renderer ); ?></tbody> 393 </table> 394 </div> 395 <p class="cartflush-rule-card__hint"><?php echo esc_html( $hint ); ?></p> 396 </div> 397 <?php 398 } 399 400 private function render_templates( $roles, $customer_types, $categories, $tags ) { 401 ?> 402 <script type="text/html" id="tmpl-cartflush-customer-type"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[customer_type_rules][{{index}}][type]', '', $customer_types, __( 'Select customer type', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[customer_type_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 403 <script type="text/html" id="tmpl-cartflush-role-rule"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[role_rules][{{index}}][role]', '', $roles, __( 'Select a role', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[role_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 404 <script type="text/html" id="tmpl-cartflush-product-rule"><tr><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[product_rules][{{index}}][product_id]" value=""></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[product_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 405 <script type="text/html" id="tmpl-cartflush-category-rule"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[category_rules][{{index}}][slug]', '', $categories, __( 'Select a category', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[category_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 406 <script type="text/html" id="tmpl-cartflush-tag-rule"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[tag_rules][{{index}}][slug]', '', $tags, __( 'Select a tag', 'cartflush' ) ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[tag_rules][{{index}}][timeout]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 407 <script type="text/html" id="tmpl-cartflush-excluded-role"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[excluded_roles][{{index}}][role]', '', $roles, __( 'Select a role', 'cartflush' ) ); ?></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 408 <script type="text/html" id="tmpl-cartflush-excluded-product"><tr><td><input type="number" min="1" step="1" class="small-text" name="cartflush_import_rules[excluded_products][{{index}}][product_id]" value=""></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 409 <script type="text/html" id="tmpl-cartflush-excluded-category"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[excluded_categories][{{index}}][slug]', '', $categories, __( 'Select a category', 'cartflush' ) ); ?></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 410 <script type="text/html" id="tmpl-cartflush-excluded-tag"><tr><td><?php $this->render_select_field( 'cartflush_import_rules[excluded_tags][{{index}}][slug]', '', $tags, __( 'Select a tag', 'cartflush' ) ); ?></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr></script> 411 <?php 412 } 413 414 private function render_customer_type_rows( $items, $options ) { 415 if ( empty( $items ) ) { echo $this->get_select_timeout_row( 'customer_type_rules', 0, 'type', '', '', $options, __( 'Select customer type', 'cartflush' ) ); return; } // phpcs:ignore 416 $index = 0; foreach ( $items as $key => $timeout ) { echo $this->get_select_timeout_row( 'customer_type_rules', $index, 'type', $key, $timeout, $options, __( 'Select customer type', 'cartflush' ) ); ++$index; } // phpcs:ignore 417 } 418 419 private function render_map_timeout_rows( $group, $field, $items, $options, $placeholder ) { 420 if ( empty( $items ) ) { echo $this->get_select_timeout_row( $group, 0, $field, '', '', $options, $placeholder ); return; } // phpcs:ignore 421 $index = 0; foreach ( $items as $key => $timeout ) { echo $this->get_select_timeout_row( $group, $index, $field, $key, $timeout, $options, $placeholder ); ++$index; } // phpcs:ignore 422 } 423 424 private function render_product_timeout_rows( $group, $items ) { 425 if ( empty( $items ) ) { echo $this->get_number_timeout_row( $group, 0, '', '' ); return; } // phpcs:ignore 426 $index = 0; foreach ( $items as $key => $timeout ) { echo $this->get_number_timeout_row( $group, $index, $key, $timeout ); ++$index; } // phpcs:ignore 427 } 428 429 private function render_simple_select_rows( $group, $field, $items, $options, $placeholder ) { 430 if ( empty( $items ) ) { echo $this->get_simple_select_row( $group, 0, $field, '', $options, $placeholder ); return; } // phpcs:ignore 431 foreach ( array_values( $items ) as $index => $item ) { echo $this->get_simple_select_row( $group, $index, $field, $item, $options, $placeholder ); } // phpcs:ignore 432 } 433 434 private function render_simple_number_rows( $group, $field, $items ) { 435 if ( empty( $items ) ) { echo $this->get_simple_number_row( $group, 0, $field, '' ); return; } // phpcs:ignore 436 foreach ( array_values( $items ) as $index => $item ) { echo $this->get_simple_number_row( $group, $index, $field, $item ); } // phpcs:ignore 437 } 438 439 private function get_select_timeout_row( $group, $index, $field, $value, $timeout, $options, $placeholder ) { 440 ob_start(); ?><tr><td><?php $this->render_select_field( 'cartflush_import_rules[' . $group . '][' . $index . '][' . $field . ']', (string) $value, $options, $placeholder ); ?></td><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][timeout]' ); ?>" value="<?php echo esc_attr( $timeout ); ?>"></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr><?php return (string) ob_get_clean(); 441 } 442 443 private function get_number_timeout_row( $group, $index, $value, $timeout ) { 444 ob_start(); ?><tr><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][product_id]' ); ?>" value="<?php echo esc_attr( $value ); ?>"></td><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][timeout]' ); ?>" value="<?php echo esc_attr( $timeout ); ?>"></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr><?php return (string) ob_get_clean(); 445 } 446 447 private function get_simple_select_row( $group, $index, $field, $value, $options, $placeholder ) { 448 ob_start(); ?><tr><td><?php $this->render_select_field( 'cartflush_import_rules[' . $group . '][' . $index . '][' . $field . ']', (string) $value, $options, $placeholder ); ?></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr><?php return (string) ob_get_clean(); 449 } 450 451 private function get_simple_number_row( $group, $index, $field, $value ) { 452 ob_start(); ?><tr><td><input type="number" min="1" step="1" class="small-text" name="<?php echo esc_attr( 'cartflush_import_rules[' . $group . '][' . $index . '][' . $field . ']' ); ?>" value="<?php echo esc_attr( $value ); ?>"></td><td class="cartflush-row-action"><?php $this->render_remove_button(); ?></td></tr><?php return (string) ob_get_clean(); 453 } 454 455 private function render_select_field( $name, $selected, $options, $placeholder ) { 456 $options = is_array( $options ) ? $options : []; 457 if ( $selected && ! isset( $options[ $selected ] ) ) { $options = [ $selected => $selected ] + $options; } 458 ?><select class="regular-text" name="<?php echo esc_attr( $name ); ?>"><option value=""><?php echo esc_html( $placeholder ); ?></option><?php foreach ( $options as $value => $label ) : ?><option value="<?php echo esc_attr( $value ); ?>" <?php selected( $selected, (string) $value ); ?>><?php echo esc_html( $label ); ?></option><?php endforeach; ?></select><?php 459 } 460 461 private function render_remove_button() { 462 echo '<button type="button" class="button-link-delete cartflush-remove-row">' . esc_html__( 'Remove', 'cartflush' ) . '</button>'; 463 } 464 465 private function prepare_rules_for_storage( $value ) { 466 return [ 467 'customer_type_rules' => $this->prepare_timeout_rows( isset( $value['customer_type_rules'] ) ? $value['customer_type_rules'] : [], 'type', 'sanitize_key' ), 468 'role_rules' => $this->prepare_timeout_rows( isset( $value['role_rules'] ) ? $value['role_rules'] : [], 'role', 'sanitize_key' ), 469 'product_rules' => $this->prepare_integer_timeout_rows( isset( $value['product_rules'] ) ? $value['product_rules'] : [], 'product_id' ), 470 'category_rules' => $this->prepare_timeout_rows( isset( $value['category_rules'] ) ? $value['category_rules'] : [], 'slug', 'sanitize_title' ), 471 'tag_rules' => $this->prepare_timeout_rows( isset( $value['tag_rules'] ) ? $value['tag_rules'] : [], 'slug', 'sanitize_title' ), 472 'excluded_roles' => $this->prepare_string_list_rows( isset( $value['excluded_roles'] ) ? $value['excluded_roles'] : [], 'role', 'sanitize_key' ), 473 'excluded_products' => $this->prepare_integer_list_rows( isset( $value['excluded_products'] ) ? $value['excluded_products'] : [], 'product_id' ), 474 'excluded_categories' => $this->prepare_string_list_rows( isset( $value['excluded_categories'] ) ? $value['excluded_categories'] : [], 'slug', 'sanitize_title' ), 475 'excluded_tags' => $this->prepare_string_list_rows( isset( $value['excluded_tags'] ) ? $value['excluded_tags'] : [], 'slug', 'sanitize_title' ), 476 ]; 477 } 478 479 private function prepare_timeout_rows( $rows, $field, $sanitizer ) { 480 $prepared = []; if ( ! is_array( $rows ) ) { return $prepared; } foreach ( $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $key = isset( $row[ $field ] ) ? call_user_func( $sanitizer, $row[ $field ] ) : ''; $timeout = isset( $row['timeout'] ) ? absint( $row['timeout'] ) : 0; if ( $key && $timeout > 0 ) { $prepared[ $key ] = $timeout; } } return $prepared; 481 } 482 483 private function prepare_integer_timeout_rows( $rows, $field ) { 484 $prepared = []; if ( ! is_array( $rows ) ) { return $prepared; } foreach ( $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $key = isset( $row[ $field ] ) ? absint( $row[ $field ] ) : 0; $timeout = isset( $row['timeout'] ) ? absint( $row['timeout'] ) : 0; if ( $key > 0 && $timeout > 0 ) { $prepared[ $key ] = $timeout; } } return $prepared; 485 } 486 487 private function prepare_string_list_rows( $rows, $field, $sanitizer ) { 488 $prepared = []; if ( ! is_array( $rows ) ) { return $prepared; } foreach ( $rows as $row ) { $item = is_array( $row ) && isset( $row[ $field ] ) ? call_user_func( $sanitizer, $row[ $field ] ) : ''; if ( $item ) { $prepared[] = $item; } } return array_values( array_unique( $prepared ) ); 489 } 490 491 private function prepare_integer_list_rows( $rows, $field ) { 492 $prepared = []; if ( ! is_array( $rows ) ) { return $prepared; } foreach ( $rows as $row ) { $item = is_array( $row ) && isset( $row[ $field ] ) ? absint( $row[ $field ] ) : 0; if ( $item > 0 ) { $prepared[] = $item; } } return array_values( array_unique( $prepared ) ); 493 } 494 495 private function get_stats( $rules ) { 496 return [ 497 [ 'label' => __( 'Default Timeout', 'cartflush' ), 'value' => (int) get_option( 'cartflush_expiration_time', 30 ), 'meta' => __( 'minutes', 'cartflush' ) ], 498 [ 'label' => __( 'Timeout Rules', 'cartflush' ), 'value' => count( $rules['customer_type_rules'] ) + count( $rules['role_rules'] ) + count( $rules['product_rules'] ) + count( $rules['category_rules'] ) + count( $rules['tag_rules'] ), 'meta' => __( 'active', 'cartflush' ) ], 499 [ 'label' => __( 'Exclusions', 'cartflush' ), 'value' => count( $rules['excluded_roles'] ) + count( $rules['excluded_products'] ) + count( $rules['excluded_categories'] ) + count( $rules['excluded_tags'] ), 'meta' => __( 'guards', 'cartflush' ) ], 500 [ 'label' => __( 'Import Modes', 'cartflush' ), 'value' => 'CSV + JSON', 'meta' => __( 'ready', 'cartflush' ) ], 501 ]; 502 } 503 504 private function get_summary_items( $rules ) { 505 return [ 506 [ 'label' => __( 'Customer Type Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['customer_type_rules'] ) ], 507 [ 'label' => __( 'Role Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['role_rules'] ) ], 508 [ 'label' => __( 'Product Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['product_rules'] ) ], 509 [ 'label' => __( 'Category Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['category_rules'] ) ], 510 [ 'label' => __( 'Tag Rules', 'cartflush' ), 'value' => $this->format_assoc_list( $rules['tag_rules'] ) ], 511 [ 'label' => __( 'Excluded Roles', 'cartflush' ), 'value' => $this->format_simple_list( $rules['excluded_roles'] ) ], 512 [ 'label' => __( 'Excluded Products', 'cartflush' ), 'value' => $this->format_simple_list( $rules['excluded_products'] ) ], 513 [ 'label' => __( 'Excluded Categories', 'cartflush' ), 'value' => $this->format_simple_list( $rules['excluded_categories'] ) ], 514 [ 'label' => __( 'Excluded Tags', 'cartflush' ), 'value' => $this->format_simple_list( $rules['excluded_tags'] ) ], 515 ]; 516 } 517 518 private function get_role_options() { 519 if ( ! function_exists( 'wp_roles' ) ) { return []; } 520 $options = []; foreach ( wp_roles()->roles as $key => $data ) { $options[ sanitize_key( $key ) ] = isset( $data['name'] ) ? $data['name'] : $key; } return $options; 521 } 522 523 private function get_taxonomy_options( $taxonomy ) { 524 $options = []; $terms = get_terms( [ 'taxonomy' => $taxonomy, 'hide_empty' => false ] ); if ( is_wp_error( $terms ) || ! is_array( $terms ) ) { return $options; } foreach ( $terms as $term ) { if ( isset( $term->slug, $term->name ) ) { $options[ $term->slug ] = $term->name . ' (' . $term->slug . ')'; } } return $options; 525 } 526 397 527 private function format_assoc_list( $items ) { 398 if ( empty( $items ) ) { 399 return __( 'None imported yet.', 'cartflush' ); 400 } 401 402 $formatted = []; 403 404 foreach ( $items as $key => $value ) { 405 $formatted[] = sprintf( 406 /* translators: 1: rule key, 2: timeout in minutes. */ 407 __( '%1$s: %2$d min', 'cartflush' ), 408 $key, 409 absint( $value ) 410 ); 411 } 412 413 return implode( ', ', $formatted ); 414 } 415 416 /** 417 * Format flat lists for display. 418 * 419 * @param array<int|string> $items Display items. 420 * @return string 421 */ 528 if ( empty( $items ) ) { return __( 'None saved yet.', 'cartflush' ); } 529 $formatted = []; foreach ( $items as $key => $value ) { $formatted[] = sprintf( __( '%1$s: %2$d min', 'cartflush' ), (string) $key, absint( $value ) ); } return implode( ', ', $formatted ); 530 } 531 422 532 private function format_simple_list( $items ) { 423 if ( empty( $items ) ) { 424 return __( 'None imported yet.', 'cartflush' ); 425 } 426 427 return implode( ', ', array_map( 'strval', $items ) ); 428 } 429 430 /** 431 * Ensure the current user can manage the plugin. 432 * 433 * @return void 434 */ 533 return empty( $items ) ? __( 'None saved yet.', 'cartflush' ) : implode( ', ', array_map( 'strval', $items ) ); 534 } 535 435 536 private function assert_admin_permissions() { 436 if ( ! current_user_can( 'manage_options' ) ) { 437 wp_die( esc_html__( 'You do not have permission to access this page.', 'cartflush' ) ); 438 } 439 } 440 441 /** 442 * Redirect back to the settings page with a status message. 443 * 444 * @param string $notice Success message. 445 * @param string $error Error message. 446 * @return void 447 */ 537 if ( ! current_user_can( $this->get_required_capability() ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'cartflush' ) ); } 538 } 539 540 private function get_required_capability() { 541 return 'manage_woocommerce'; 542 } 543 448 544 private function redirect_with_message( $notice = '', $error = '' ) { 449 $url = add_query_arg( 450 array_filter( 451 [ 452 'page' => 'cartflush-settings', 453 'cartflush_notice' => $notice, 454 'cartflush_error' => $error, 455 ] 456 ), 457 admin_url( 'options-general.php' ) 458 ); 459 545 $url = add_query_arg( array_filter( [ 'page' => 'cartflush-settings', 'cartflush_notice' => $notice, 'cartflush_error' => $error ] ), admin_url( 'admin.php' ) ); 460 546 wp_safe_redirect( $url ); 461 547 exit; -
cartflush-autoclear-cart-for-inactive-users/trunk/includes/class-cartflush-rules.php
r3484352 r3488606 34 34 $rules = wp_parse_args( $value, $defaults ); 35 35 36 $normalized_role_rules = [];37 if ( is_array( $rules['role_rules'] ) ) {38 foreach ( $rules['role_rules'] as $role => $timeout ) {39 $role = sanitize_key( $role );40 $timeout = absint( $timeout );41 42 if ( $role && $timeout > 0 ) {43 $normalized_role_rules[ $role ] = $timeout;44 }45 }46 }47 48 $normalized_category_rules = [];49 if ( is_array( $rules['category_rules'] ) ) {50 foreach ( $rules['category_rules'] as $slug => $timeout ) {51 $slug = sanitize_title( $slug );52 $timeout = absint( $timeout );53 54 if ( $slug && $timeout > 0 ) {55 $normalized_category_rules[ $slug ] = $timeout;56 }57 }58 }59 60 $excluded_products = [];61 if ( is_array( $rules['excluded_products'] ) ) {62 foreach ( $rules['excluded_products'] as $product_id ) {63 $product_id = absint( $product_id );64 65 if ( $product_id > 0 ) {66 $excluded_products[] = $product_id;67 }68 }69 }70 71 $excluded_categories = [];72 if ( is_array( $rules['excluded_categories'] ) ) {73 foreach ( $rules['excluded_categories'] as $slug ) {74 $slug = sanitize_title( $slug );75 76 if ( $slug ) {77 $excluded_categories[] = $slug;78 }79 }80 }81 82 36 return [ 83 'role_rules' => $normalized_role_rules, 84 'category_rules' => $normalized_category_rules, 85 'excluded_products' => array_values( array_unique( $excluded_products ) ), 86 'excluded_categories' => array_values( array_unique( $excluded_categories ) ), 37 'customer_type_rules' => $this->normalize_customer_type_rules( $rules['customer_type_rules'] ), 38 'role_rules' => $this->normalize_timeout_map( $rules['role_rules'], 'sanitize_key' ), 39 'category_rules' => $this->normalize_timeout_map( $rules['category_rules'], 'sanitize_title' ), 40 'tag_rules' => $this->normalize_timeout_map( $rules['tag_rules'], 'sanitize_title' ), 41 'product_rules' => $this->normalize_product_timeout_rules( $rules['product_rules'] ), 42 'excluded_roles' => $this->normalize_string_list( $rules['excluded_roles'], 'sanitize_key' ), 43 'excluded_products' => $this->normalize_integer_list( $rules['excluded_products'] ), 44 'excluded_categories' => $this->normalize_string_list( $rules['excluded_categories'], 'sanitize_title' ), 45 'excluded_tags' => $this->normalize_string_list( $rules['excluded_tags'], 'sanitize_title' ), 87 46 ]; 88 47 } … … 102 61 $user = wp_get_current_user(); 103 62 $cart_items = WC()->cart->get_cart(); 63 $is_guest = ! ( $user instanceof WP_User ) || 0 === (int) $user->ID; 64 65 if ( $is_guest && isset( $rules['customer_type_rules']['guest'] ) ) { 66 $timeouts[] = (int) $rules['customer_type_rules']['guest']; 67 } 68 69 if ( ! $is_guest && isset( $rules['customer_type_rules']['logged_in'] ) ) { 70 $timeouts[] = (int) $rules['customer_type_rules']['logged_in']; 71 } 104 72 105 73 if ( $user instanceof WP_User && ! empty( $user->roles ) ) { … … 114 82 115 83 foreach ( $cart_items as $cart_item ) { 116 $product_id = isset( $cart_item['product_id'] ) ? absint( $cart_item['product_id'] ) : 0; 117 $category_ids = $product_id ? wc_get_product_term_ids( $product_id, 'product_cat' ) : []; 118 119 foreach ( $category_ids as $category_id ) { 120 $term = get_term( $category_id, 'product_cat' ); 121 122 if ( $term && ! is_wp_error( $term ) && isset( $rules['category_rules'][ $term->slug ] ) ) { 123 $timeouts[] = (int) $rules['category_rules'][ $term->slug ]; 84 $product_id = isset( $cart_item['product_id'] ) ? absint( $cart_item['product_id'] ) : 0; 85 86 if ( ! $product_id ) { 87 continue; 88 } 89 90 if ( isset( $rules['product_rules'][ $product_id ] ) ) { 91 $timeouts[] = (int) $rules['product_rules'][ $product_id ]; 92 } 93 94 foreach ( $this->get_product_term_slugs( $product_id, 'product_cat' ) as $slug ) { 95 if ( isset( $rules['category_rules'][ $slug ] ) ) { 96 $timeouts[] = (int) $rules['category_rules'][ $slug ]; 97 } 98 } 99 100 foreach ( $this->get_product_term_slugs( $product_id, 'product_tag' ) as $slug ) { 101 if ( isset( $rules['tag_rules'][ $slug ] ) ) { 102 $timeouts[] = (int) $rules['tag_rules'][ $slug ]; 124 103 } 125 104 } … … 137 116 138 117 /** 139 * Determine whether the current cart contains excluded products or categories.118 * Determine whether the current cart contains excluded items. 140 119 * 141 120 * @return bool … … 147 126 148 127 $rules = $this->get_rules_option(); 128 $user = wp_get_current_user(); 129 130 if ( $user instanceof WP_User && ! empty( $user->roles ) ) { 131 foreach ( $user->roles as $role ) { 132 if ( in_array( sanitize_key( $role ), $rules['excluded_roles'], true ) ) { 133 return true; 134 } 135 } 136 } 149 137 150 138 foreach ( WC()->cart->get_cart() as $cart_item ) { … … 155 143 } 156 144 157 $category_ids = $product_id ? wc_get_product_term_ids( $product_id, 'product_cat' ) : []; 158 159 foreach ( $category_ids as $category_id ) { 160 $term = get_term( $category_id, 'product_cat' ); 161 162 if ( $term && ! is_wp_error( $term ) && in_array( $term->slug, $rules['excluded_categories'], true ) ) { 145 foreach ( $this->get_product_term_slugs( $product_id, 'product_cat' ) as $slug ) { 146 if ( in_array( $slug, $rules['excluded_categories'], true ) ) { 147 return true; 148 } 149 } 150 151 foreach ( $this->get_product_term_slugs( $product_id, 'product_tag' ) as $slug ) { 152 if ( in_array( $slug, $rules['excluded_tags'], true ) ) { 163 153 return true; 164 154 } … … 176 166 public function get_default_rules() { 177 167 return [ 168 'customer_type_rules' => [], 178 169 'role_rules' => [], 179 170 'category_rules' => [], 171 'tag_rules' => [], 172 'product_rules' => [], 173 'excluded_roles' => [], 180 174 'excluded_products' => [], 181 175 'excluded_categories' => [], 176 'excluded_tags' => [], 182 177 ]; 183 178 } 179 180 /** 181 * Normalize customer type rules. 182 * 183 * @param mixed $rules Customer type rules. 184 * @return array<string, int> 185 */ 186 private function normalize_customer_type_rules( $rules ) { 187 $allowed = [ 'guest', 'logged_in' ]; 188 $normalized = []; 189 190 if ( ! is_array( $rules ) ) { 191 return $normalized; 192 } 193 194 foreach ( $rules as $type => $timeout ) { 195 $type = sanitize_key( $type ); 196 $timeout = absint( $timeout ); 197 198 if ( in_array( $type, $allowed, true ) && $timeout > 0 ) { 199 $normalized[ $type ] = $timeout; 200 } 201 } 202 203 return $normalized; 204 } 205 206 /** 207 * Normalize a generic timeout map. 208 * 209 * @param mixed $rules Timeout rules. 210 * @param callable $sanitizer Key sanitizer. 211 * @return array<string, int> 212 */ 213 private function normalize_timeout_map( $rules, $sanitizer ) { 214 $normalized = []; 215 216 if ( ! is_array( $rules ) ) { 217 return $normalized; 218 } 219 220 foreach ( $rules as $key => $timeout ) { 221 $key = call_user_func( $sanitizer, $key ); 222 $timeout = absint( $timeout ); 223 224 if ( $key && $timeout > 0 ) { 225 $normalized[ $key ] = $timeout; 226 } 227 } 228 229 return $normalized; 230 } 231 232 /** 233 * Normalize product-specific timeout rules. 234 * 235 * @param mixed $rules Product rules. 236 * @return array<int, int> 237 */ 238 private function normalize_product_timeout_rules( $rules ) { 239 $normalized = []; 240 241 if ( ! is_array( $rules ) ) { 242 return $normalized; 243 } 244 245 foreach ( $rules as $product_id => $timeout ) { 246 $product_id = absint( $product_id ); 247 $timeout = absint( $timeout ); 248 249 if ( $product_id > 0 && $timeout > 0 ) { 250 $normalized[ $product_id ] = $timeout; 251 } 252 } 253 254 return $normalized; 255 } 256 257 /** 258 * Normalize a list of integer IDs. 259 * 260 * @param mixed $items List value. 261 * @return array<int> 262 */ 263 private function normalize_integer_list( $items ) { 264 $normalized = []; 265 266 if ( ! is_array( $items ) ) { 267 return $normalized; 268 } 269 270 foreach ( $items as $item ) { 271 $item = absint( $item ); 272 273 if ( $item > 0 ) { 274 $normalized[] = $item; 275 } 276 } 277 278 return array_values( array_unique( $normalized ) ); 279 } 280 281 /** 282 * Normalize a list of strings. 283 * 284 * @param mixed $items List value. 285 * @param callable $sanitizer Item sanitizer. 286 * @return array<int, string> 287 */ 288 private function normalize_string_list( $items, $sanitizer ) { 289 $normalized = []; 290 291 if ( ! is_array( $items ) ) { 292 return $normalized; 293 } 294 295 foreach ( $items as $item ) { 296 $item = call_user_func( $sanitizer, $item ); 297 298 if ( $item ) { 299 $normalized[] = $item; 300 } 301 } 302 303 return array_values( array_unique( $normalized ) ); 304 } 305 306 /** 307 * Get sanitized term slugs for a product and taxonomy. 308 * 309 * @param int $product_id Product ID. 310 * @param string $taxonomy Taxonomy name. 311 * @return array<int, string> 312 */ 313 private function get_product_term_slugs( $product_id, $taxonomy ) { 314 $product_id = absint( $product_id ); 315 316 if ( $product_id <= 0 ) { 317 return []; 318 } 319 320 $terms = get_the_terms( $product_id, $taxonomy ); 321 322 if ( is_wp_error( $terms ) || ! is_array( $terms ) ) { 323 return []; 324 } 325 326 $slugs = []; 327 328 foreach ( $terms as $term ) { 329 if ( isset( $term->slug ) ) { 330 $slugs[] = sanitize_title( $term->slug ); 331 } 332 } 333 334 return array_values( array_unique( array_filter( $slugs ) ) ); 335 } 184 336 } -
cartflush-autoclear-cart-for-inactive-users/trunk/readme.txt
r3486304 r3488606 1 ## === CartFlush –Auto Clear WooCommerce Cart for Inactive Users ===1 === CartFlush - Auto Clear WooCommerce Cart for Inactive Users === 2 2 3 3 Contributors: wprashed … … 6 6 Tested up to: 6.8 7 7 Requires PHP: 7.4 8 Stable tag: 2. 0.08 Stable tag: 2.1.0 9 9 License: GPLv2 or later 10 License URI: [https://www.gnu.org/licenses/gpl-2.0.html](https://www.gnu.org/licenses/gpl-2.0.html)10 License URI: https://www.gnu.org/licenses/gpl-2.0.html 11 11 12 12 Automatically clear inactive WooCommerce carts with advanced timeout rules, exclusions, and import/export tools. 13 13 14 --- 15 16 ## == Description == 14 == Description == 17 15 18 16 CartFlush helps you automatically clear inactive WooCommerce carts, keeping your store clean, fast, and optimized. 19 17 20 Instead of relying on a basic timeout, CartFlush gives you full control over how cart expiration works. You can define different timeout rules based on user roles, product categories, and exclusions—making it flexible enough for real-world eCommerce scenarios.18 Instead of relying on a single timeout, CartFlush gives you full control over cart expiration from one settings screen under WooCommerce. You can define different timeout rules based on customer type, user roles, products, categories, and tags, then add exclusions for carts that should never be cleared automatically. 21 19 22 20 Whether you want faster cart turnover, better session management, or cleaner abandoned cart handling, CartFlush gives you the tools to do it properly. … … 24 22 == Live Demo == 25 23 26 🚀 [Try Live Demo (No Setup Required)](https://playground.wordpress.net/?blueprint-url=https%3A%2F%2Fraw.githubusercontent.com%2Fwprashed%2Fcartflush-autoclear-cart-for-inactive-users%2F185b7faf5918e286fe9ca1fb1563d70455d4e095%2Fblueprint.json) 27 28 --- 29 30 ## 🚀 Why Use CartFlush? 24 [Try Live Demo (No Setup Required)](https://playground.wordpress.net/?blueprint-url=https%3A%2F%2Fraw.githubusercontent.com%2Fwprashed%2Fcartflush-autoclear-cart-for-inactive-users%2F185b7faf5918e286fe9ca1fb1563d70455d4e095%2Fblueprint.json) 25 26 == Why Use CartFlush? == 31 27 32 28 * Prevent stale and abandoned carts from piling up 33 29 * Improve WooCommerce session performance 34 * Apply smarter rules based on users and products 35 * Save time with import/export configuration tools 36 * Maintain clean and optimized cart behavior 37 38 --- 39 40 ## == Key Features == 41 42 ### ⏱ Default Cart Timeout 43 44 Set a global inactivity timeout (in minutes). If no other rules apply, this value determines when a cart is cleared. 45 46 --- 47 48 ### 👤 Role-Based Timeout Rules 49 50 Define custom cart expiration times based on user roles. 30 * Apply smarter rules based on customers and products 31 * Manage rules visually from the WooCommerce settings page 32 * Import or export rules for fast setup and migration 33 34 == Key Features == 35 36 === Default Cart Timeout === 37 38 Set a global inactivity timeout in minutes. If no other rules apply, this value determines when a cart is cleared. 39 40 === Visual Rule Builder === 41 42 Create and edit cart timeout rules directly from the CartFlush settings page under WooCommerce. No CSV import is required for day-to-day management. 43 44 === Customer Type Rules === 45 46 Define separate timeouts for: 47 48 * Guest customers 49 * Logged-in customers 50 51 === Role-Based Timeout Rules === 52 53 Define custom cart expiration times for specific user roles. 51 54 52 55 Examples: 53 56 54 * Customers → 30 minutes 55 * Subscribers → 60 minutes 56 * Wholesale users → 120 minutes 57 58 Perfect for stores with different user types and behaviors. 59 60 --- 61 62 ### 🛍 Category-Based Timeout Rules 63 64 Set cart timeout rules based on product categories. 65 66 Use cases: 67 68 * Flash sale items → shorter timeout 69 * Subscription products → shorter timeout 70 * High-value products → longer timeout 71 72 CartFlush checks all items in the cart and applies the most relevant rule. 73 74 --- 75 76 ### ⚡ Smart Timeout Logic 77 78 When multiple rules apply, CartFlush automatically selects the **shortest timeout**. 79 80 This ensures: 81 82 * Predictable behavior 83 * Better control over urgency 84 * No rule conflicts 85 86 --- 87 88 ### 🚫 Product Exclusions 89 90 Exclude specific products from cart clearing. 91 92 If a cart contains an excluded product: 93 → The cart will NOT be cleared. 94 95 --- 96 97 ### 📂 Category Exclusions 98 99 Exclude entire categories from auto-clear. 100 101 If any product in the cart belongs to an excluded category: 102 → Cart clearing is skipped. 103 104 --- 105 106 ### 📥 CSV Import for Rules 107 108 Bulk import rules using CSV. 109 110 Supported types: 111 57 * Customers - 30 minutes 58 * Subscribers - 60 minutes 59 * Wholesale users - 120 minutes 60 61 === Product, Category, and Tag Rules === 62 63 Apply specific timeout values based on: 64 65 * Product ID 66 * Product category 67 * Product tag 68 69 This makes it easy to create shorter or longer expiration windows for special items, campaigns, or collections. 70 71 === Smart Timeout Logic === 72 73 When multiple timeout rules apply, CartFlush automatically uses the shortest valid timeout. 74 75 === Exclusion Rules === 76 77 Prevent cart clearing entirely for matching carts using: 78 79 * Excluded roles 80 * Excluded products 81 * Excluded categories 82 * Excluded tags 83 84 === CSV Import for Bulk Rules === 85 86 Bulk import rule data with CSV when that is faster than manual entry. 87 88 Supported CSV types: 89 90 * customer_type 112 91 * role 92 * product_rule 113 93 * category 94 * tag 95 * excluded_role 114 96 * excluded_product 115 97 * excluded_category 116 117 Quickly configure large stores without manual setup. 118 119 --- 120 121 ### 📤 JSON Export (Full Backup) 122 123 Export all settings into a JSON file. 124 125 Includes: 126 127 * Default timeout 128 * Role rules 129 * Category rules 130 * Exclusions 131 132 Perfect for backups and migrations. 133 134 --- 135 136 ### 🔁 JSON Import (Quick Setup) 137 138 Import settings instantly on another site. 139 140 Ideal for: 141 142 * Agencies 143 * Multi-store setups 144 * Staging → production deployment 145 146 --- 147 148 ### 👥 Works for Guests & Logged-in Users 149 150 CartFlush uses WooCommerce sessions, so it works for: 151 152 * Guest users 153 * Logged-in customers 154 155 No additional configuration required. 156 157 --- 158 159 ### 🎯 Lightweight & Efficient 160 161 No unnecessary overhead. The plugin focuses only on: 162 163 * Tracking inactivity 164 * Applying rules 165 * Clearing carts 166 167 --- 168 169 ### 🌍 Translation Ready 170 171 Includes text domain and POT file for easy localization. 172 173 --- 174 175 ### 🧹 Clean Uninstall 176 177 When the plugin is deleted: 178 179 * All data and settings are removed automatically 180 181 --- 182 183 ## == How It Works == 184 185 1. Customer adds items to cart 186 2. Inactivity timer starts 187 3. Plugin checks: 188 189 * Default timeout 190 * User role rules 191 * Product category rules 192 4. Shortest valid timeout is applied 193 5. If excluded items exist → skip clearing 194 6. Cart is cleared after timeout 195 196 --- 197 198 ## == Supported Import Formats == 98 * excluded_tag 99 100 === JSON Import and Export === 101 102 Export the full CartFlush configuration as JSON for backup or migration, then import it on another store when needed. 103 104 === WooCommerce Menu Integration === 105 106 The CartFlush settings page is available directly under the WooCommerce admin menu for quicker access. 107 108 === Lightweight and Efficient === 109 110 CartFlush focuses only on inactivity tracking, rule evaluation, and cart clearing without adding unnecessary overhead. 111 112 === Translation Ready === 113 114 Includes the `cartflush` text domain for localization. 115 116 === Clean Uninstall === 117 118 When the plugin is deleted, CartFlush removes its stored settings automatically. 119 120 == How It Works == 121 122 1. A customer adds items to the cart. 123 2. The inactivity timer begins. 124 3. CartFlush checks the default timeout and any matching timeout rules. 125 4. The shortest valid timeout is selected. 126 5. If an exclusion rule matches, cart clearing is skipped. 127 6. The cart is cleared after the final timeout is reached. 128 129 == Supported Import Formats == 199 130 200 131 CSV headers: … … 202 133 `type,key,timeout_minutes` 203 134 204 ### Supported types: 205 135 Supported types: 136 137 * customer_type 206 138 * role 139 * product_rule 207 140 * category 141 * tag 142 * excluded_role 208 143 * excluded_product 209 144 * excluded_category 210 211 ### Example: 212 145 * excluded_tag 146 147 Example rows: 148 149 `customer_type,guest,20` 213 150 `role,customer,30` 214 `category,subscription-box,10` 151 `product_rule,321,10` 152 `category,flash-sale,15` 153 `tag,seasonal,25` 154 `excluded_role,wholesale_customer,` 215 155 `excluded_product,123,` 216 156 `excluded_category,high-ticket,` 217 218 --- 219 220 ## == Frequently Asked Questions == 221 222 ### Does this work for guest users and logged-in users? 157 `excluded_tag,fragile,` 158 159 == Frequently Asked Questions == 160 161 === Does this work for guest users and logged-in users? === 223 162 224 163 Yes. CartFlush uses WooCommerce sessions, so both are supported. 225 164 226 --- 227 228 ### How is the timeout calculated? 229 230 The plugin starts with the default timeout, then checks role and category rules. The shortest valid timeout is applied. 231 232 --- 233 234 ### What if a cart contains excluded items? 235 236 CartFlush will skip clearing the cart entirely. 237 238 --- 239 240 ### Can I migrate settings between sites? 165 === How is the timeout calculated? === 166 167 The plugin starts with the default timeout, then checks matching customer type, role, product, category, and tag rules. The shortest valid timeout is applied. 168 169 === What if a cart contains excluded items? === 170 171 CartFlush skips clearing the cart entirely. 172 173 === Can I manage rules without importing CSV? === 174 175 Yes. Rules can be added and edited directly from the CartFlush settings page. 176 177 === Can I migrate settings between sites? === 241 178 242 179 Yes. Export settings as JSON and import them on another site. 243 180 244 --- 245 246 ### Does uninstall remove all data? 181 === Does uninstall remove all data? === 247 182 248 183 Yes. All plugin options are deleted during uninstall. 249 184 250 --- 251 252 ## == Screenshots == 253 254 1. Clean and modern CartFlush settings panel 255 2. CSV import interface for rules 256 3. JSON export/import tools 257 4. Active rules and exclusions overview 258 259 --- 260 261 ## == Changelog == 185 == Screenshots == 186 187 1. Modern CartFlush settings page under WooCommerce 188 2. Manual rule builder for timeout rules, CSV and JSON import/export tools 189 3. Exclusion Rules 190 4. Saved configuration overview 191 192 == Changelog == 193 194 = 2.1.0 = 195 196 * Added a full visual rule builder to the settings page 197 * Moved the plugin page under the WooCommerce admin menu 198 * Added customer type, product, and tag timeout rules 199 * Added excluded role and excluded tag support 200 * Expanded CSV import to support all new rule types 201 * Redesigned the admin settings interface with a more modern layout 202 * Improved import/export presentation and rule card usability 262 203 263 204 = 2.0.0 =
Note: See TracChangeset
for help on using the changeset viewer.