Plugin Directory

Changeset 3759386


Ignore:
Timestamp:
10/22/2026 06:21:05 AM (13 days ago)
Author:
sandy786
Message:

Release 1.4: secure product service pricing, reliable cart and order handling, accessible options, and setup documentation

Publish trunk and tags/1.4 with server-side service prices, validated input,
fixed radio grouping and sparse editor rows, free services, dependency guard,
included setup guide and documentation links, translations template, and
refreshed directory screenshots.

Verified with 30 integration checks, a dependency guard check, 50 desktop/mobile
browser checks, and the official WordPress Plugin Check (no findings).

Location:
custom-checkbox-ultimate-for-woocommerce
Files:
8 added
32 edited
1 copied

Legend:

Unmodified
Added
Removed
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/assets/css/icwcu-admin-styling.css

    r3264079 r3759386  
    1 div#icwcu_custom_services_meta_box h2 {
    2     position: relative;
    3     display: inline-block;
    4 }
    5 
    6 div#icwcu_custom_services_meta_box h2 svg {
    7     float: left;
    8     height: 20px;
    9 }
    10 .icwcu-custom-service-field svg {
    11     vertical-align: middle;
    12     margin-right: 5px;
    13 }
    14 .icwcu-custom-service-field textarea{
    15         vertical-align:middle;
    16 }
    17 
    18 .menu-icon-ic-woo-ultimate svg {
    19     vertical-align: middle;
    20     margin-right: 10px;
    21 }
    22 .icwcu-custom-service-field label {
    23     width: 144px;
    24     display: inline-block;
    25     padding: 0px 20px;
    26 }
    27 .icwcu-custom-service-field {
    28     border: 1px solid #ddd;
    29     margin: 10px;
    30 }
    31 button#icwcu-add-more-fields:hover {
    32         background: #167fae;
    33 }
    34 button#icwcu-add-more-fields {
    35     margin: 10px;
    36     background: #03A9F4;
    37     color: #fff;
    38     border: 0;
    39 }
    40 button#icwcu-add-moreradio-fields:hover {
    41         background: #167fae;
    42 }
    43 button#icwcu-add-moreradio-fields {
    44     margin: 10px;
    45     background: #03A9F4;
    46     color: #fff;
    47     border: 0;
    48 }
    49 
    50 
    51 .icwcu-admin-notice-wrapper {
    52   padding: 20px; }
    53   .icwcu-admin-notice-wrapper h2 {
    54     margin-top: 0; }
    55   .icwcu-admin-notice-wrapper p {
    56     font-size: 14px; }
    57   .icwcu-admin-notice-wrapper .icwcu-admin-notice-primary-button {
    58     padding: 5px 20px;
    59     background: #67bc45;
    60     color: #fff;
    61     font-weight: normal;
    62     text-decoration: none;
    63     margin-top: 5px;
    64     display: inline-block;
    65     border-radius: 50px; }
    66 
    67 .is-style-outline > .wp-block-button__link:not(.has-text-color),
    68 .wp-block-button__link.is-style-outline:not(.has-text-color) {
    69   color: var(--wp--preset--color--primary); }
    70 
    71 .icwcu--getting-started--wrapper .icwcu-header-left > .fs-notice {
    72   display: none !important; }
    73 
    74 .icwcu-options-features > li span {
    75   color: green;
    76   margin-right: 20px; }
    77 
    78 .icwcu-compare-table table td span {
    79   font-size: 21px;
    80   background: #f0f0f1;
    81   border-radius: 100%;
    82   padding: 5px;
    83   color: #d72b2b; }
    84 
    85 .icwcu-compare-table table td span.dashicons.dashicons-yes {
    86   text-align: center;
    87   color: #4caf50; }
    88 
    89 .icwcu-about-container.icwcu-admin-card.icwcu-admin-card-header {
    90   background: linear-gradient(45deg, #3F51B5, #E91E63);
    91   justify-content: center;
    92   align-items: center; }
    93 
    94 .icwcu-about-side {
    95   margin-top: 30px; }
    96 #icwcu-admin-about-page p, #icwcu-admin-about-page {
    97   font-size: 17px;
    98   line-height: 1.7;
    99   margin: 0 0 15px; }
    100 
    101 .subscribe-panel li span {
    102   margin-top: 6px;
    103   margin-right: 10px;
    104   color: #3fb345; }
    105 
    106 #icwcu-admin-about-page {
    107   margin: 20px;
    108   max-width: 100%; }
    109 
    110 .icwcu-about-container {
    111   padding: 40px 60px;
    112   border-bottom: 1px solid #ddd;
    113   border-radius: 5px;
    114   margin: 30px 0;
    115   background: #fff;
    116   box-shadow: 0 0 50px #dddddd7d; }
    117 
    118 a.icwcu-admin-button.premium-button span.dashicons{
    119   vertical-align: middle;
    120 }
    121 a.icwcu-admin-button.premium-button {
    122   background: #2196F3;
    123   margin-left: 5px; }
    124 
    125 h3.icwcu-admin-card-title {
    126   font-size: 1.4rem !important;
    127   text-transform: capitalize;
    128   color: #fff;
    129   margin-bottom: 0;
    130   padding: 0; }
    131 
    132 a.icwcu-admin-button {
    133   padding: 12px 36px;
    134   text-decoration: inherit;
    135   background: #3e3541;
    136   color: #fff;
    137   border-radius: 50px;
    138   margin-top: 20px;
    139   display: inline-block;
    140   font-size: 16px;
    141   line-height: 1.75;
    142   display: flex;
    143   align-items: center;
    144   gap: 4px;
    145   display: inline-block; }
    146 
    147 @keyframes spin {
    148   100% {
    149     -webkit-transform: rotate(360deg);
    150     transform: rotate(360deg); } }
    151   a.icwcu-admin-button.icwcu-importing-icwcu span {
    152     animation: spin 4s linear infinite; }
    153 
    154 a.icwcu-admin-button.primary.large {
    155   background: #e68836;
    156   margin-right: 9px; }
    157 
    158 .feature-list {
    159   display: grid;
    160   grid-gap: 50px;
    161   grid-template-columns: 3fr 1fr;
    162   grid-template-rows: 1fr;
    163   max-width: 100%; }
    164 
    165 .icwcu-about-container.icwcu-about-content {
    166   margin: 0; }
    167 
    168 .icwcu-options-features > li {
    169   display: -webkit-inline-flex;
    170   display: -ms-inline-flexbox;
    171   display: inline-flex;
    172   -webkit-align-items: center;
    173   -ms-flex-align: center;
    174   align-items: center;
    175   width: 100%;
    176   float: left;
    177   margin-bottom: 15px;
    178   font-weight: 500; }
    179 ul.icwcu-options-features b {
    180     font-weight: 900;
    181     margin-right: 10px;
    182 }
    183 .icwcu-options-features > li img {
    184   margin-right: 5px; }
    185 
    186 .icwcu-options-features {
    187   list-style: none;
    188   padding: 0;
    189   margin: 0 0 30px; }
    190   .icwcu-about-container.icwcu-compare-table,
    191   .icwcu-admin-card.member {
    192     display: none;
    193 }
    194 .icwcu-admin-card-title.is-sub {
    195   margin-left: -30px;
    196   margin-right: -30px;
    197   padding: 0 30px 30px;
    198   border-bottom: 1px solid #ebf2f5;
    199   font-size: 2rem;
    200   text-transform: capitalize;
    201   margin-top: 0;
    202   color: #000; }
    203 
    204 .icwcu-about-side .icwcu-admin-card {
    205   background-color: #fff;
    206   padding: 30px;
    207   margin-bottom: 30px;
    208   background-repeat: no-repeat;
    209   background-position: 100% 100%;
    210   border-radius: 15px;
    211   border: 1px solid #ebf2f5;
    212   background: -webkit-linear-gradient(315deg, #0dcad2, #0060f3);
    213   background: -o-linear-gradient(315deg, #0dcad2, #0060f3);
    214   background: linear-gradient(135deg, #0dcad2, #0060f3);
    215   color: #fff; }
    216 
    217 h4.icwcu-admin-card-title.is-small {
    218   color: #fff;
    219   margin: 0; }
    220 
    221 .icwcu-about-side .icwcu-admin-card a.icwcu-admin-button.large.icwcu-block.is-default {
    222   background: #fff;
    223   color: #000; }
    224 
    225 .icwcu-admin-card.cart-two {
    226   background: #fff;
    227   color: #000;
    228   margin: 0; }
    229 
    230 #icwcu-admin-about-page .icwcu-about-side .icwcu-admin-card h3 {
    231   font-size: 1.5rem;
    232   color: #fff; }
    233 
    234 #icwcu-admin-about-page .icwcu-admin-card.cart-two h3 {
    235   color: #010101; }
    236 
    237 #icwcu-admin-about-page div#setting-error-tgmpa {
    238   display: none; }
    239 
    240 .cart-two h3 {
    241   color: #000; }
    242 
    243 .icwcu-header-left h2 {
    244   font-size: 3rem;
    245   text-transform: capitalize;
    246   margin: 0;
    247   color: #fff; }
    248 
    249 #icwcu-admin-about-page .icwcu-header-left p {
    250   color: #fff; }
    251 
    252 .admin-grid-2 {
    253   display: grid;
    254   grid-gap: 4%;
    255   grid-template-columns: 48% 48%;
    256   grid-template-rows: 1fr;
    257   max-width: 100%; }
    258 
    259 .admin-grid-3 {
    260   display: grid;
    261   grid-gap: 50px;
    262   grid-template-columns: 1fr 1fr 1fr;
    263   grid-template-rows: 1fr;
    264   max-width: 100%; }
    265 
    266 #subcription-email {
    267   width: 500px;
    268   /* justify-content: center; */
    269   display: flex;
    270   padding: 10px;
    271   border: 1px solid #f0f0f1;
    272   border-radius: 50px;
    273   position: relative;
    274   display: grid;
    275   /* grid-gap: 50px; */
    276   grid-template-columns: 2fr 1fr;
    277   grid-template-rows: 1fr;
    278   max-width: 100%;
    279   box-shadow: 0 0 23px #0000001a;
    280   margin: 22px auto; }
    281 
    282 #subcription-email button.wp-block-search__button.has-icon {
    283   background: #2960f3;
    284   color: #fff;
    285   border-radius: 50px;
    286   border: none;
    287   cursor: pointer; }
    288 
    289 #subcription-email input[type="text"] {
    290   border: none;
    291   padding: 10px 25px;
    292   background: #f1f1f1;
    293   border-radius: 50px; }
    294 
    295 #subcription-email input[type="text"] {
    296   border: none;
    297   padding: 10px 15px;
    298   background: #fff;
    299   border-radius: 50px; }
    300 
    301 #icwcu-admin-about-page h2 {
    302   font-size: 2.5rem;
    303   line-height: 1.5;
    304   margin: 0; }
    305 
    306 #icwcu-admin-about-page h3 {
    307   font-size: 2rem;
    308   line-height: 1.5;
    309   margin-bottom: 0; }
    310 
    311 #icwcu-admin-about-page,
    312 #icwcu-admin-about-page p {
    313   color: #838383;
    314   font-size: 15px;
    315   line-height: 1.7; }
    316 
    317 #icwcu-admin-about-page img {
    318   max-width: 100%;
    319   vertical-align: middle; }
    320 
    321 .admin-col > img {
    322   box-shadow: 0 15px 24px #0000001c; }
    323 
    324 .icwcu-about-container.icwcu-admin-card.icwcu-admin-card-header {
    325   text-align: center; }
    326 
    327 .icwcu-card-header-button-group {
    328   text-align: center;
    329   display: flex;
    330   align-items: center;
    331   justify-content: center; }
    332 
    333 a.icwcu-template-kit-admin-button:hover,
    334 #icwcu-admin-about-page button.wp-block-search__button.has-icon:hover,
    335 a.icwcu-template-kit-admin-button.primary.large:hover {
    336   background: #4caf50; }
    337 
    338 a.icwcu-admin-button.premium-button:hover {
    339   background: #2960f3; }
    340 
    341 .admin-col {
    342   position: relative; }
    343 
    344 .icwcu-compare-table-subtitle {
    345   font-size: 16px; }
    346   .icwcu-compare-table-subtitle a {
    347     text-decoration: none; }
    348     .icwcu-compare-table-subtitle a i {
    349       font-size: 15px;
    350       line-height: 1.7; }
    351 
    352 .icwcu-compare-table table {
    353   border: 1px solid #e5e4e4;
    354   border-collapse: collapse;
    355   border-spacing: 0;
    356   margin-top: 2em;
    357   width: 100%; }
    358 
    359 .icwcu-compare-table table th {
    360   background-color: #F0F0F1;
    361   border-bottom: 1px solid #e5e4e4; }
    362 
    363 .icwcu-compare-table table th,
    364 .icwcu-compare-table table td {
    365   padding: 1em;
    366   text-align: center; }
    367 
    368 .icwcu-compare-table table tr:nth-child(2n) td {
    369   background: #fbfbfb; }
    370 
    371 .icwcu--getting-started--video-wrapper {
    372   min-width: 600px; }
    373   .icwcu--getting-started--video-wrapper video {
    374     width: 100%; }
    375 
    376 @media screen and (max-width: 782px) {
    377   #icwcu-admin-about-page .admin-grid-3, #icwcu-admin-about-page .admin-grid-2 {
    378     grid-template-columns: 1fr; }
    379   .icwcu-header-left.admin-col {
    380     text-align: center; }
    381   .icwcu-header-left h2 {
    382     font-size: 2rem; }
    383   .feature-list {
    384     grid-template-columns: 1fr; }
    385   #icwcu-admin-about-page, #setting-error-tgmpa {
    386     margin: 20px; }
    387   .icwcu-options-features > li {
    388     width: 100%; } }
    389 
    390 
    391 .icwcu-tab{
    392   cursor: pointer;
    393   padding:10px 20px;
    394   margin:0px 2px;
    395   background:#000;
    396   display:inline-block;
    397   color:#fff;
    398   border-radius:3px 3px 0px 0px;
    399   box-shadow: 0 0.5rem 0.8rem #00000080;
    400 }
    401 input.icwcu-radio {
    402     display: none;
    403 }
    404 .icwcu-panels{
    405   overflow:hidden;
    406   padding:20px; 
    407 }
    408 .icwcu-panel{
    409   display:none;
    410   animation: fadein .8s;
    411 }
    412 @keyframes fadein {
    413     from {
    414         opacity:0;
    415     }
    416     to {
    417         opacity:1;
    418     }
    419 }
    420 .icwcu-panel-title{
    421   font-size:1.5em;
    422   font-weight:bold
    423 }
    424 .radio{
    425   display:none;
    426 }
    427 #one:checked ~ .icwcu-panels #one-panel,
    428 #two:checked ~ .icwcu-panels #two-panel,
    429 #three:checked ~ .icwcu-panels #three-panel{
    430   display:block
    431 }
    432 #one:checked ~ .icwcu-tabs #one-tab,
    433 #two:checked ~ .icwcu-tabs #two-tab,
    434 #three:checked ~ .icwcu-tabs #three-tab{
    435   background:#fffffff6;
    436   color:#000;
    437   border-top: 3px solid #000;
    438 }
     1#icwcu_custom_services_meta_box .icwcu-tab { display: inline-block; padding: 12px; cursor: pointer; font-weight: 600; }
     2#icwcu_custom_services_meta_box .icwcu-tab-radio { margin-left: 12px; }
     3#icwcu_custom_services_meta_box .icwcu-editor-panel { display: none; padding: 12px; }
     4#icwcu_custom_services_meta_box #icwcu-tab-checkbox:checked ~ #icwcu-panel-checkbox,
     5#icwcu_custom_services_meta_box #icwcu-tab-radio:checked ~ #icwcu-panel-radio { display: block; }
     6#icwcu_custom_services_meta_box .icwcu-custom-service-field { border: 1px solid #dcdcde; padding: 8px 16px; margin-bottom: 12px; }
     7#icwcu_custom_services_meta_box label { display: inline-block; min-width: 155px; }
     8#icwcu_custom_services_meta_box input[type=text],
     9#icwcu_custom_services_meta_box input[type=number],
     10#icwcu_custom_services_meta_box textarea { width: min(100%, 360px); vertical-align: middle; }
     11.icwcu-guide { max-width: 980px; }
     12.icwcu-guide p, .icwcu-guide li { font-size: 14px; line-height: 1.8; }
     13.icwcu-guide h2 { margin-top: 30px; }
     14@media (max-width: 600px) { #icwcu_custom_services_meta_box label { display: block; } }
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/assets/css/icwcu-services-style.css

    r3264079 r3759386  
    1 h4.icwcu-checkbox-title {
    2     margin-bottom: 10px;
    3     margin-top: 0;
    4 }
    5 .icwcu-custom-services table {
    6     width: 100%;
    7 }
    8 .icwcu-custom-services {
    9     padding-bottom: 20px;
    10 }
    11 .icwcu-tooltip {
    12 position: relative;
    13     display: inline-block;
    14     cursor: pointer;
    15     color: #0073e6;
    16     font-weight: bold;
    17     vertical-align: bottom;
    18     padding: 0 10px;
    19 }
    20 
    21 .icwcu-tooltiptext {
    22         visibility: hidden;
    23     width: auto;
    24     min-width: 50px;
    25     max-height: 500px;
    26     background-color: #333333c7;
    27     color: #fff;
    28     border-radius: 6px;
    29     padding: 5px;
    30     position: absolute;
    31     z-index: 2;
    32     left: 100%;
    33     top: 50%;
    34     transform: translateY(-50%);
    35     opacity: 0;
    36     transition: opacity 0.3s ease;
    37     font-size: 12px;
    38     pointer-events: none;
    39 }
    40 .icwcu-tooltiptext::before {
    41     content: '';
    42     position: absolute;
    43     top: 50%;
    44     left: -9px;
    45     transform: translateY(-50%);
    46     border-width: 5px;
    47     border-style: solid;
    48     border-color: transparent #333333c7 transparent transparent;
    49 }
    50 input.icwcu-radio {
    51     margin-right: 5px;
    52 }
    53 /* Tooltip arrow */
    54 /*
    55 .icwcu-tooltiptext::after {
    56   content: '';
    57   position: absolute;
    58   top: 50%;
    59   right: -10px;
    60   transform: translateY(-50%);
    61   border-width: 5px;
    62   border-style: solid;
    63   border-color: transparent transparent transparent #333333c7;
    64 }*/
    65 
    66 /* Show tooltip when hovering over the trigger or tooltip content */
    67 .icwcu-tooltip:hover .icwcu-tooltiptext,
    68 .icwcu-tooltip .icwcu-tooltiptext:hover {
    69   visibility: visible;
    70   opacity: 1;
    71   pointer-events: auto; /* Allow interaction with tooltip when visible */
    72 }
    73 
    74 /* Hide tooltip when mouse leaves both the trigger and tooltip content */
    75 .icwcu-tooltip:not(:hover) .icwcu-tooltiptext:not(:hover) {
    76   visibility: hidden;
    77   opacity: 0;
    78   pointer-events: none; /* Disable interaction again */
    79 }
    80 
    81 
    82 .icwcu-checkbox {
    83   position: relative;
    84   margin: 0 1rem 0 0;
    85   cursor: pointer;
    86   z-index: 1;
    87 }
    88 
    89 .icwcu-checkbox:before {
    90   transition: all .3s ease-in-out;
    91   content: "";
    92   position: absolute;
    93   left: 0;
    94   z-index: 1;
    95   width: 1rem;
    96   height: 1rem;
    97   border: 2px solid #f2f2f2;
    98 }
    99 
    100 .icwcu-checkbox:checked:before {
    101   transform: rotate(-45deg);
    102   height: .5rem;
    103   border-color: green; /* Replace $green with actual value */
    104   border-top-style: none;
    105   border-right-style: none;
    106 }
    107 
    108 .icwcu-checkbox:after {
    109 content: "";
    110     position: absolute;
    111     left: 0;
    112     width: 1.1rem;
    113     height: 1.1rem;
    114     background: #ffffff;
    115     cursor: pointer;
    116     border: 1px solid #ddd;
    117 }
    118 span.icwcu-currency {
    119     margin-left: 20px;
    120 }
     1.icwcu-custom-services { border: 1px solid #ddd; padding: 16px; margin: 0 0 18px; min-width: 0; }
     2.icwcu-custom-services legend { font-weight: 600; padding: 0 6px; }
     3.icwcu-custom-services .icwcu-service { display: block; margin: 10px 0; overflow-wrap: anywhere; }
     4.icwcu-custom-services input { margin-right: 6px; }
     5.icwcu-custom-services .icwcu-currency { white-space: nowrap; margin-left: 8px; }
     6.icwcu-custom-services .icwcu-service-description { display: block; font-size: .85em; margin: 3px 0 0 24px; }
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/assets/js/custom-checkbox-frontend.js

    r3264079 r3759386  
    1 jQuery(document).ready(function($) {
    2  document.querySelectorAll('.icwcu-radio').forEach((radio) => {
     1/* Lets customers click a selected radio service again to unselect it (add-ons are optional). */
     2document.addEventListener('DOMContentLoaded', function () {
     3  document.querySelectorAll('input.icwcu-radio[name="icwcu_radio_service"]').forEach(function (radio) {
     4    radio.addEventListener('mousedown', function () {
     5      this.dataset.wasChecked = this.checked ? '1' : '';
     6    });
    37    radio.addEventListener('click', function () {
    4       const group = this.getAttribute('data-group');
    5       document.querySelectorAll(`.icwcu-radio[data-group="${group}"]`).forEach((btn) => {
    6         if (btn !== this) btn.checked = false;
    7       });
     8      if (this.dataset.wasChecked === '1') {
     9        this.checked = false;
     10        this.dataset.wasChecked = '';
     11      }
    812    });
    913  });
    10  });
     14});
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/assets/js/custom-services-meta-box.js

    r3264079 r3759386  
    1 jQuery(document).ready(function($) {
    2     var fieldIndex = customServicesMetaBox.initialFieldCount;
    3     var fieldIndexradio = customRadioServicesMetaBox.initialRadioFieldCount;
    4 
    5     $('#icwcu-add-more-fields').on('click', function() {
    6         fieldIndex++;
    7         $('#icwcu-custom-services-fields').append(
    8             '<div class="icwcu-custom-service-field" data-index="' + fieldIndex + '">' +
    9             '<p>' +
    10             '<label for="service_name_' + fieldIndex + '">' + fieldIndex + '. Service Name</label>' +
    11             '<input type="text" id="service_name_' + fieldIndex + '" name="icwcu_custom_services[' + fieldIndex + '][name]" value="" />' +
    12             '<label for="service_price_' + fieldIndex + '"> Service Price ' + customServicesMetaBox.currencySymbol + ' </label>' +
    13             '<input type="number" step="0.01" id="service_price_' + fieldIndex + '" name="icwcu_custom_services[' + fieldIndex + '][price]" value="" />' +
    14                          '<label for="service_desc_' + fieldIndex + '"> Service Description: ' + ' </label>' +
    15             '<textarea id="service_desc_' + fieldIndex + '" name="icwcu_custom_services[' + fieldIndex + '][desc]" value="" ></textarea>' +
    16             '</p>' +
    17             '</div>'
    18         );
    19     });
    20         $('#icwcu-add-moreradio-fields').on('click', function() {
    21         fieldIndexradio++;
    22         $('#icwcu-custom-radio-services-fields').append(
    23             '<div class="icwcu-custom-service-field" data-index="' + fieldIndexradio + '">' +
    24             '<p>' +
    25             '<label for="radioservice_name_' + fieldIndexradio + '">' + fieldIndexradio + '. Service Name</label>' +
    26             '<input type="text" id="radioservice_name_' + fieldIndexradio + '" name="icwcu_custom_servicesradio[' + fieldIndexradio + '][name]" value="" />' +
    27             '<label for="radioservice_price_' + fieldIndexradio + '"> Service Price ' + customServicesMetaBox.currencySymbol + ' </label>' +
    28             '<input type="number" step="0.01" id="radioservice_price_' + fieldIndexradio + '" name="icwcu_custom_servicesradio[' + fieldIndexradio + '][price]" value="" />' +
    29                          '<label for="radioservice_desc_' + fieldIndexradio + '"> Service Description: ' + ' </label>' +
    30             '<textarea id="radioservice_desc_' + fieldIndexradio + '" name="icwcu_custom_servicesradio[' + fieldIndexradio + '][desc]" value="" ></textarea>' +
    31             '</p>' +
    32             '</div>'
    33         );
     1document.addEventListener('DOMContentLoaded', function () {
     2    document.querySelectorAll('#icwcu_custom_services_meta_box .icwcu-add-fields').forEach(function (button) {
     3        button.addEventListener('click', function () {
     4            var fields = button.previousElementSibling;
     5            var index = fields.children.length + 1;
     6            var row = document.createElement('div');
     7            row.className = 'icwcu-custom-service-field';
     8            row.dataset.index = index;
     9            ['name', 'price', 'desc'].forEach(function (key) {
     10                var p = document.createElement('p');
     11                var label = document.createElement('label');
     12                var input = document.createElement(key === 'desc' ? 'textarea' : 'input');
     13                input.id = fields.dataset.meta + '-' + index + '-' + key;
     14                input.name = fields.dataset.meta + '[' + index + '][' + key + ']';
     15                if (key !== 'desc') input.type = key === 'price' ? 'number' : 'text';
     16                if (key === 'price') { input.min = '0'; input.step = 'any'; }
     17                label.htmlFor = input.id;
     18                label.textContent = icwcuFields[key];
     19                p.appendChild(label);
     20                p.appendChild(input);
     21                row.appendChild(p);
     22            });
     23            fields.appendChild(row);
     24            row.querySelector('input').focus();
     25        });
    3426    });
    3527});
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/custom-checkbox-ultimate-for-woocommerce.php

    r3264079 r3759386  
    44 * Plugin URI:        https://www.insertcart.com/insertcart-custom-checkbox-ultimate-for-woocommerce-documentation/
    55 * Description:       Add customizable checkbox options on WooCommerce product pages for additional services and charges.
    6  * Version:           1.3
     6 * Version:           1.4
    77 * Requires at least: 6.3
    88 * Requires PHP:      7.2
    … …  
    1212 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
    1313 * Text Domain:       custom-checkbox-ultimate-for-woocommerce
     14 * Domain Path:       /languages
    1415 * Requires Plugins:  woocommerce
     16 * WC requires at least: 8.0
     17 * WC tested up to:   10.3
    1518 */
    1619
    … …  
    1922}
    2023
    21 define( 'ICWCU_PLUGIN_VERSION', '1.3' );
     24define( 'ICWCU_PLUGIN_VERSION', '1.4' );
     25define( 'ICWCU_PLUGIN_FILE', __FILE__ );
     26define( 'ICWCU_DOCUMENTATION_URL', 'https://www.insertcart.com/insertcart-custom-checkbox-ultimate-for-woocommerce-documentation/' );
    2227
    2328// Include our enqueue file (even if empty)
    24 require_once 'includes/enqueue.php';
     29require_once plugin_dir_path( __FILE__ ) . 'includes/enqueue.php';
    2530
    2631// Initialize our plugin classes
    27 require_once 'includes/class-icwcu-init.php';
    28 $icwcu_init = new ICWCU_Init();
     32require_once plugin_dir_path( __FILE__ ) . 'includes/class-icwcu-init.php';
     33add_action( 'plugins_loaded', 'icwcu_initialize' );
     34function icwcu_initialize() {
     35    if ( class_exists( 'WooCommerce' ) ) {
     36        new ICWCU_Init();
     37    } else {
     38        add_action( 'admin_notices', 'icwcu_dependency_notice' );
     39    }
     40}
     41function icwcu_dependency_notice() {
     42    if ( current_user_can( 'activate_plugins' ) ) {
     43        echo '<div class="notice notice-warning"><p>' . esc_html__( 'Custom Checkbox Ultimate requires WooCommerce to be installed and active.', 'custom-checkbox-ultimate-for-woocommerce' ) . '</p></div>';
     44    }
     45}
     46require_once __DIR__ . '/includes/class-icwcu-help.php';
     47ICWCU_Help::hooks();
    2948
    3049// Create a Plugin menu in the dashboard
    … …  
    3453        esc_html__( 'IC Woo Ultimate', 'custom-checkbox-ultimate-for-woocommerce' ),
    3554        esc_html__( 'IC Woo Ultimate', 'custom-checkbox-ultimate-for-woocommerce' ),
    36         'manage_options',
     55        'manage_woocommerce',
    3756        'ic-woo-ultimate',
    3857        'icwcu_about_page',
    39         'data:image/svg+xml;base64,' . base64_encode( icwcu_get_custom_svg_icon() ),
     58        'dashicons-yes-alt',
    4059        100
    4160    );
    4261}
    4362function icwcu_about_page() {
     63    if ( ! current_user_can( 'manage_woocommerce' ) ) {
     64        wp_die( esc_html__( 'You cannot access this setup guide.', 'custom-checkbox-ultimate-for-woocommerce' ), '', array( 'response' => 403 ) );
     65    }
    4466    include_once plugin_dir_path( __FILE__ ) . 'includes/plugin-info.php';
    4567}
     68
     69// Declare compatibility with WooCommerce High-Performance Order Storage (HPOS) and Cart/Checkout blocks.
     70add_action( 'before_woocommerce_init', 'icwcu_declare_wc_compatibility' );
     71function icwcu_declare_wc_compatibility() {
     72    if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) {
     73        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', ICWCU_PLUGIN_FILE, true );
     74        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', ICWCU_PLUGIN_FILE, true );
     75    }
     76}
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/includes/class-icwcu-cart.php

    r3264079 r3759386  
    55
    66class ICWCU_Cart {
     7
     8    /**
     9     * Original product prices, so add-ons are never added twice
     10     * when cart totals are calculated more than once per request.
     11     *
     12     * @var array
     13     */
     14    private $base_prices = array();
     15
    716    public function __construct() {
    8         add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_custom_fields_to_cart_item' ), 10, 2 );
    9         add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_customradio_fields_to_cart_item' ), 10, 2 );
    10         add_filter( 'woocommerce_get_item_data', array( $this, 'display_custom_fields_in_cart' ), 10, 2 );
    11        
    12         add_action( 'woocommerce_before_calculate_totals', array( $this, 'adjust_cart_item_price' ), 10, 1 );
    13         add_action( 'woocommerce_before_calculate_totals', array( $this, 'adjustradio_cart_item_price' ), 10, 1 );
    14                 //add_action( 'woocommerce_cart_calculate_fees', array( $this, 'icwcu_adjust_cart_total' ), 10, 1 );
    15                
    16                 add_action( 'woocommerce_after_calculate_totals', array( $this, 'adjust_cart_item_priceless' ), 10, 1 );
    17                 add_action( 'woocommerce_after_calculate_totals', array( $this, 'adjustradio_cart_item_priceless' ), 10, 1 );
    18        // add_action( 'woocommerce_cart_item_price', array( $this, 'adjust_cart_item_price' ), 10, 1 );
     17        add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_services_to_cart_item' ), 10, 2 );
     18        add_filter( 'woocommerce_get_item_data', array( $this, 'display_services_in_cart' ), 10, 2 );
     19        add_action( 'woocommerce_before_calculate_totals', array( $this, 'adjust_cart_item_prices' ), 20, 1 );
     20    }
    1921
    20     }
    21    
    22     public function add_custom_fields_to_cart_item( $cart_item_data, $product_id ) {   
    23         if ( ! isset( $_POST['icwcu_custom_checkbox_nonce'] ) || ! wp_verify_nonce( sanitize_text_field(wp_unslash( $_POST['icwcu_custom_checkbox_nonce']) ), 'icwcu_custom_checkbox_nonce_action' ) ) {
    24             return $cart_item_data;
    25         }
    26         $custom_services = get_post_meta( $product_id, 'icwcu_custom_services', true );
    27         if ( ! is_array( $custom_services ) ) {
    28             $custom_services = array();
    29         }
    30    
    31         if ( ! empty( $custom_services ) ) {
    32             foreach ( $custom_services as $service ) {
    33                 $service_key = sanitize_title( $service['name'] );
    34                 if ( isset( $_POST[ $service_key ] ) ) {
    35                     $price = floatval( wp_unslash( $_POST[ $service_key ] ) );
    36                     $cart_item_data[ $service_key ] = ( $price >= 0 ) ? $price : 0;
    37                 }
    38             }
    39         }
    40         return $cart_item_data;
    41     }
    42    
    43     public function add_customradio_fields_to_cart_item( $cart_item_data, $product_id ) {
    44         if ( ! isset( $_POST['icwcu_custom_radio_nonce'] ) || ! wp_verify_nonce( sanitize_text_field(wp_unslash( $_POST['icwcu_custom_radio_nonce']) ), 'icwcu_custom_radio_nonce_action' ) ) {
    45             return $cart_item_data;
    46         }
    47         $custom_servicesradio = get_post_meta( $product_id, 'icwcu_custom_servicesradio', true );
    48         if ( ! is_array( $custom_servicesradio ) ) {
    49             $custom_servicesradio = array();
    50         }
    51    
    52         if ( ! empty( $custom_servicesradio ) ) {
    53             foreach ( $custom_servicesradio as $serviceradio ) {
    54                 $service_keyradio = sanitize_title( $serviceradio['name'] );       
    55                 if ( isset( $_POST[ $service_keyradio ] ) ) {
    56                     $price = floatval( wp_unslash( $_POST[ $service_keyradio ] ) );
    57                     $cart_item_data[ $service_keyradio ] = ( $price >= 0 ) ? $price : 0;
    58                 }
     22    /**
     23     * Save the selected services on the cart item.
     24     * Prices are always read from the product settings, never from the request.
     25     */
     26    public function add_services_to_cart_item( $cart_item_data, $product_id ) {
     27        $selected = array();
    5928
     29        // Checkbox services.
     30        if ( isset( $_POST['icwcu_custom_checkbox_nonce'] )
     31            && is_string( $_POST['icwcu_custom_checkbox_nonce'] )
     32            && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['icwcu_custom_checkbox_nonce'] ) ), 'icwcu_custom_checkbox_nonce_action' )
     33            && ! empty( $_POST['icwcu_services'] ) && is_array( $_POST['icwcu_services'] ) ) {
    6034
     35            $services = icwcu_get_product_services( $product_id, 'checkbox' );
     36            $posted   = array_map( 'sanitize_title', array_filter( wp_unslash( $_POST['icwcu_services'] ), 'is_string' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Non-string entries are discarded and every remaining entry passes through sanitize_title.
    6137
    62             }
    63         }
    64         return $cart_item_data;
    65     }
    66    
    67     public function display_custom_fields_in_cart( $item_data, $cart_item ) {
    68         $custom_services = get_post_meta( $cart_item['product_id'], 'icwcu_custom_services', true );
    69         $custom_servicesradio = get_post_meta( $cart_item['product_id'], 'icwcu_custom_servicesradio', true );
    70    
    71         if ( ! empty( $custom_services ) ) {
    72             foreach ( $custom_services as $service ) {
    73                 $service_key = sanitize_title( $service['name'] );
    74                 if ( isset( $cart_item[ $service_key ] ) ) {
    75                     $item_data[] = array(
    76                         'name'  => $service['name'],
    77                         'value' => get_woocommerce_currency_symbol() . esc_html( $cart_item[ $service_key ] )
     38            foreach ( array_unique( $posted ) as $key ) {
     39                if ( isset( $services[ $key ] ) ) {
     40                    $selected[] = array(
     41                        'name'  => $services[ $key ]['name'],
     42                        'price' => $services[ $key ]['price'],
    7843                    );
    7944                }
    8045            }
    8146        }
    82    
    83         if ( ! empty( $custom_servicesradio ) ) {
    84             foreach ( $custom_servicesradio as $serviceradio ) {
    85                 $service_keyradio = sanitize_title( $serviceradio['name'] );
    86                 if ( isset( $cart_item[ $service_keyradio ] ) ) {
    87                     $item_data[] = array(
    88                         'name'  => $serviceradio['name'],
    89                         'value' => get_woocommerce_currency_symbol() . esc_html( $cart_item[ $service_keyradio ] )
     47
     48        // Radio service (only one can be chosen).
     49        if ( isset( $_POST['icwcu_custom_radio_nonce'] )
     50            && is_string( $_POST['icwcu_custom_radio_nonce'] )
     51            && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['icwcu_custom_radio_nonce'] ) ), 'icwcu_custom_radio_nonce_action' )
     52            && ! empty( $_POST['icwcu_radio_service'] ) && is_string( $_POST['icwcu_radio_service'] ) ) {
     53
     54            $services = icwcu_get_product_services( $product_id, 'radio' );
     55            $key      = sanitize_title( wp_unslash( $_POST['icwcu_radio_service'] ) );
     56
     57            if ( isset( $services[ $key ] ) ) {
     58                $selected[] = array(
     59                    'name'  => $services[ $key ]['name'],
     60                    'price' => $services[ $key ]['price'],
     61                );
     62            }
     63        }
     64
     65        if ( ! empty( $selected ) ) {
     66            $cart_item_data['icwcu_services'] = $selected;
     67        }
     68
     69        return $cart_item_data;
     70    }
     71
     72    /**
     73     * Get the services selected for a cart item.
     74     *
     75     * @param array $cart_item Cart item.
     76     * @return array List of array( 'name' => string, 'price' => float ).
     77     */
     78    public static function get_cart_item_services( $cart_item ) {
     79        if ( ! empty( $cart_item['icwcu_services'] ) && is_array( $cart_item['icwcu_services'] ) ) {
     80            return array_values( array_filter( $cart_item['icwcu_services'], function( $service ) {
     81                return is_array( $service ) && isset( $service['name'], $service['price'] ) && is_string( $service['name'] ) && is_numeric( $service['price'] ) && is_finite( (float) $service['price'] ) && $service['price'] >= 0;
     82            } ) );
     83        }
     84
     85        // Items added to the cart by version 1.3 or older.
     86        $selected = array();
     87        if ( empty( $cart_item['product_id'] ) ) {
     88            return $selected;
     89        }
     90        foreach ( array( 'checkbox', 'radio' ) as $type ) {
     91            foreach ( icwcu_get_product_services( $cart_item['product_id'], $type ) as $key => $service ) {
     92                if ( isset( $cart_item[ $key ] ) && is_numeric( $cart_item[ $key ] ) ) {
     93                    $selected[] = array(
     94                        'name'  => $service['name'],
     95                        'price' => $service['price'],
    9096                    );
     97                    if ( 'radio' === $type ) {
     98                        break;
     99                    }
    91100                }
    92101            }
    93102        }
    94    
     103        return $selected;
     104    }
     105
     106    /**
     107     * Show the selected services under the product name in cart and checkout.
     108     */
     109    public function display_services_in_cart( $item_data, $cart_item ) {
     110        foreach ( self::get_cart_item_services( $cart_item ) as $service ) {
     111            $item_data[] = array(
     112                'name'  => $service['name'],
     113                'value' => icwcu_format_price( $service['price'] ),
     114            );
     115        }
    95116        return $item_data;
    96117    }
    97    
    98118
    99 
    100     private function adjust_cart_item_price_generic( $cart, $meta_key, $add = true ) {
    101         if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
     119    /**
     120     * Add the selected services to the item price.
     121     */
     122    public function adjust_cart_item_prices( $cart ) {
     123        if ( is_admin() && ! wp_doing_ajax() ) {
    102124            return;
    103125        }
    104         foreach ( $cart->get_cart() as $cart_item ) {
    105             $services = get_post_meta( $cart_item['product_id'], $meta_key, true );
    106             if ( ! is_array( $services ) ) {
    107                 $services = array();
     126
     127        foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
     128            $services = self::get_cart_item_services( $cart_item );
     129            if ( empty( $services ) || empty( $cart_item['data'] ) ) {
     130                continue;
    108131            }
    109             if ( ! empty( $services ) ) {
    110                 $additional_cost = 0;
    111                 foreach ( $services as $service ) {
    112                     $service_key = sanitize_title( $service['name'] );
    113                     if ( isset( $cart_item[ $service_key ] ) ) {
    114                         $additional_cost += floatval( $cart_item[ $service_key ] );
    115                     }
    116                 }
    117                 if ( $additional_cost > 0 ) {
    118                     $new_price = $add ?
    119                         $cart_item['data']->get_price() + $additional_cost :
    120                         $cart_item['data']->get_price() - $additional_cost;
    121                     $cart_item['data']->set_price( $new_price );
    122                 }
     132
     133            $extra = 0;
     134            foreach ( $services as $service ) {
     135                $extra += (float) $service['price'];
    123136            }
     137            if ( $extra <= 0 ) {
     138                continue;
     139            }
     140
     141            $product = $cart_item['data'];
     142            $id      = spl_object_hash( $product );
     143            if ( ! isset( $this->base_prices[ $id ] ) ) {
     144                $this->base_prices[ $id ] = (float) $product->get_price( 'edit' );
     145            }
     146
     147            $product->set_price( $this->base_prices[ $id ] + $extra );
    124148        }
    125149    }
    126    
    127     public function adjust_cart_item_price( $cart ) {
    128         $this->adjust_cart_item_price_generic( $cart, 'icwcu_custom_services', true );
    129     }
    130    
    131     public function adjustradio_cart_item_price( $cart ) {
    132         $this->adjust_cart_item_price_generic( $cart, 'icwcu_custom_servicesradio', true );
    133     }
    134    
    135     public function adjust_cart_item_priceless( $cart ) {
    136         $this->adjust_cart_item_price_generic( $cart, 'icwcu_custom_services', false );
    137     }
    138    
    139     public function adjustradio_cart_item_priceless( $cart ) {
    140         $this->adjust_cart_item_price_generic( $cart, 'icwcu_custom_servicesradio', false );
    141     }
    142 
    143         /********
    144         ** Substract from item price
    145         ****/
    146        
    147    
    148        
    149        
    150         public function icwcu_adjust_cart_total( $cart ) {
    151                 if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
    152                         return;
    153                 }
    154 
    155                 $additional_cost = 0;
    156 
    157                 foreach ( $cart->get_cart() as $cart_item ) {
    158                         $custom_services = get_post_meta( $cart_item['product_id'], 'icwcu_custom_services', true );
    159                         if ( is_array( $custom_services ) && ! empty( $custom_services ) ) {
    160                                 foreach ( $custom_services as $service ) {
    161                                         $service_key = sanitize_title( $service['name'] );
    162                                         if ( isset( $cart_item[ $service_key ] ) ) {
    163                                                 $additional_cost += floatval( $cart_item[ $service_key ] );
    164                                         }
    165                                 }
    166                         }
    167 
    168                         $custom_servicesradio = get_post_meta( $cart_item['product_id'], 'icwcu_custom_servicesradio', true );
    169                         if ( is_array( $custom_servicesradio ) && ! empty( $custom_servicesradio ) ) {
    170                                 foreach ( $custom_servicesradio as $serviceradio ) {
    171                                         $service_keyradio = sanitize_title( $serviceradio['name'] );
    172                                         if ( isset( $cart_item[ $service_keyradio ] ) ) {
    173                                                 $additional_cost += floatval( $cart_item[ $service_keyradio ] );
    174                                         }
    175                                 }
    176                         }
    177                 }
    178 
    179                 if ( $additional_cost > 0 ) {
    180                         WC()->cart->add_fee( __( 'Additional Services', 'custom-checkbox-ultimate-for-woocommerce' ), $additional_cost );
    181                 }
    182         }
    183 
    184150}
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/includes/class-icwcu-init.php

    r3264079 r3759386  
    77    public function __construct() {
    88        // Include required class files and helpers
    9         require_once 'class-icwcu-meta-box.php';
    10         require_once 'class-icwcu-cart.php';
    11         require_once 'class-icwcu-order.php';
    12         require_once 'class-icwcu-settings.php';
    13         require_once 'helpers.php';
     9        require_once __DIR__ . '/class-icwcu-meta-box.php';
     10        require_once __DIR__ . '/class-icwcu-cart.php';
     11        require_once __DIR__ . '/class-icwcu-order.php';
     12        require_once __DIR__ . '/class-icwcu-settings.php';
     13        require_once __DIR__ . '/helpers.php';
    1414       
    1515        // Instantiate the classes to initialize functionality
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/includes/class-icwcu-meta-box.php

    r3264079 r3759386  
    11<?php
    2 if ( ! defined( 'ABSPATH' ) ) {
    3     exit;
    4 }
     2defined( 'ABSPATH' ) || exit;
    53
     4/** Product editor and classic product form fields. */
    65class ICWCU_Meta_Box {
    76    public function __construct() {
    87        add_action( 'add_meta_boxes', array( $this, 'add_custom_services_meta_box' ) );
    9         add_action( 'save_post', array( $this, 'save_custom_services_meta_box_data' ) );
     8        add_action( 'save_post_product', array( $this, 'save_custom_services_meta_box_data' ) );
    109        add_action( 'admin_enqueue_scripts', array( $this, 'meta_box_scripts' ) );
    1110        add_action( 'woocommerce_before_add_to_cart_button', array( $this, 'add_custom_fields' ) );
    1211    }
    13    
    1412    public function add_custom_services_meta_box() {
    15         add_meta_box(
    16             'icwcu_custom_services_meta_box',
    17             icwcu_get_custom_svg_icon() . 'Custom Services - IC Woo Ultimate',
    18             array( $this, 'display_custom_services_meta_box' ),
    19             'product',
    20             'normal',
    21             'high'
    22         );
     13        add_meta_box( 'icwcu_custom_services_meta_box', __( 'Custom Services - IC Woo Ultimate', 'custom-checkbox-ultimate-for-woocommerce' ), array( $this, 'display_custom_services_meta_box' ), 'product', 'normal', 'high' );
    2314    }
    24    
    2515    public function display_custom_services_meta_box( $post ) {
    26         $custom_services = get_post_meta( $post->ID, 'icwcu_custom_services', true );
    27         if ( ! is_array( $custom_services ) ) {
    28             $custom_services = array();
     16        wp_nonce_field( 'icwcu_custom_services_meta_box_nonce', 'icwcu_custom_services_meta_box_nonce' );
     17        $help_url = current_user_can( 'manage_woocommerce' ) ? ICWCU_Help::url() : ICWCU_DOCUMENTATION_URL;
     18        echo '<p>' . esc_html__( 'Add optional services per product unit. Use checkboxes for multiple choices and radio buttons for one choice.', 'custom-checkbox-ultimate-for-woocommerce' ) . ' <a href="' . esc_url( $help_url ) . '">' . esc_html__( 'Setup guide', 'custom-checkbox-ultimate-for-woocommerce' ) . '</a></p>';
     19        echo '<input class="icwcu-tab-radio" id="icwcu-tab-checkbox" name="icwcu-editor-tab" type="radio" checked><label class="icwcu-tab" for="icwcu-tab-checkbox">' . esc_html__( 'Checkbox Type', 'custom-checkbox-ultimate-for-woocommerce' ) . '</label>';
     20        echo '<input class="icwcu-tab-radio" id="icwcu-tab-radio" name="icwcu-editor-tab" type="radio"><label class="icwcu-tab" for="icwcu-tab-radio">' . esc_html__( 'Radio Type', 'custom-checkbox-ultimate-for-woocommerce' ) . '</label>';
     21        foreach ( array( 'checkbox', 'radio' ) as $type ) {
     22            $meta = 'radio' === $type ? 'icwcu_custom_servicesradio' : 'icwcu_custom_services';
     23            $title_meta = 'radio' === $type ? 'icwcu_custom_radioservices_title' : 'icwcu_custom_services_title';
     24            $saved = get_post_meta( $post->ID, $meta, true );
     25            $services = is_array( $saved ) ? array_values( array_filter( $saved, 'is_array' ) ) : array();
     26            echo '<div class="icwcu-editor-panel" id="icwcu-panel-' . esc_attr( $type ) . '">';
     27            echo '<p><label for="' . esc_attr( $title_meta ) . '">' . esc_html__( 'Custom Services Title:', 'custom-checkbox-ultimate-for-woocommerce' ) . '</label> <input type="text" id="' . esc_attr( $title_meta ) . '" name="' . esc_attr( $title_meta ) . '" value="' . esc_attr( get_post_meta( $post->ID, $title_meta, true ) ) . '"></p>';
     28            echo '<div class="icwcu-service-fields" data-meta="' . esc_attr( $meta ) . '">';
     29            for ( $i = 0; $i < max( 5, count( $services ) ); $i++ ) {
     30                $service = isset( $services[ $i ] ) ? $services[ $i ] : array();
     31                echo '<div class="icwcu-custom-service-field" data-index="' . esc_attr( $i + 1 ) . '">';
     32                foreach ( self::labels() as $key => $label ) {
     33                    $field = $meta . '[' . ( $i + 1 ) . '][' . $key . ']';
     34                    $id = $meta . '-' . ( $i + 1 ) . '-' . $key;
     35                    $value = isset( $service[ $key ] ) && is_scalar( $service[ $key ] ) ? $service[ $key ] : '';
     36                    echo '<p><label for="' . esc_attr( $id ) . '">' . esc_html( $label ) . '</label> ';
     37                    if ( 'desc' === $key ) {
     38                        echo '<textarea id="' . esc_attr( $id ) . '" name="' . esc_attr( $field ) . '">' . esc_textarea( $value ) . '</textarea>';
     39                    } else {
     40                        echo '<input type="' . ( 'price' === $key ? 'number' : 'text' ) . '" ' . ( 'price' === $key ? 'min="0" step="any" ' : '' ) . 'id="' . esc_attr( $id ) . '" name="' . esc_attr( $field ) . '" value="' . esc_attr( $value ) . '">';
     41                    }
     42                    echo '</p>';
     43                }
     44                echo '</div>';
     45            }
     46            echo '</div><button type="button" class="button icwcu-add-fields">+ ' . esc_html__( 'Add More Fields', 'custom-checkbox-ultimate-for-woocommerce' ) . '</button></div>';
    2947        }
    30         $custom_servicesradio = get_post_meta( $post->ID, 'icwcu_custom_servicesradio', true );
    31         if ( ! is_array( $custom_servicesradio ) ) {
    32             $custom_servicesradio = array();
    33         }
    34    
    35         wp_nonce_field( 'icwcu_custom_services_meta_box_nonce', 'icwcu_custom_services_meta_box_nonce' );
    36         $custom_services_title = get_post_meta( $post->ID, 'icwcu_custom_services_title', true );
    37         $custom_radioservices_title = get_post_meta( $post->ID, 'icwcu_custom_radioservices_title', true );
    38    
    39         echo '<input class="radio icwcu-radio" id="one" name="group" type="radio" checked>
    40               <input class="radio icwcu-radio" id="two" name="group" type="radio">
    41               <div class="icwcu-tabs">
    42                   <label class="icwcu-tab" id="one-tab" for="one">'.esc_html__( 'Checkbox Type', 'custom-checkbox-ultimate-for-woocommerce' ).'</label>
    43                   <label class="icwcu-tab" id="two-tab" for="two">'.esc_html__( 'Radio Type', 'custom-checkbox-ultimate-for-woocommerce' ).'</label>
    44               </div>';
    45    
    46         echo '<div class="icwcu-panels">';
    47    
    48         // Checkbox Type Panel
    49         echo '<div class="icwcu-panel" id="one-panel">';
    50         echo '<div id="icwcu-custom-services-title">';
    51         echo '<p><label for="icwcu_custom_services_title">' . esc_html__( 'Custom Services Title:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    52         echo '<input type="text" id="icwcu_custom_services_title" name="icwcu_custom_services_title" value="' . esc_attr( sanitize_text_field($custom_services_title) ) . '" /></p>';
    53         echo '</div>';
    54    
    55         $default_fields = 5;
    56         $field_count = max( count( $custom_services ), $default_fields );
    57    
    58         echo '<div id="icwcu-custom-services-fields">';
    59         for ( $i = 1; $i <= $field_count; $i++ ) {
    60             $service_name = isset( $custom_services[$i]['name'] ) ? $custom_services[$i]['name'] : '';
    61            // $service_price = isset( $custom_services[$i]['price'] ) ? $custom_services[$i]['price'] : '';
    62            $service_price = isset( $custom_services[$i]['price'] ) ? floatval( $custom_services[$i]['price'] ) : 0;
    63             $service_desc = isset( $custom_services[$i]['desc'] ) ? $custom_services[$i]['desc'] : '';
    64 
    65             echo '<div class="icwcu-custom-service-field" data-index="' . esc_html( $i ) . '">';
    66             echo '<p><label for="service_name_' . esc_html( $i ) . '">' . esc_html( $i ) . '. ' . esc_html__( 'Service Name:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    67             echo '<input type="text" id="service_name_' . esc_html( $i ) . '" name="icwcu_custom_services[' . esc_html( $i ) . '][name]" value="' . esc_attr(sanitize_text_field( $service_name) ) . '" />';
    68             echo '<label for="service_price_' . esc_html( $i ) . '">' . esc_html__( 'Service Price', 'custom-checkbox-ultimate-for-woocommerce' ) . ' ' . esc_html( get_woocommerce_currency_symbol() ) . ' </label>';
    69             echo '<input type="number" step="0.01" id="service_price_' . esc_html( $i ) . '" name="icwcu_custom_services[' . esc_html( $i ) . '][price]" value="' . esc_attr( $service_price ) . '" />';
    70             echo '<label for="service_desc_' . esc_html( $i ) . '">' . esc_html__( 'Service Description:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    71             echo '<textarea id="service_desc_' . esc_html( $i ) . '" name="icwcu_custom_services[' . esc_html( $i ) . '][desc]">' . esc_textarea( sanitize_textarea_field($service_desc) ) . '</textarea></p>';
    72             echo '</div>';
    73         }
    74         echo '</div>';
    75         echo '<button type="button" id="icwcu-add-more-fields" class="button">+ ' . esc_html__( 'Add More Fields', 'custom-checkbox-ultimate-for-woocommerce' ) . '</button>';
    76         echo '</div>';
    77    
    78         // Radio Type Panel
    79         echo '<div class="icwcu-panel" id="two-panel">';
    80         echo '<div id="icwcu-custom-services-title">';
    81         echo '<p><label for="icwcu_custom_radioservices_title">' . esc_html__( 'Custom Radio Services Title:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    82         echo '<input type="text" id="icwcu_custom_radioservices_title" name="icwcu_custom_radioservices_title" value="' . esc_attr( sanitize_text_field($custom_radioservices_title) ) . '" /></p>';
    83         echo '</div>';
    84    
    85         $default_fieldsradio = 5;
    86         $field_countradio = max( count( $custom_servicesradio ), $default_fieldsradio );
    87    
    88         echo '<div id="icwcu-custom-radio-services-fields">';
    89         for ( $i = 1; $i <= $field_countradio; $i++ ) {
    90             $radioservice_name = isset( $custom_servicesradio[$i]['name'] ) ? $custom_servicesradio[$i]['name'] : '';
    91           //  $radioservice_price = isset( $custom_servicesradio[$i]['price'] ) ? $custom_servicesradio[$i]['price'] : '';
    92             $radioservice_price = isset( $custom_servicesradio[$i]['price'] ) ? floatval( $custom_servicesradio[$i]['price'] ) : 0;
    93             $radioservice_desc = isset( $custom_servicesradio[$i]['desc'] ) ? $custom_servicesradio[$i]['desc'] : '';
    94             echo '<div class="icwcu-custom-service-field" data-index="' . esc_html( $i ) . '">';
    95             echo '<p><label for="radioservice_name_' . esc_html( $i ) . '">' . esc_html( $i ) . '. ' . esc_html__( 'Service Name:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    96             echo '<input type="text" id="radioservice_name_' . esc_html( $i ) . '" name="icwcu_custom_servicesradio[' . esc_html( $i ) . '][name]" value="' . esc_attr( sanitize_text_field($radioservice_name) ) . '" />';
    97             echo '<label for="radioservice_price_' . esc_html( $i ) . '">' . esc_html__( 'Service Price', 'custom-checkbox-ultimate-for-woocommerce' ) . ' ' . esc_html( get_woocommerce_currency_symbol() ) . ' </label>';
    98             echo '<input type="number" step="0.01" id="radioservice_price_' . esc_html( $i ) . '" name="icwcu_custom_servicesradio[' . esc_html( $i ) . '][price]" value="' . esc_attr(  $radioservice_price  )  . '" />';
    99             echo '<label for="radioservice_desc_' . esc_html( $i ) . '">' . esc_html__( 'Service Description:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    100             echo '<textarea id="radioservice_desc_' . esc_html( $i ) . '" name="icwcu_custom_servicesradio[' . esc_html( $i ) . '][desc]">' . esc_textarea( sanitize_textarea_field($radioservice_desc )) . '</textarea></p>';
    101             echo '</div>';
    102         }
    103         echo '</div>';
    104         echo '<button type="button" id="icwcu-add-moreradio-fields" class="button">+ ' . esc_html__( 'Add More Fields', 'custom-checkbox-ultimate-for-woocommerce' ) . '</button>';
    105         echo '</div>';
    106    
    107         echo '</div>'; // End of panels
    10848    }
    109    
     49    public static function labels() {
     50        return array( 'name' => __( 'Service Name:', 'custom-checkbox-ultimate-for-woocommerce' ), 'price' => __( 'Service Price:', 'custom-checkbox-ultimate-for-woocommerce' ), 'desc' => __( 'Service Description:', 'custom-checkbox-ultimate-for-woocommerce' ) );
     51    }
    11052    public function meta_box_scripts( $hook ) {
    111         if ( 'post.php' != $hook && 'post-new.php' != $hook ) {
     53        $screen = get_current_screen();
     54        if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) || ! $screen || 'product' !== $screen->post_type ) {
    11255            return;
    11356        }
    114    
    115         wp_enqueue_script( 'icwcu-custom-services-meta-box', plugin_dir_url( __FILE__ ) . '../assets/js/custom-services-meta-box.js', array( 'jquery' ), ICWCU_PLUGIN_VERSION, true );
    116         $initial_field_count = max( 5, count( (array) get_post_meta( get_the_ID(), 'icwcu_custom_services', true ) ) );
    117         $initial_radiofield_count = max( 5, count( (array) get_post_meta( get_the_ID(), 'icwcu_custom_servicesradio', true ) ) );
    118         wp_localize_script( 'icwcu-custom-services-meta-box', 'customServicesMetaBox', array(
    119             'initialFieldCount' => $initial_field_count,
    120             'currencySymbol'    => get_woocommerce_currency_symbol(),
    121         ) );
    122         wp_localize_script( 'icwcu-custom-services-meta-box', 'customRadioServicesMetaBox', array(
    123             'initialRadioFieldCount' => $initial_radiofield_count
    124         ) );
     57        wp_enqueue_script( 'icwcu-custom-services-meta-box', plugins_url( 'assets/js/custom-services-meta-box.js', ICWCU_PLUGIN_FILE ), array(), ICWCU_PLUGIN_VERSION, true );
     58        wp_localize_script( 'icwcu-custom-services-meta-box', 'icwcuFields', self::labels() );
    12559    }
    126    
    12760    public function save_custom_services_meta_box_data( $post_id ) {
    128         if ( ! isset( $_POST['icwcu_custom_services_meta_box_nonce'] ) ||
    129             ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['icwcu_custom_services_meta_box_nonce'] ) ), 'icwcu_custom_services_meta_box_nonce' ) ) {
     61        if ( ! isset( $_POST['icwcu_custom_services_meta_box_nonce'] ) || ! is_string( $_POST['icwcu_custom_services_meta_box_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['icwcu_custom_services_meta_box_nonce'] ) ), 'icwcu_custom_services_meta_box_nonce' ) ) {
    13062            return;
    13163        }
    132         if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
     64        if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $post_id ) || 'product' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
    13365            return;
    13466        }
    135         if ( isset( $_POST['post_type'] ) && 'product' === $_POST['post_type'] ) {
    136             if ( ! current_user_can( 'edit_post', $post_id ) ) {
    137                 return;
     67        foreach ( array( 'icwcu_custom_services', 'icwcu_custom_servicesradio' ) as $meta ) {
     68            if ( isset( $_POST[ $meta ] ) ) {
     69                update_post_meta( $post_id, $meta, icwcu_sanitize_services( wp_unslash( $_POST[ $meta ] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- icwcu_sanitize_services validates each row and sanitizes all allowed fields.
    13870            }
    139         } else {
     71        }
     72        foreach ( array( 'icwcu_custom_services_title', 'icwcu_custom_radioservices_title' ) as $meta ) {
     73            if ( isset( $_POST[ $meta ] ) && is_string( $_POST[ $meta ] ) ) {
     74                update_post_meta( $post_id, $meta, sanitize_text_field( wp_unslash( $_POST[ $meta ] ) ) );
     75            }
     76        }
     77    }
     78    public function add_custom_fields() {
     79        global $product;
     80        if ( ! $product instanceof WC_Product ) {
    14081            return;
    14182        }
    142    
    143         if ( isset( $_POST['icwcu_custom_services'] ) ) {
    144             $custom_services = wp_unslash( $_POST['icwcu_custom_services'] );
    145             if ( is_array( $custom_services ) ) {
    146                 array_walk_recursive( $custom_services, 'icwcu_sanitize_service_item' );
    147                 // Filter out empty services
    148                 $custom_services = array_filter( $custom_services, function( $service ) {
    149                     return ! empty( $service['name'] );
    150                 } );
    151             } else {
    152                 $custom_services = array();
     83        foreach ( array( 'checkbox', 'radio' ) as $type ) {
     84            $services = icwcu_get_product_services( $product->get_id(), $type );
     85            if ( ! $services ) {
     86                continue;
    15387            }
    154             update_post_meta( $post_id, 'icwcu_custom_services', $custom_services );
    155         }
    156        
    157         if ( isset( $_POST['icwcu_custom_servicesradio'] ) ) {
    158             $custom_servicesradio = wp_unslash( $_POST['icwcu_custom_servicesradio'] );
    159             if ( is_array( $custom_servicesradio ) ) {
    160                 array_walk_recursive( $custom_servicesradio, 'icwcu_sanitize_service_item' );
    161                 $custom_servicesradio = array_filter( $custom_servicesradio, function( $service ) {
    162                     return ! empty( $service['name'] );
    163                 } );
    164             } else {
    165                 $custom_servicesradio = array();
     88            $nonce = 'icwcu_custom_' . $type . '_nonce';
     89            wp_nonce_field( $nonce . '_action', $nonce, false );
     90            $title_key = 'radio' === $type ? 'icwcu_custom_radioservices_title' : 'icwcu_custom_services_title';
     91            $title = get_post_meta( $product->get_id(), $title_key, true );
     92            echo '<fieldset class="icwcu-custom-services"><legend>' . esc_html( $title ? $title : __( 'Additional Services', 'custom-checkbox-ultimate-for-woocommerce' ) ) . '</legend>';
     93            if ( 'radio' === $type ) {
     94                echo '<label class="icwcu-service"><input type="radio" name="icwcu_radio_service" value="" checked> ' . esc_html__( 'No additional radio service', 'custom-checkbox-ultimate-for-woocommerce' ) . '</label>';
    16695            }
    167             update_post_meta( $post_id, 'icwcu_custom_servicesradio', $custom_servicesradio );
    168         }
    169        
    170    
    171         if ( isset( $_POST['icwcu_custom_services_title'] ) ) {
    172             update_post_meta(
    173                 $post_id,
    174                 'icwcu_custom_services_title',
    175                 sanitize_text_field( wp_unslash( $_POST['icwcu_custom_services_title'] ) )
    176             );
    177         }
    178         if ( isset( $_POST['icwcu_custom_radioservices_title'] ) ) {
    179             update_post_meta(
    180                 $post_id,
    181                 'icwcu_custom_radioservices_title',
    182                 sanitize_text_field( wp_unslash( $_POST['icwcu_custom_radioservices_title'] ) )
    183             );
    184         }
    185     }
    186    
    187     public function add_custom_fields() {
    188         global $post;
    189         $custom_services = get_post_meta( $post->ID, 'icwcu_custom_services', true );
    190         $custom_servicesradio = get_post_meta( $post->ID, 'icwcu_custom_servicesradio', true );
    191        
    192         wp_nonce_field( 'icwcu_custom_checkbox_nonce_action', 'icwcu_custom_checkbox_nonce' );
    193         wp_nonce_field( 'icwcu_custom_radio_nonce_action', 'icwcu_custom_radio_nonce' );
    194         if ( ! is_array( $custom_services ) ) {
    195             $custom_services = array();
    196         }
    197         if ( ! is_array( $custom_servicesradio ) ) {
    198             $custom_servicesradio = array();
    199         }
    200    
    201         if ( ! empty( $custom_services ) ) {
    202             echo '<div class="icwcu-custom-services">';
    203             $custom_services_title = get_post_meta( $post->ID, 'icwcu_custom_services_title', true );
    204             if ( ! empty( $custom_services_title ) ) {
    205                 echo '<h4 class="icwcu-checkbox-title">' . esc_html( $custom_services_title ) . '</h4><table>';
    206             } else {
    207                 echo '<h4 class="icwcu-checkbox-title">' . esc_html__( 'Additional Services', 'custom-checkbox-ultimate-for-woocommerce' ) . '</h4><table>';
     96            foreach ( $services as $key => $service ) {
     97                $id = 'icwcu-' . $type . '-' . $key;
     98                $name = 'radio' === $type ? 'icwcu_radio_service' : 'icwcu_services[]';
     99                echo '<div class="icwcu-service"><label for="' . esc_attr( $id ) . '"><input id="' . esc_attr( $id ) . '" type="' . esc_attr( $type ) . '" name="' . esc_attr( $name ) . '" value="' . esc_attr( $key ) . '" aria-describedby="' . esc_attr( $id . '-description' ) . '"> ' . esc_html( $service['name'] ) . '</label> <span class="icwcu-currency">' . wp_kses_post( wc_price( wc_get_price_to_display( $product, array( 'price' => $service['price'] ) ) ) ) . '</span><small id="' . esc_attr( $id . '-description' ) . '" class="icwcu-service-description">' . esc_html( $service['desc'] ) . '</small></div>';
    208100            }
    209             foreach ( $custom_services as $service ) {
    210                 if ( ! empty( $service['name'] ) && ! empty( $service['price'] ) ) {
    211                     echo '<tr><td><label><input type="checkbox" class="icwcu-checkbox" name="' . esc_html( $service['name'] ) . '" value="' . esc_attr( $service['price'] ) . '">' . esc_html( $service['name'] );
    212    
    213                     if ( ! empty( $service['desc'] ) ) {
    214             //            echo '<span class="icwcu-tooltip">'.wp_kses(icwcu_get_frontend_svg_icon()).'<span class="icwcu-tooltiptext">' . esc_html( $service['desc'] ) . '</span></span></td>';
    215             echo '<span class="icwcu-tooltip">' .
    216     wp_kses( icwcu_get_frontend_svg_icon(), array(
    217         'svg' => array(
    218             'xmlns' => array(),
    219             'width' => array(),
    220             'height' => array(),
    221             'fill' => array(),
    222             'class' => array(),
    223             'viewBox' => array(),
    224         ),
    225         'path' => array(
    226             'd' => array(),
    227         ),
    228     ) ) .
    229     '<span class="icwcu-tooltiptext">' . esc_html( $service['desc'] ) . '</span></span>';
    230 
    231                     }
    232    
    233                     echo '<td></td><td><span class="icwcu-currency">' . esc_html( get_woocommerce_currency_symbol() ) . esc_attr( $service['price'] ) . '</span></td>';
    234                     echo '</label></tr>';
    235                 }
    236             }
    237             echo '</table></div>';
    238         }
    239        
    240         if ( ! empty( $custom_servicesradio ) ) {
    241             echo '<div class="icwcu-custom-services">';
    242             $custom_services_title = get_post_meta( $post->ID, 'icwcu_custom_radioservices_title', true );
    243             if ( ! empty( $custom_services_title ) ) {
    244                 echo '<h4 class="icwcu-checkbox-title">' . esc_html( $custom_services_title ) . '</h4><table>';
    245             } else {
    246                 echo '<h4 class="icwcu-checkbox-title">' . esc_html__( 'Additional Services', 'custom-checkbox-ultimate-for-woocommerce' ) . '</h4><table>';
    247             }
    248             foreach ( $custom_servicesradio as $serviceradio ) {
    249                 if ( ! empty( $serviceradio['name'] ) && ! empty( $serviceradio['price'] ) ) {
    250                     echo '<tr><td><label><input type="radio" class="icwcu-radio" data-group="icwcupayment"  name="' . esc_html( sanitize_title( $serviceradio['name'] ) ) . '" value="' . esc_attr( $serviceradio['price'] ) . '">' . esc_html( $serviceradio['name'] );
    251    
    252                     if ( ! empty( $serviceradio['desc'] ) ) {
    253                       //  echo '<span class="icwcu-tooltip">'.wp_kses(icwcu_get_frontend_svg_icon()).'<span class="icwcu-tooltiptext">' . esc_html( $serviceradio['desc'] ) . '</span></span></td>';
    254                       echo '<span class="icwcu-tooltip">' .
    255                             wp_kses( icwcu_get_frontend_svg_icon(), array(
    256                                 'svg' => array(
    257                                     'xmlns' => array(),
    258                                     'width' => array(),
    259                                     'height' => array(),
    260                                     'fill' => array(),
    261                                     'class' => array(),
    262                                     'viewBox' => array(),
    263                                 ),
    264                                 'path' => array(
    265                                     'd' => array(),
    266                                 ),
    267                             ) ) .
    268                             '<span class="icwcu-tooltiptext">' . esc_html( $serviceradio['desc'] ) . '</span></span>';
    269 
    270                     }
    271    
    272                     echo '<td></td><td><span class="icwcu-currency">' . esc_html( get_woocommerce_currency_symbol() ) . esc_attr( $serviceradio['price'] ) . '</span></td>';
    273                     echo '</label></tr>';
    274                 }
    275             }
    276             echo '</table></div>';
     101            echo '</fieldset>';
    277102        }
    278103    }
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/includes/class-icwcu-order.php

    r3264079 r3759386  
    66class ICWCU_Order {
    77    public function __construct() {
    8         add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'add_custom_fields_to_order_items' ), 10, 4 );
    9         add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'add_customradio_fields_to_order_items' ), 10, 4 );
     8        // Runs for both classic checkout and the Checkout block (Store API).
     9        add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'add_services_to_order_item' ), 10, 4 );
    1010    }
    11    
    12     public function add_custom_fields_to_order_items( $item, $cart_item_key, $values, $order ) {
    13         $custom_services = get_post_meta( $values['product_id'], 'icwcu_custom_services', true );
    14         if ( ! is_array( $custom_services ) ) {
    15             $custom_services = array();
    16         }
    17    
    18         if ( ! empty( $custom_services ) ) {
    19             foreach ( $custom_services as $service ) {
    20                 $service_key = sanitize_title( $service['name'] );
    21                 if ( isset( $values[ $service_key ] ) ) {
    22                     $item->add_meta_data( $service['name'], get_woocommerce_currency_symbol() . $values[ $service_key ] );
    23                 }
    24             }
    25         }
    26     }
    27    
    28     public function add_customradio_fields_to_order_items( $item, $cart_item_key, $values, $order ) {
    29         $custom_servicesradio = get_post_meta( $values['product_id'], 'icwcu_custom_servicesradio', true );
    30         if ( ! is_array( $custom_servicesradio ) ) {
    31             $custom_servicesradio = array();
    32         }
    33    
    34         if ( ! empty( $custom_servicesradio ) ) {
    35             foreach ( $custom_servicesradio as $serviceradio ) {
    36                 $service_keyradio = sanitize_title( $serviceradio['name'] );
    37                 if ( isset( $values[ $service_keyradio ] ) ) {
    38                     $item->add_meta_data( $serviceradio['name'], get_woocommerce_currency_symbol() . $values[ $service_keyradio ] );
    39                 }
    40             }
     11
     12    public function add_services_to_order_item( $item, $cart_item_key, $values, $order ) {
     13        foreach ( ICWCU_Cart::get_cart_item_services( $values ) as $service ) {
     14            $item->add_meta_data( $service['name'], icwcu_format_price( $service['price'] ) );
    4115        }
    4216    }
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/includes/class-icwcu-settings.php

    r3264079 r3759386  
    4747        global $post;
    4848   
    49         if ( is_product() ) {
     49        if ( function_exists( 'is_product' ) && is_product() && $post instanceof WP_Post ) {
    5050            $custom_servicesradio = get_post_meta( $post->ID, 'icwcu_custom_servicesradio', true );
    5151   
    5252            if ( ! empty( $custom_servicesradio ) ) {
    53                 wp_enqueue_script( 'icwcu-radio-script', plugin_dir_url( __FILE__ ) . '../assets/js/custom-checkbox-frontend.js', array( 'jquery' ), ICWCU_PLUGIN_VERSION, true );
     53                wp_enqueue_script( 'icwcu-radio-script', plugin_dir_url( __FILE__ ) . '../assets/js/custom-checkbox-frontend.js', array(), ICWCU_PLUGIN_VERSION, array( 'in_footer' => true, 'strategy' => 'defer' ) );
    5454            }
    5555        }
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/includes/enqueue.php

    r3264079 r3759386  
    11<?php
    2 /*
    3 Plugin Name: Custom Checkbox Ultimate for WooCommerce
    4 *
    5 */
     2defined( 'ABSPATH' ) || exit;
    63
    7 if ( ! defined( 'ABSPATH' ) ) {
    8     exit; // Exit if accessed directly
    9 }
    10 /*
    11 Enqueue Styles
    12 */
    13 // Enqueue custom SVG icon
    14 add_action('admin_enqueue_scripts', 'icwcu_enqueue_custom_svg_icon');
    15 function icwcu_enqueue_custom_svg_icon() {
    16     wp_enqueue_style('icwcu-custom-svg-icon', plugin_dir_url(__FILE__) . '../assets/css/icwcu-admin-styling.css', array(), ICWCU_PLUGIN_VERSION );
    17        
     4add_action( 'admin_enqueue_scripts', 'icwcu_enqueue_admin_styles' );
     5function icwcu_enqueue_admin_styles( $hook ) {
     6    $screen = get_current_screen();
     7    if ( 'toplevel_page_ic-woo-ultimate' === $hook || ( $screen && 'product' === $screen->post_type ) ) {
     8        wp_enqueue_style( 'icwcu-admin-style', plugins_url( 'assets/css/icwcu-admin-styling.css', ICWCU_PLUGIN_FILE ), array(), ICWCU_PLUGIN_VERSION );
     9    }
    1810}
    1911
    20 // Enqueue custom CSS for frontend product page
    21 add_action('wp_enqueue_scripts', 'icwcu_enqueue_custom_css');
     12add_action( 'wp_enqueue_scripts', 'icwcu_enqueue_custom_css' );
    2213function icwcu_enqueue_custom_css() {
    23     // Check if WooCommerce is active
    24     if (class_exists('WooCommerce') && function_exists('is_product') && is_product()) {
    25         wp_enqueue_style('icwcu-custom-services-style', plugin_dir_url(__FILE__) . '../assets/css/icwcu-services-style.css', array(),ICWCU_PLUGIN_VERSION );
     14    if ( function_exists( 'is_product' ) && is_product() ) {
     15        wp_enqueue_style( 'icwcu-custom-services-style', plugins_url( 'assets/css/icwcu-services-style.css', ICWCU_PLUGIN_FILE ), array(), ICWCU_PLUGIN_VERSION );
    2616    }
    2717}
    28 function icwcu_enqueue_admin_styles($hook) {
    29     // Check if we are on the plugin's dashboard page
    30     if ( 'toplevel_page_ic-woo-ultimate' != $hook ) {
    31         return;
    32     }
    33    
    34     // Enqueue the admin CSS file
    35     wp_enqueue_style(
    36         'icwcu-admin-style',
    37         plugin_dir_url(__FILE__) . '../assets/css/icwcu-admin-styling.css', // Path to your CSS file
    38         [],
    39         ICWCU_PLUGIN_VERSION // Version
    40     );
    41 }
    42 add_action( 'admin_enqueue_scripts', 'icwcu_enqueue_admin_styles' );
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/includes/helpers.php

    r3264079 r3759386  
    11<?php
    2 /*
    3 * Plugin Name: Custom Checkbox Ultimate for WooCommerce
    4 *
    5 */
    62if ( ! defined( 'ABSPATH' ) ) {
    73    exit;
    … …  
    4945                }
    5046        }
     47
     48if ( ! function_exists( 'icwcu_get_product_services' ) ) {
     49        /**
     50         * Get the saved services of a product.
     51         *
     52         * @param int    $product_id Product ID.
     53         * @param string $type       'checkbox' or 'radio'.
     54         * @return array Services keyed by slug: array( 'name' => string, 'price' => float, 'desc' => string ).
     55         */
     56        function icwcu_get_product_services( $product_id, $type = 'checkbox' ) {
     57                $meta_key = ( 'radio' === $type ) ? 'icwcu_custom_servicesradio' : 'icwcu_custom_services';
     58                $services = get_post_meta( $product_id, $meta_key, true );
     59                $result   = array();
     60
     61                if ( ! is_array( $services ) ) {
     62                        return $result;
     63                }
     64
     65                foreach ( icwcu_sanitize_services( $services ) as $index => $service ) {
     66                        if ( ! is_array( $service ) || empty( $service['name'] ) ) {
     67                                continue;
     68                        }
     69                        $key = sanitize_title( $service['name'] );
     70                        if ( '' === $key ) {
     71                                continue;
     72                        }
     73                        if ( isset( $result[ $key ] ) ) {
     74                                $key .= '-' . sanitize_key( (string) $index );
     75                        }
     76                        $result[ $key ] = array(
     77                                'name'  => (string) $service['name'],
     78                                'price' => isset( $service['price'] ) ? max( 0, (float) $service['price'] ) : 0,
     79                                'desc'  => isset( $service['desc'] ) ? (string) $service['desc'] : '',
     80                        );
     81                }
     82
     83                return $result;
     84        }
     85}
     86
     87/** Normalize submitted rows, removing gaps and rejecting malformed fields. */
     88function icwcu_sanitize_services( $rows ) {
     89    $result = array();
     90    if ( ! is_array( $rows ) ) {
     91        return $result;
     92    }
     93    foreach ( $rows as $row ) {
     94        if ( ! is_array( $row ) || ! isset( $row['name'] ) || ! is_string( $row['name'] ) ) {
     95            continue;
     96        }
     97        $name = sanitize_text_field( $row['name'] );
     98        if ( '' === trim( $name ) ) {
     99            continue;
     100        }
     101        $price = isset( $row['price'] ) && is_scalar( $row['price'] ) ? (float) wc_format_decimal( $row['price'] ) : 0;
     102        $result[ count( $result ) + 1 ] = array(
     103            'name' => $name,
     104            'price' => is_finite( $price ) ? max( 0, $price ) : 0,
     105            'desc' => isset( $row['desc'] ) && is_string( $row['desc'] ) ? sanitize_textarea_field( $row['desc'] ) : '',
     106        );
     107    }
     108    return $result;
     109}
     110
     111if ( ! function_exists( 'icwcu_format_price' ) ) {
     112        /**
     113         * Format a price as plain text (safe for cart item data and order item meta).
     114         *
     115         * @param float $price Price.
     116         * @return string
     117         */
     118        function icwcu_format_price( $price ) {
     119                return html_entity_decode( wp_strip_all_tags( wc_price( $price ) ), ENT_QUOTES, 'UTF-8' );
     120        }
     121}
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/includes/plugin-info.php

    r3264079 r3759386  
    11<?php
    2 if ( ! defined( 'ABSPATH' ) ) {
    3     exit;
    4 }
     2defined( 'ABSPATH' ) || exit;
    53?>
    6 <div class="wrap">
    7     <h1><?php esc_html_e( 'Custom Checkbox Ultimate for WooCommerce Documentation', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h1>
    8    
    9     <h2><?php esc_html_e( 'Introduction', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
    10     <p><?php esc_html_e( 'Custom Checkbox Ultimate for WooCommerce is a plugin that allows you to add customizable checkbox options on WooCommerce product pages for additional services and charges.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
    11    
    12     <h2><?php esc_html_e( 'Installation', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
     4<div class="wrap icwcu-guide">
     5    <h1><?php esc_html_e( 'Custom Checkbox Ultimate: setup guide', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h1>
     6    <p><?php esc_html_e( 'Offer optional services such as gift wrapping, assembly, or premium packaging. Configure each product in the classic WooCommerce product editor.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     7    <h2><?php esc_html_e( '1. Configure a product', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
    138    <ol>
    14         <li><?php esc_html_e( 'Navigate to Plugins > Add New in your WordPress dashboard.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    15         <li><?php esc_html_e( 'Click on "Upload Plugin" and select the plugin ZIP file.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    16         <li><?php esc_html_e( 'Click "Install Now" and then "Activate" to enable the plugin.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
     9        <li><?php esc_html_e( 'Install and activate WooCommerce, then open Products > All Products and edit a simple or variable product.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
     10        <li><?php esc_html_e( 'Find Custom Services - IC Woo Ultimate below the editor. If it is hidden, enable it in Screen Options.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
     11        <li><?php esc_html_e( 'In Checkbox Type, enter a group title and services customers can combine. In Radio Type, enter alternatives where customers choose at most one.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
     12        <li><?php esc_html_e( 'For each row, enter a service name, a non-negative price in the store currency, and an optional description. Zero-priced services are supported. Blank names are ignored.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
     13        <li><?php esc_html_e( 'Use Add More Fields for additional rows. To remove a service, clear its name. Update or publish the product.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    1714    </ol>
    18    
    19     <h2><?php esc_html_e( 'Setup', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
    20     <ol>
    21         <li><?php esc_html_e( 'Go to WooCommerce > Settings > Custom Checkbox Ultimate.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    22         <li><?php esc_html_e( 'Configure the default settings for your checkboxes, such as label text, default state, and additional costs.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    23         <li><?php esc_html_e( 'Save your settings.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    24     </ol>
    25    
    26     <h2><?php esc_html_e( 'Usage', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
    27     <ol>
    28         <li><?php esc_html_e( 'Edit the product where you want to add a custom checkbox.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    29         <li><?php esc_html_e( 'In the product data section, navigate to the "Custom Checkbox" tab.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    30         <li><?php esc_html_e( 'Enable the checkbox option and customize the label, description, and additional cost as needed.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    31         <li><?php esc_html_e( 'Update or publish the product to apply the changes.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    32     </ol>
    33    
    34     <h2><?php esc_html_e( 'Support', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
    35     <p><?php esc_html_e( 'For further assistance, please visit our support page or contact us at support@insertcart.com.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     15    <h2><?php esc_html_e( '2. Test the customer journey', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
     16    <p><?php esc_html_e( 'Open the product page, select services, choose a variation if applicable, and add the product to the cart. Radio services include a no-service option. Selections are optional and are not preselected.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     17    <p><?php esc_html_e( 'The product headline price does not change before adding to cart. In the cart, selected service costs are added to each product unit. For a product priced at 20 with a service priced at 5, quantity 2 has a subtotal of 50 before tax, discounts, and shipping. Taxes and coupons follow the product settings in WooCommerce.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     18    <h2><?php esc_html_e( '3. Review orders', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
     19    <p><?php esc_html_e( 'Selected service names and unit costs appear beneath the product in the cart and checkout. After checkout, find them on the order line item in WooCommerce > Orders. They also appear in standard WooCommerce order details and emails. Refunds use the product line amount; services are not separate fee lines.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     20    <h2><?php esc_html_e( 'Compatibility and limitations', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
     21    <p><?php esc_html_e( 'Variable products share services configured on the parent product. Existing selections work with classic and block cart/checkout and with HPOS. Service selection requires the standard WooCommerce product add-to-cart form; the block Add to Cart form and direct Store API selection are not provided. Shop-loop quick add does not collect services. Grouped, external, subscription, bundle, and custom product forms need separate testing.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     22    <h2><?php esc_html_e( 'Troubleshooting and privacy', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
     23    <p><?php esc_html_e( 'If options are missing, confirm that service names are saved and the theme renders the standard add-to-cart hook. If selections are missing from the cart, refresh the product page and exclude cart and checkout from full-page caching. Test theme and add-on conflicts on a staging site. After changing prices, remove the old cart line and add the product again.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     24    <p><?php esc_html_e( 'The plugin sends no analytics or service data to an external server. Configuration is stored with products; selections are stored in WooCommerce carts and order line metadata. Deactivation preserves configuration and orders.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     25    <p><a class="button" href="<?php echo esc_url( admin_url( 'edit.php?post_type=product' ) ); ?>"><?php esc_html_e( 'Open products', 'custom-checkbox-ultimate-for-woocommerce' ); ?></a> <a class="button" href="<?php echo esc_url( ICWCU_DOCUMENTATION_URL ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Online documentation', 'custom-checkbox-ultimate-for-woocommerce' ); ?></a> <a href="https://wordpress.org/support/plugin/custom-checkbox-ultimate-for-woocommerce/"><?php esc_html_e( 'Support forum', 'custom-checkbox-ultimate-for-woocommerce' ); ?></a></p>
    3626</div>
    37 
  • custom-checkbox-ultimate-for-woocommerce/tags/1.4/readme.txt

    r3264079 r3759386  
    11=== Custom Checkbox Ultimate for WooCommerce ===
    22Contributors: sandy786
    3 Tags: WooCommerce, checkbox, additional services, customizable options, e-commerce
    4 Plugin URI: https://www.insertcart.com/insertcart-custom-checkbox-ultimate-for-woocommerce-documentation/
    5 Author URI: https://www.insertcart.com/
     3Tags: woocommerce, checkbox, product add-ons, extra services, radio buttons
    64Requires at least: 6.3
    7 Tested up to: 6.7
     5Tested up to: 7.1
    86Requires PHP: 7.2
    9 Stable tag: 1.3
    10 License: GPL v2 or later
     7Requires Plugins: woocommerce
     8Stable tag: 1.4
     9License: GPLv2 or later
    1110License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1211
    13 Add customizable checkbox options on WooCommerce product pages for additional services and charges.
     12Add optional checkbox and radio services to WooCommerce products, with per-unit prices and service details in the cart and orders.
    1413
    1514== Description ==
    16 Custom Checkbox Ultimate for Woocommerce is a powerful WooCommerce extension that allows store owners to offer additional services and charges to customers through customizable checkboxes on product pages. Enhance your product offerings by enabling customers to select extra features, services, or options, seamlessly integrating additional costs into their purchases.
    1715
    18 == Key Features ==
     16Offer gift wrapping, assembly, premium packaging, and other optional services on WooCommerce product pages. Configure services for each product in the classic product editor.
    1917
    20 **Customizable Checkbox Options:** Easily add multiple checkbox fields to your WooCommerce product pages for additional services.
    21 **Dynamic Pricing:** Automatically adjust product prices based on selected additional services.
    22 **Service Descriptions:** Provide detailed descriptions for each additional service using tooltips for better customer understanding.
    23 **Admin-Friendly Interface:** Manage and configure additional services directly from the product edit screen with an intuitive meta box.
    24 **Order Integration:** Ensure that selected services and their costs are accurately reflected in the cart, checkout, and order details.
    25 **Flexible Field Management:** Add as many custom service fields as needed with ease.
     18* Checkbox services: customers can select several options.
     19* Radio services: customers choose at most one option or no additional service.
     20* Set a name, non-negative price, and description for each service. Free services are supported.
     21* Start with five rows per type and add more when needed.
     22* Service prices come from saved product settings, never customer-submitted prices.
     23* Selected services increase the product unit price and appear in the cart, checkout, order details, and standard emails.
     24* Works with simple products and parent-level services on variable products.
     25* Supports HPOS and classic or block cart/checkout for items added through the standard product form.
    2626
    27 Check out our [Documentation](https://www.insertcart.com/insertcart-custom-checkbox-ultimate-for-woocommerce-documentation/), [Support Ticket](https://www.insertcart.com/support-ticket/), and [Contact us](https://www.insertcart.com/contact-us/).
     27Descriptions are displayed beside the service options, including for keyboard and touch users. The product headline price does not update as options are selected; the adjusted price appears in the cart.
    2828
    29 ==Installation==
     29= Documentation and first setup =
    3030
    31 Upload the Plugin:
     31Open **IC Woo Ultimate** in the WordPress dashboard for the included setup guide. You can also use **Plugins > Setup guide** or the guide link inside the Custom Services product meta box. The included guide works without an external website.
    3232
    33 *   Download the plugin zip file.
    34 *   In your WordPress dashboard, navigate to Plugins > Add New.
    35 *   Click on the Upload Plugin button at the top.
    36 *   Choose the downloaded zip file and click Install Now.
    37 *   Activate the Plugin:
    38 *   After installation, click on Activate Plugin.
    39 *   Ensure WooCommerce is Active:
     33[Online documentation](https://www.insertcart.com/insertcart-custom-checkbox-ultimate-for-woocommerce-documentation/) | [Support forum](https://wordpress.org/support/plugin/custom-checkbox-ultimate-for-woocommerce/) | [Contact InsertCart](https://www.insertcart.com/contact-us/)
    4034
    41 This plugin requires WooCommerce to be installed and activated.
    42 If WooCommerce is not active, you will receive an admin notice prompting you to activate it.
     35= How pricing works =
    4336
    44 ==Quick Start==
     37Services are charged per product unit, in the store currency. A product priced at 20 with a service priced at 5 has a unit price of 25. Quantity 2 has a subtotal of 50 before taxes, discounts, and shipping. WooCommerce applies product taxes and coupons to the combined product price. Services are not separate cart fees.
    4538
    46 * Adding Custom Services to a Product
     39= Compatibility limits =
    4740
    48 Navigate to Products:
     41Service selection uses the standard WooCommerce product add-to-cart form. This release does not provide selection controls for the block Add to Cart form, direct Store API additions, shop-loop quick add, or third-party custom product forms. Variable products share the parent product's services. Test grouped, external, bundled, and subscription products separately.
    4942
    50 *   In your WordPress dashboard, go to Products > All Products.
    51 *   Click on the product you want to add additional services to, or create a new product.
    52 *   Locate the Custom Services Meta Box:
    53 *   Scroll down to find the Custom Services – IC Woo Ultimate meta box.
     43= Privacy =
    5444
    55 ==Configure the Services==
     45No analytics or service data is sent to an external server. Product settings are stored in WordPress product metadata. Selections are stored in WooCommerce carts and order item metadata. Deactivation preserves settings and historical orders.
    5646
    57 **Custom Services Title:** Enter a title that will appear above the checkbox options on the product page (e.g., "Add-Ons", "Extra Services").
    58 **Service Fields:** By default, five service fields are available. For each service, provide:
    59 **Service Name:** The label for the checkbox (e.g., "Gift Wrapping").
    60 **Service Price:** The additional cost associated with the service.
    61 **Service Description: (Optional)** A brief description that appears as a tooltip for more information.
    62 **Add More Fields:** Click the **+Add More Fields** button to add additional service options beyond the default five.
    63 **Save the Product:** Once configured, click Update or Publish to save your changes.
     47== Installation ==
    6448
    65 Displaying Custom Services on the Frontend
    66 On the product page, customers will see the Custom Services Title followed by a list of checkbox options with their respective prices.
    67 Selecting a checkbox will automatically update the product price in the cart to include the additional service cost.
    68 Service descriptions are available as tooltips for more detailed information.
    69 Managing Orders
    70 Selected additional services and their costs are displayed in the cart, checkout, and order details, ensuring transparency and accurate billing.
     491. Install and activate WooCommerce.
     502. Install Custom Checkbox Ultimate for WooCommerce from Plugins > Add New, or upload the plugin ZIP.
     513. Activate the plugin and open IC Woo Ultimate for the included guide.
     524. Edit a product and locate Custom Services - IC Woo Ultimate. If hidden, enable it in Screen Options.
     535. Configure Checkbox Type and/or Radio Type, then update the product.
     546. Test the product page, cart, checkout, and a sample order before using it on your live store.
    7155
     56== Frequently Asked Questions ==
    7257
     58= Where are the service settings? =
     59Edit a product in the classic product editor and use Custom Services - IC Woo Ultimate. There is no global WooCommerce settings tab for configuring service rows.
     60
     61= Can I add more than five services? =
     62Yes. Use Add More Fields in either type. Blank service names are ignored. Clear the name and update the product to remove a service.
     63
     64= Are services required or preselected? =
     65No. Checkboxes start unchecked. Radio services include a no-service choice.
     66
     67= Can services be free? =
     68Yes. Enter 0 as the service price. Negative prices are not supported.
     69
     70= Do services work with variations? =
     71Yes. Configure services on the parent product; they are added to the chosen variation's unit price. Per-variation service definitions are not provided.
     72
     73= Why is the product page price unchanged? =
     74This release shows each service price beside its option. The product's combined price is calculated after adding it to the cart.
     75
     76= Why are selections missing? =
     77Check that your theme renders the standard add-to-cart form and refresh the product page to obtain a fresh form nonce. Exclude cart and checkout from page caching. Shop-loop quick add and block product forms do not collect selections.
     78
     79= What happens when a service price changes? =
     80New additions use the saved product price. Existing 1.4 cart selections retain their recorded service amount; remove and re-add the product to use new settings. Legacy 1.3 cart entries are resolved against current settings. Historical orders retain their stored line item details.
    7381
    7482== Screenshots ==
    7583
    76 1. Checkbox you can add fields and services name with price and description.
    77 2. Radio button you can select only one of theme.
    78 3. Pricing will display on WooCommerce Product Page.
    79 4. Check screen option in product page to hide and show plugin settings
    80 
    81 
    82 ===Frequently Asked Questions===
    83 
    84 = Does this plugin work with all WooCommerce themes? =
    85 Yes, the plugin is designed to be compatible with most WooCommerce-compatible themes. However, styling may vary based on your theme. You can use custom CSS to adjust the appearance as needed.
    86 
    87 = Can I add more than five custom service fields? =
    88 Absolutely! While the plugin initializes with five fields by default, you can add as many additional service fields as required using the + Add More Fields button in the meta box.
    89 
    90 = Is it possible to set different prices for different user roles? =
    91 By default, the plugin does not support role-based pricing. However, with custom development or additional extensions, this functionality can be achieved.
    92 
    93 = How are the additional service costs handled during checkout? =
    94 Selected additional services are added to the cart item data, ensuring that the total price reflects the added costs. These details are visible in the cart, checkout, and order summaries.
    95 
    96 = What happens if WooCommerce is deactivated? =
    97 If WooCommerce is not active, the plugin will display an admin notice prompting you to activate WooCommerce, as it is a required dependency.
    98 
    99 = More Features =
    100 Working on more features and settings like Shortcodes, Styling options etc & more. [Contact us](https://www.insertcart.com/contact-us/) for more requests.
     841. Checkbox service fields with names, prices, and descriptions, rendered with a sample product.
     852. Radio service fields with alternative choices and an additional row.
     863. Product service options, descriptions, prices, and the no-service radio choice.
     874. The setup guide included in version 1.4. Screenshots use actual plugin output in a local test harness.
    10188
    10289== Changelog ==
    103 1.3
    104 Initial release of Custom Checkbox Ultimate for WooCommerce.
    105 Features include adding customizable checkbox options, dynamic pricing adjustments, service descriptions with tooltips, and order integration.
    106 Upgrade Notice
    107 1.0
    108 Initial release.
    109 License
    110 This plugin is licensed under the GPL v2 or later. You can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or any later version.
     90
     91= 1.4 =
     92* Security: use saved service prices instead of submitted form prices; validate malformed requests and product editor fields.
     93* Fix checkbox names containing spaces or capital letters and group radio choices correctly.
     94* Keep add-on prices on cart lines and avoid adding charges twice during repeated totals calculations.
     95* Preserve existing services when saved rows contain gaps and support free services.
     96* Improve accessible service descriptions and provide an explicit no-radio-service option.
     97* Add an accurate built-in setup guide, product help link, Plugins list links, and a one-time introduction for store managers.
     98* Guard against missing WooCommerce and limit admin styling to plugin/product screens.
     99* Declare HPOS and Cart/Checkout block compatibility for selections made on standard product forms.
     100* Verified locally with WordPress 7.1.3 and WooCommerce 10.3.5.
     101
     102= 1.3 =
     103* Initial release with checkbox and radio services, service descriptions, and cart/order integration.
    111104
    112105== Upgrade Notice ==
    113106
    114 No major requirements
     107= 1.4 =
     108Security and reliability update. Service prices now come from saved product settings. Includes a setup guide and improved cart/order handling. Test on staging before updating your live store.
  • custom-checkbox-ultimate-for-woocommerce/trunk/assets/css/icwcu-admin-styling.css

    r3264079 r3759386  
    1 div#icwcu_custom_services_meta_box h2 {
    2     position: relative;
    3     display: inline-block;
    4 }
    5 
    6 div#icwcu_custom_services_meta_box h2 svg {
    7     float: left;
    8     height: 20px;
    9 }
    10 .icwcu-custom-service-field svg {
    11     vertical-align: middle;
    12     margin-right: 5px;
    13 }
    14 .icwcu-custom-service-field textarea{
    15         vertical-align:middle;
    16 }
    17 
    18 .menu-icon-ic-woo-ultimate svg {
    19     vertical-align: middle;
    20     margin-right: 10px;
    21 }
    22 .icwcu-custom-service-field label {
    23     width: 144px;
    24     display: inline-block;
    25     padding: 0px 20px;
    26 }
    27 .icwcu-custom-service-field {
    28     border: 1px solid #ddd;
    29     margin: 10px;
    30 }
    31 button#icwcu-add-more-fields:hover {
    32         background: #167fae;
    33 }
    34 button#icwcu-add-more-fields {
    35     margin: 10px;
    36     background: #03A9F4;
    37     color: #fff;
    38     border: 0;
    39 }
    40 button#icwcu-add-moreradio-fields:hover {
    41         background: #167fae;
    42 }
    43 button#icwcu-add-moreradio-fields {
    44     margin: 10px;
    45     background: #03A9F4;
    46     color: #fff;
    47     border: 0;
    48 }
    49 
    50 
    51 .icwcu-admin-notice-wrapper {
    52   padding: 20px; }
    53   .icwcu-admin-notice-wrapper h2 {
    54     margin-top: 0; }
    55   .icwcu-admin-notice-wrapper p {
    56     font-size: 14px; }
    57   .icwcu-admin-notice-wrapper .icwcu-admin-notice-primary-button {
    58     padding: 5px 20px;
    59     background: #67bc45;
    60     color: #fff;
    61     font-weight: normal;
    62     text-decoration: none;
    63     margin-top: 5px;
    64     display: inline-block;
    65     border-radius: 50px; }
    66 
    67 .is-style-outline > .wp-block-button__link:not(.has-text-color),
    68 .wp-block-button__link.is-style-outline:not(.has-text-color) {
    69   color: var(--wp--preset--color--primary); }
    70 
    71 .icwcu--getting-started--wrapper .icwcu-header-left > .fs-notice {
    72   display: none !important; }
    73 
    74 .icwcu-options-features > li span {
    75   color: green;
    76   margin-right: 20px; }
    77 
    78 .icwcu-compare-table table td span {
    79   font-size: 21px;
    80   background: #f0f0f1;
    81   border-radius: 100%;
    82   padding: 5px;
    83   color: #d72b2b; }
    84 
    85 .icwcu-compare-table table td span.dashicons.dashicons-yes {
    86   text-align: center;
    87   color: #4caf50; }
    88 
    89 .icwcu-about-container.icwcu-admin-card.icwcu-admin-card-header {
    90   background: linear-gradient(45deg, #3F51B5, #E91E63);
    91   justify-content: center;
    92   align-items: center; }
    93 
    94 .icwcu-about-side {
    95   margin-top: 30px; }
    96 #icwcu-admin-about-page p, #icwcu-admin-about-page {
    97   font-size: 17px;
    98   line-height: 1.7;
    99   margin: 0 0 15px; }
    100 
    101 .subscribe-panel li span {
    102   margin-top: 6px;
    103   margin-right: 10px;
    104   color: #3fb345; }
    105 
    106 #icwcu-admin-about-page {
    107   margin: 20px;
    108   max-width: 100%; }
    109 
    110 .icwcu-about-container {
    111   padding: 40px 60px;
    112   border-bottom: 1px solid #ddd;
    113   border-radius: 5px;
    114   margin: 30px 0;
    115   background: #fff;
    116   box-shadow: 0 0 50px #dddddd7d; }
    117 
    118 a.icwcu-admin-button.premium-button span.dashicons{
    119   vertical-align: middle;
    120 }
    121 a.icwcu-admin-button.premium-button {
    122   background: #2196F3;
    123   margin-left: 5px; }
    124 
    125 h3.icwcu-admin-card-title {
    126   font-size: 1.4rem !important;
    127   text-transform: capitalize;
    128   color: #fff;
    129   margin-bottom: 0;
    130   padding: 0; }
    131 
    132 a.icwcu-admin-button {
    133   padding: 12px 36px;
    134   text-decoration: inherit;
    135   background: #3e3541;
    136   color: #fff;
    137   border-radius: 50px;
    138   margin-top: 20px;
    139   display: inline-block;
    140   font-size: 16px;
    141   line-height: 1.75;
    142   display: flex;
    143   align-items: center;
    144   gap: 4px;
    145   display: inline-block; }
    146 
    147 @keyframes spin {
    148   100% {
    149     -webkit-transform: rotate(360deg);
    150     transform: rotate(360deg); } }
    151   a.icwcu-admin-button.icwcu-importing-icwcu span {
    152     animation: spin 4s linear infinite; }
    153 
    154 a.icwcu-admin-button.primary.large {
    155   background: #e68836;
    156   margin-right: 9px; }
    157 
    158 .feature-list {
    159   display: grid;
    160   grid-gap: 50px;
    161   grid-template-columns: 3fr 1fr;
    162   grid-template-rows: 1fr;
    163   max-width: 100%; }
    164 
    165 .icwcu-about-container.icwcu-about-content {
    166   margin: 0; }
    167 
    168 .icwcu-options-features > li {
    169   display: -webkit-inline-flex;
    170   display: -ms-inline-flexbox;
    171   display: inline-flex;
    172   -webkit-align-items: center;
    173   -ms-flex-align: center;
    174   align-items: center;
    175   width: 100%;
    176   float: left;
    177   margin-bottom: 15px;
    178   font-weight: 500; }
    179 ul.icwcu-options-features b {
    180     font-weight: 900;
    181     margin-right: 10px;
    182 }
    183 .icwcu-options-features > li img {
    184   margin-right: 5px; }
    185 
    186 .icwcu-options-features {
    187   list-style: none;
    188   padding: 0;
    189   margin: 0 0 30px; }
    190   .icwcu-about-container.icwcu-compare-table,
    191   .icwcu-admin-card.member {
    192     display: none;
    193 }
    194 .icwcu-admin-card-title.is-sub {
    195   margin-left: -30px;
    196   margin-right: -30px;
    197   padding: 0 30px 30px;
    198   border-bottom: 1px solid #ebf2f5;
    199   font-size: 2rem;
    200   text-transform: capitalize;
    201   margin-top: 0;
    202   color: #000; }
    203 
    204 .icwcu-about-side .icwcu-admin-card {
    205   background-color: #fff;
    206   padding: 30px;
    207   margin-bottom: 30px;
    208   background-repeat: no-repeat;
    209   background-position: 100% 100%;
    210   border-radius: 15px;
    211   border: 1px solid #ebf2f5;
    212   background: -webkit-linear-gradient(315deg, #0dcad2, #0060f3);
    213   background: -o-linear-gradient(315deg, #0dcad2, #0060f3);
    214   background: linear-gradient(135deg, #0dcad2, #0060f3);
    215   color: #fff; }
    216 
    217 h4.icwcu-admin-card-title.is-small {
    218   color: #fff;
    219   margin: 0; }
    220 
    221 .icwcu-about-side .icwcu-admin-card a.icwcu-admin-button.large.icwcu-block.is-default {
    222   background: #fff;
    223   color: #000; }
    224 
    225 .icwcu-admin-card.cart-two {
    226   background: #fff;
    227   color: #000;
    228   margin: 0; }
    229 
    230 #icwcu-admin-about-page .icwcu-about-side .icwcu-admin-card h3 {
    231   font-size: 1.5rem;
    232   color: #fff; }
    233 
    234 #icwcu-admin-about-page .icwcu-admin-card.cart-two h3 {
    235   color: #010101; }
    236 
    237 #icwcu-admin-about-page div#setting-error-tgmpa {
    238   display: none; }
    239 
    240 .cart-two h3 {
    241   color: #000; }
    242 
    243 .icwcu-header-left h2 {
    244   font-size: 3rem;
    245   text-transform: capitalize;
    246   margin: 0;
    247   color: #fff; }
    248 
    249 #icwcu-admin-about-page .icwcu-header-left p {
    250   color: #fff; }
    251 
    252 .admin-grid-2 {
    253   display: grid;
    254   grid-gap: 4%;
    255   grid-template-columns: 48% 48%;
    256   grid-template-rows: 1fr;
    257   max-width: 100%; }
    258 
    259 .admin-grid-3 {
    260   display: grid;
    261   grid-gap: 50px;
    262   grid-template-columns: 1fr 1fr 1fr;
    263   grid-template-rows: 1fr;
    264   max-width: 100%; }
    265 
    266 #subcription-email {
    267   width: 500px;
    268   /* justify-content: center; */
    269   display: flex;
    270   padding: 10px;
    271   border: 1px solid #f0f0f1;
    272   border-radius: 50px;
    273   position: relative;
    274   display: grid;
    275   /* grid-gap: 50px; */
    276   grid-template-columns: 2fr 1fr;
    277   grid-template-rows: 1fr;
    278   max-width: 100%;
    279   box-shadow: 0 0 23px #0000001a;
    280   margin: 22px auto; }
    281 
    282 #subcription-email button.wp-block-search__button.has-icon {
    283   background: #2960f3;
    284   color: #fff;
    285   border-radius: 50px;
    286   border: none;
    287   cursor: pointer; }
    288 
    289 #subcription-email input[type="text"] {
    290   border: none;
    291   padding: 10px 25px;
    292   background: #f1f1f1;
    293   border-radius: 50px; }
    294 
    295 #subcription-email input[type="text"] {
    296   border: none;
    297   padding: 10px 15px;
    298   background: #fff;
    299   border-radius: 50px; }
    300 
    301 #icwcu-admin-about-page h2 {
    302   font-size: 2.5rem;
    303   line-height: 1.5;
    304   margin: 0; }
    305 
    306 #icwcu-admin-about-page h3 {
    307   font-size: 2rem;
    308   line-height: 1.5;
    309   margin-bottom: 0; }
    310 
    311 #icwcu-admin-about-page,
    312 #icwcu-admin-about-page p {
    313   color: #838383;
    314   font-size: 15px;
    315   line-height: 1.7; }
    316 
    317 #icwcu-admin-about-page img {
    318   max-width: 100%;
    319   vertical-align: middle; }
    320 
    321 .admin-col > img {
    322   box-shadow: 0 15px 24px #0000001c; }
    323 
    324 .icwcu-about-container.icwcu-admin-card.icwcu-admin-card-header {
    325   text-align: center; }
    326 
    327 .icwcu-card-header-button-group {
    328   text-align: center;
    329   display: flex;
    330   align-items: center;
    331   justify-content: center; }
    332 
    333 a.icwcu-template-kit-admin-button:hover,
    334 #icwcu-admin-about-page button.wp-block-search__button.has-icon:hover,
    335 a.icwcu-template-kit-admin-button.primary.large:hover {
    336   background: #4caf50; }
    337 
    338 a.icwcu-admin-button.premium-button:hover {
    339   background: #2960f3; }
    340 
    341 .admin-col {
    342   position: relative; }
    343 
    344 .icwcu-compare-table-subtitle {
    345   font-size: 16px; }
    346   .icwcu-compare-table-subtitle a {
    347     text-decoration: none; }
    348     .icwcu-compare-table-subtitle a i {
    349       font-size: 15px;
    350       line-height: 1.7; }
    351 
    352 .icwcu-compare-table table {
    353   border: 1px solid #e5e4e4;
    354   border-collapse: collapse;
    355   border-spacing: 0;
    356   margin-top: 2em;
    357   width: 100%; }
    358 
    359 .icwcu-compare-table table th {
    360   background-color: #F0F0F1;
    361   border-bottom: 1px solid #e5e4e4; }
    362 
    363 .icwcu-compare-table table th,
    364 .icwcu-compare-table table td {
    365   padding: 1em;
    366   text-align: center; }
    367 
    368 .icwcu-compare-table table tr:nth-child(2n) td {
    369   background: #fbfbfb; }
    370 
    371 .icwcu--getting-started--video-wrapper {
    372   min-width: 600px; }
    373   .icwcu--getting-started--video-wrapper video {
    374     width: 100%; }
    375 
    376 @media screen and (max-width: 782px) {
    377   #icwcu-admin-about-page .admin-grid-3, #icwcu-admin-about-page .admin-grid-2 {
    378     grid-template-columns: 1fr; }
    379   .icwcu-header-left.admin-col {
    380     text-align: center; }
    381   .icwcu-header-left h2 {
    382     font-size: 2rem; }
    383   .feature-list {
    384     grid-template-columns: 1fr; }
    385   #icwcu-admin-about-page, #setting-error-tgmpa {
    386     margin: 20px; }
    387   .icwcu-options-features > li {
    388     width: 100%; } }
    389 
    390 
    391 .icwcu-tab{
    392   cursor: pointer;
    393   padding:10px 20px;
    394   margin:0px 2px;
    395   background:#000;
    396   display:inline-block;
    397   color:#fff;
    398   border-radius:3px 3px 0px 0px;
    399   box-shadow: 0 0.5rem 0.8rem #00000080;
    400 }
    401 input.icwcu-radio {
    402     display: none;
    403 }
    404 .icwcu-panels{
    405   overflow:hidden;
    406   padding:20px; 
    407 }
    408 .icwcu-panel{
    409   display:none;
    410   animation: fadein .8s;
    411 }
    412 @keyframes fadein {
    413     from {
    414         opacity:0;
    415     }
    416     to {
    417         opacity:1;
    418     }
    419 }
    420 .icwcu-panel-title{
    421   font-size:1.5em;
    422   font-weight:bold
    423 }
    424 .radio{
    425   display:none;
    426 }
    427 #one:checked ~ .icwcu-panels #one-panel,
    428 #two:checked ~ .icwcu-panels #two-panel,
    429 #three:checked ~ .icwcu-panels #three-panel{
    430   display:block
    431 }
    432 #one:checked ~ .icwcu-tabs #one-tab,
    433 #two:checked ~ .icwcu-tabs #two-tab,
    434 #three:checked ~ .icwcu-tabs #three-tab{
    435   background:#fffffff6;
    436   color:#000;
    437   border-top: 3px solid #000;
    438 }
     1#icwcu_custom_services_meta_box .icwcu-tab { display: inline-block; padding: 12px; cursor: pointer; font-weight: 600; }
     2#icwcu_custom_services_meta_box .icwcu-tab-radio { margin-left: 12px; }
     3#icwcu_custom_services_meta_box .icwcu-editor-panel { display: none; padding: 12px; }
     4#icwcu_custom_services_meta_box #icwcu-tab-checkbox:checked ~ #icwcu-panel-checkbox,
     5#icwcu_custom_services_meta_box #icwcu-tab-radio:checked ~ #icwcu-panel-radio { display: block; }
     6#icwcu_custom_services_meta_box .icwcu-custom-service-field { border: 1px solid #dcdcde; padding: 8px 16px; margin-bottom: 12px; }
     7#icwcu_custom_services_meta_box label { display: inline-block; min-width: 155px; }
     8#icwcu_custom_services_meta_box input[type=text],
     9#icwcu_custom_services_meta_box input[type=number],
     10#icwcu_custom_services_meta_box textarea { width: min(100%, 360px); vertical-align: middle; }
     11.icwcu-guide { max-width: 980px; }
     12.icwcu-guide p, .icwcu-guide li { font-size: 14px; line-height: 1.8; }
     13.icwcu-guide h2 { margin-top: 30px; }
     14@media (max-width: 600px) { #icwcu_custom_services_meta_box label { display: block; } }
  • custom-checkbox-ultimate-for-woocommerce/trunk/assets/css/icwcu-services-style.css

    r3264079 r3759386  
    1 h4.icwcu-checkbox-title {
    2     margin-bottom: 10px;
    3     margin-top: 0;
    4 }
    5 .icwcu-custom-services table {
    6     width: 100%;
    7 }
    8 .icwcu-custom-services {
    9     padding-bottom: 20px;
    10 }
    11 .icwcu-tooltip {
    12 position: relative;
    13     display: inline-block;
    14     cursor: pointer;
    15     color: #0073e6;
    16     font-weight: bold;
    17     vertical-align: bottom;
    18     padding: 0 10px;
    19 }
    20 
    21 .icwcu-tooltiptext {
    22         visibility: hidden;
    23     width: auto;
    24     min-width: 50px;
    25     max-height: 500px;
    26     background-color: #333333c7;
    27     color: #fff;
    28     border-radius: 6px;
    29     padding: 5px;
    30     position: absolute;
    31     z-index: 2;
    32     left: 100%;
    33     top: 50%;
    34     transform: translateY(-50%);
    35     opacity: 0;
    36     transition: opacity 0.3s ease;
    37     font-size: 12px;
    38     pointer-events: none;
    39 }
    40 .icwcu-tooltiptext::before {
    41     content: '';
    42     position: absolute;
    43     top: 50%;
    44     left: -9px;
    45     transform: translateY(-50%);
    46     border-width: 5px;
    47     border-style: solid;
    48     border-color: transparent #333333c7 transparent transparent;
    49 }
    50 input.icwcu-radio {
    51     margin-right: 5px;
    52 }
    53 /* Tooltip arrow */
    54 /*
    55 .icwcu-tooltiptext::after {
    56   content: '';
    57   position: absolute;
    58   top: 50%;
    59   right: -10px;
    60   transform: translateY(-50%);
    61   border-width: 5px;
    62   border-style: solid;
    63   border-color: transparent transparent transparent #333333c7;
    64 }*/
    65 
    66 /* Show tooltip when hovering over the trigger or tooltip content */
    67 .icwcu-tooltip:hover .icwcu-tooltiptext,
    68 .icwcu-tooltip .icwcu-tooltiptext:hover {
    69   visibility: visible;
    70   opacity: 1;
    71   pointer-events: auto; /* Allow interaction with tooltip when visible */
    72 }
    73 
    74 /* Hide tooltip when mouse leaves both the trigger and tooltip content */
    75 .icwcu-tooltip:not(:hover) .icwcu-tooltiptext:not(:hover) {
    76   visibility: hidden;
    77   opacity: 0;
    78   pointer-events: none; /* Disable interaction again */
    79 }
    80 
    81 
    82 .icwcu-checkbox {
    83   position: relative;
    84   margin: 0 1rem 0 0;
    85   cursor: pointer;
    86   z-index: 1;
    87 }
    88 
    89 .icwcu-checkbox:before {
    90   transition: all .3s ease-in-out;
    91   content: "";
    92   position: absolute;
    93   left: 0;
    94   z-index: 1;
    95   width: 1rem;
    96   height: 1rem;
    97   border: 2px solid #f2f2f2;
    98 }
    99 
    100 .icwcu-checkbox:checked:before {
    101   transform: rotate(-45deg);
    102   height: .5rem;
    103   border-color: green; /* Replace $green with actual value */
    104   border-top-style: none;
    105   border-right-style: none;
    106 }
    107 
    108 .icwcu-checkbox:after {
    109 content: "";
    110     position: absolute;
    111     left: 0;
    112     width: 1.1rem;
    113     height: 1.1rem;
    114     background: #ffffff;
    115     cursor: pointer;
    116     border: 1px solid #ddd;
    117 }
    118 span.icwcu-currency {
    119     margin-left: 20px;
    120 }
     1.icwcu-custom-services { border: 1px solid #ddd; padding: 16px; margin: 0 0 18px; min-width: 0; }
     2.icwcu-custom-services legend { font-weight: 600; padding: 0 6px; }
     3.icwcu-custom-services .icwcu-service { display: block; margin: 10px 0; overflow-wrap: anywhere; }
     4.icwcu-custom-services input { margin-right: 6px; }
     5.icwcu-custom-services .icwcu-currency { white-space: nowrap; margin-left: 8px; }
     6.icwcu-custom-services .icwcu-service-description { display: block; font-size: .85em; margin: 3px 0 0 24px; }
  • custom-checkbox-ultimate-for-woocommerce/trunk/assets/js/custom-checkbox-frontend.js

    r3264079 r3759386  
    1 jQuery(document).ready(function($) {
    2  document.querySelectorAll('.icwcu-radio').forEach((radio) => {
     1/* Lets customers click a selected radio service again to unselect it (add-ons are optional). */
     2document.addEventListener('DOMContentLoaded', function () {
     3  document.querySelectorAll('input.icwcu-radio[name="icwcu_radio_service"]').forEach(function (radio) {
     4    radio.addEventListener('mousedown', function () {
     5      this.dataset.wasChecked = this.checked ? '1' : '';
     6    });
    37    radio.addEventListener('click', function () {
    4       const group = this.getAttribute('data-group');
    5       document.querySelectorAll(`.icwcu-radio[data-group="${group}"]`).forEach((btn) => {
    6         if (btn !== this) btn.checked = false;
    7       });
     8      if (this.dataset.wasChecked === '1') {
     9        this.checked = false;
     10        this.dataset.wasChecked = '';
     11      }
    812    });
    913  });
    10  });
     14});
  • custom-checkbox-ultimate-for-woocommerce/trunk/assets/js/custom-services-meta-box.js

    r3264079 r3759386  
    1 jQuery(document).ready(function($) {
    2     var fieldIndex = customServicesMetaBox.initialFieldCount;
    3     var fieldIndexradio = customRadioServicesMetaBox.initialRadioFieldCount;
    4 
    5     $('#icwcu-add-more-fields').on('click', function() {
    6         fieldIndex++;
    7         $('#icwcu-custom-services-fields').append(
    8             '<div class="icwcu-custom-service-field" data-index="' + fieldIndex + '">' +
    9             '<p>' +
    10             '<label for="service_name_' + fieldIndex + '">' + fieldIndex + '. Service Name</label>' +
    11             '<input type="text" id="service_name_' + fieldIndex + '" name="icwcu_custom_services[' + fieldIndex + '][name]" value="" />' +
    12             '<label for="service_price_' + fieldIndex + '"> Service Price ' + customServicesMetaBox.currencySymbol + ' </label>' +
    13             '<input type="number" step="0.01" id="service_price_' + fieldIndex + '" name="icwcu_custom_services[' + fieldIndex + '][price]" value="" />' +
    14                          '<label for="service_desc_' + fieldIndex + '"> Service Description: ' + ' </label>' +
    15             '<textarea id="service_desc_' + fieldIndex + '" name="icwcu_custom_services[' + fieldIndex + '][desc]" value="" ></textarea>' +
    16             '</p>' +
    17             '</div>'
    18         );
    19     });
    20         $('#icwcu-add-moreradio-fields').on('click', function() {
    21         fieldIndexradio++;
    22         $('#icwcu-custom-radio-services-fields').append(
    23             '<div class="icwcu-custom-service-field" data-index="' + fieldIndexradio + '">' +
    24             '<p>' +
    25             '<label for="radioservice_name_' + fieldIndexradio + '">' + fieldIndexradio + '. Service Name</label>' +
    26             '<input type="text" id="radioservice_name_' + fieldIndexradio + '" name="icwcu_custom_servicesradio[' + fieldIndexradio + '][name]" value="" />' +
    27             '<label for="radioservice_price_' + fieldIndexradio + '"> Service Price ' + customServicesMetaBox.currencySymbol + ' </label>' +
    28             '<input type="number" step="0.01" id="radioservice_price_' + fieldIndexradio + '" name="icwcu_custom_servicesradio[' + fieldIndexradio + '][price]" value="" />' +
    29                          '<label for="radioservice_desc_' + fieldIndexradio + '"> Service Description: ' + ' </label>' +
    30             '<textarea id="radioservice_desc_' + fieldIndexradio + '" name="icwcu_custom_servicesradio[' + fieldIndexradio + '][desc]" value="" ></textarea>' +
    31             '</p>' +
    32             '</div>'
    33         );
     1document.addEventListener('DOMContentLoaded', function () {
     2    document.querySelectorAll('#icwcu_custom_services_meta_box .icwcu-add-fields').forEach(function (button) {
     3        button.addEventListener('click', function () {
     4            var fields = button.previousElementSibling;
     5            var index = fields.children.length + 1;
     6            var row = document.createElement('div');
     7            row.className = 'icwcu-custom-service-field';
     8            row.dataset.index = index;
     9            ['name', 'price', 'desc'].forEach(function (key) {
     10                var p = document.createElement('p');
     11                var label = document.createElement('label');
     12                var input = document.createElement(key === 'desc' ? 'textarea' : 'input');
     13                input.id = fields.dataset.meta + '-' + index + '-' + key;
     14                input.name = fields.dataset.meta + '[' + index + '][' + key + ']';
     15                if (key !== 'desc') input.type = key === 'price' ? 'number' : 'text';
     16                if (key === 'price') { input.min = '0'; input.step = 'any'; }
     17                label.htmlFor = input.id;
     18                label.textContent = icwcuFields[key];
     19                p.appendChild(label);
     20                p.appendChild(input);
     21                row.appendChild(p);
     22            });
     23            fields.appendChild(row);
     24            row.querySelector('input').focus();
     25        });
    3426    });
    3527});
  • custom-checkbox-ultimate-for-woocommerce/trunk/custom-checkbox-ultimate-for-woocommerce.php

    r3264079 r3759386  
    44 * Plugin URI:        https://www.insertcart.com/insertcart-custom-checkbox-ultimate-for-woocommerce-documentation/
    55 * Description:       Add customizable checkbox options on WooCommerce product pages for additional services and charges.
    6  * Version:           1.3
     6 * Version:           1.4
    77 * Requires at least: 6.3
    88 * Requires PHP:      7.2
    … …  
    1212 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
    1313 * Text Domain:       custom-checkbox-ultimate-for-woocommerce
     14 * Domain Path:       /languages
    1415 * Requires Plugins:  woocommerce
     16 * WC requires at least: 8.0
     17 * WC tested up to:   10.3
    1518 */
    1619
    … …  
    1922}
    2023
    21 define( 'ICWCU_PLUGIN_VERSION', '1.3' );
     24define( 'ICWCU_PLUGIN_VERSION', '1.4' );
     25define( 'ICWCU_PLUGIN_FILE', __FILE__ );
     26define( 'ICWCU_DOCUMENTATION_URL', 'https://www.insertcart.com/insertcart-custom-checkbox-ultimate-for-woocommerce-documentation/' );
    2227
    2328// Include our enqueue file (even if empty)
    24 require_once 'includes/enqueue.php';
     29require_once plugin_dir_path( __FILE__ ) . 'includes/enqueue.php';
    2530
    2631// Initialize our plugin classes
    27 require_once 'includes/class-icwcu-init.php';
    28 $icwcu_init = new ICWCU_Init();
     32require_once plugin_dir_path( __FILE__ ) . 'includes/class-icwcu-init.php';
     33add_action( 'plugins_loaded', 'icwcu_initialize' );
     34function icwcu_initialize() {
     35    if ( class_exists( 'WooCommerce' ) ) {
     36        new ICWCU_Init();
     37    } else {
     38        add_action( 'admin_notices', 'icwcu_dependency_notice' );
     39    }
     40}
     41function icwcu_dependency_notice() {
     42    if ( current_user_can( 'activate_plugins' ) ) {
     43        echo '<div class="notice notice-warning"><p>' . esc_html__( 'Custom Checkbox Ultimate requires WooCommerce to be installed and active.', 'custom-checkbox-ultimate-for-woocommerce' ) . '</p></div>';
     44    }
     45}
     46require_once __DIR__ . '/includes/class-icwcu-help.php';
     47ICWCU_Help::hooks();
    2948
    3049// Create a Plugin menu in the dashboard
    … …  
    3453        esc_html__( 'IC Woo Ultimate', 'custom-checkbox-ultimate-for-woocommerce' ),
    3554        esc_html__( 'IC Woo Ultimate', 'custom-checkbox-ultimate-for-woocommerce' ),
    36         'manage_options',
     55        'manage_woocommerce',
    3756        'ic-woo-ultimate',
    3857        'icwcu_about_page',
    39         'data:image/svg+xml;base64,' . base64_encode( icwcu_get_custom_svg_icon() ),
     58        'dashicons-yes-alt',
    4059        100
    4160    );
    4261}
    4362function icwcu_about_page() {
     63    if ( ! current_user_can( 'manage_woocommerce' ) ) {
     64        wp_die( esc_html__( 'You cannot access this setup guide.', 'custom-checkbox-ultimate-for-woocommerce' ), '', array( 'response' => 403 ) );
     65    }
    4466    include_once plugin_dir_path( __FILE__ ) . 'includes/plugin-info.php';
    4567}
     68
     69// Declare compatibility with WooCommerce High-Performance Order Storage (HPOS) and Cart/Checkout blocks.
     70add_action( 'before_woocommerce_init', 'icwcu_declare_wc_compatibility' );
     71function icwcu_declare_wc_compatibility() {
     72    if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) {
     73        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', ICWCU_PLUGIN_FILE, true );
     74        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', ICWCU_PLUGIN_FILE, true );
     75    }
     76}
  • custom-checkbox-ultimate-for-woocommerce/trunk/includes/class-icwcu-cart.php

    r3264079 r3759386  
    55
    66class ICWCU_Cart {
     7
     8    /**
     9     * Original product prices, so add-ons are never added twice
     10     * when cart totals are calculated more than once per request.
     11     *
     12     * @var array
     13     */
     14    private $base_prices = array();
     15
    716    public function __construct() {
    8         add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_custom_fields_to_cart_item' ), 10, 2 );
    9         add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_customradio_fields_to_cart_item' ), 10, 2 );
    10         add_filter( 'woocommerce_get_item_data', array( $this, 'display_custom_fields_in_cart' ), 10, 2 );
    11        
    12         add_action( 'woocommerce_before_calculate_totals', array( $this, 'adjust_cart_item_price' ), 10, 1 );
    13         add_action( 'woocommerce_before_calculate_totals', array( $this, 'adjustradio_cart_item_price' ), 10, 1 );
    14                 //add_action( 'woocommerce_cart_calculate_fees', array( $this, 'icwcu_adjust_cart_total' ), 10, 1 );
    15                
    16                 add_action( 'woocommerce_after_calculate_totals', array( $this, 'adjust_cart_item_priceless' ), 10, 1 );
    17                 add_action( 'woocommerce_after_calculate_totals', array( $this, 'adjustradio_cart_item_priceless' ), 10, 1 );
    18        // add_action( 'woocommerce_cart_item_price', array( $this, 'adjust_cart_item_price' ), 10, 1 );
     17        add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_services_to_cart_item' ), 10, 2 );
     18        add_filter( 'woocommerce_get_item_data', array( $this, 'display_services_in_cart' ), 10, 2 );
     19        add_action( 'woocommerce_before_calculate_totals', array( $this, 'adjust_cart_item_prices' ), 20, 1 );
     20    }
    1921
    20     }
    21    
    22     public function add_custom_fields_to_cart_item( $cart_item_data, $product_id ) {   
    23         if ( ! isset( $_POST['icwcu_custom_checkbox_nonce'] ) || ! wp_verify_nonce( sanitize_text_field(wp_unslash( $_POST['icwcu_custom_checkbox_nonce']) ), 'icwcu_custom_checkbox_nonce_action' ) ) {
    24             return $cart_item_data;
    25         }
    26         $custom_services = get_post_meta( $product_id, 'icwcu_custom_services', true );
    27         if ( ! is_array( $custom_services ) ) {
    28             $custom_services = array();
    29         }
    30    
    31         if ( ! empty( $custom_services ) ) {
    32             foreach ( $custom_services as $service ) {
    33                 $service_key = sanitize_title( $service['name'] );
    34                 if ( isset( $_POST[ $service_key ] ) ) {
    35                     $price = floatval( wp_unslash( $_POST[ $service_key ] ) );
    36                     $cart_item_data[ $service_key ] = ( $price >= 0 ) ? $price : 0;
    37                 }
    38             }
    39         }
    40         return $cart_item_data;
    41     }
    42    
    43     public function add_customradio_fields_to_cart_item( $cart_item_data, $product_id ) {
    44         if ( ! isset( $_POST['icwcu_custom_radio_nonce'] ) || ! wp_verify_nonce( sanitize_text_field(wp_unslash( $_POST['icwcu_custom_radio_nonce']) ), 'icwcu_custom_radio_nonce_action' ) ) {
    45             return $cart_item_data;
    46         }
    47         $custom_servicesradio = get_post_meta( $product_id, 'icwcu_custom_servicesradio', true );
    48         if ( ! is_array( $custom_servicesradio ) ) {
    49             $custom_servicesradio = array();
    50         }
    51    
    52         if ( ! empty( $custom_servicesradio ) ) {
    53             foreach ( $custom_servicesradio as $serviceradio ) {
    54                 $service_keyradio = sanitize_title( $serviceradio['name'] );       
    55                 if ( isset( $_POST[ $service_keyradio ] ) ) {
    56                     $price = floatval( wp_unslash( $_POST[ $service_keyradio ] ) );
    57                     $cart_item_data[ $service_keyradio ] = ( $price >= 0 ) ? $price : 0;
    58                 }
     22    /**
     23     * Save the selected services on the cart item.
     24     * Prices are always read from the product settings, never from the request.
     25     */
     26    public function add_services_to_cart_item( $cart_item_data, $product_id ) {
     27        $selected = array();
    5928
     29        // Checkbox services.
     30        if ( isset( $_POST['icwcu_custom_checkbox_nonce'] )
     31            && is_string( $_POST['icwcu_custom_checkbox_nonce'] )
     32            && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['icwcu_custom_checkbox_nonce'] ) ), 'icwcu_custom_checkbox_nonce_action' )
     33            && ! empty( $_POST['icwcu_services'] ) && is_array( $_POST['icwcu_services'] ) ) {
    6034
     35            $services = icwcu_get_product_services( $product_id, 'checkbox' );
     36            $posted   = array_map( 'sanitize_title', array_filter( wp_unslash( $_POST['icwcu_services'] ), 'is_string' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Non-string entries are discarded and every remaining entry passes through sanitize_title.
    6137
    62             }
    63         }
    64         return $cart_item_data;
    65     }
    66    
    67     public function display_custom_fields_in_cart( $item_data, $cart_item ) {
    68         $custom_services = get_post_meta( $cart_item['product_id'], 'icwcu_custom_services', true );
    69         $custom_servicesradio = get_post_meta( $cart_item['product_id'], 'icwcu_custom_servicesradio', true );
    70    
    71         if ( ! empty( $custom_services ) ) {
    72             foreach ( $custom_services as $service ) {
    73                 $service_key = sanitize_title( $service['name'] );
    74                 if ( isset( $cart_item[ $service_key ] ) ) {
    75                     $item_data[] = array(
    76                         'name'  => $service['name'],
    77                         'value' => get_woocommerce_currency_symbol() . esc_html( $cart_item[ $service_key ] )
     38            foreach ( array_unique( $posted ) as $key ) {
     39                if ( isset( $services[ $key ] ) ) {
     40                    $selected[] = array(
     41                        'name'  => $services[ $key ]['name'],
     42                        'price' => $services[ $key ]['price'],
    7843                    );
    7944                }
    8045            }
    8146        }
    82    
    83         if ( ! empty( $custom_servicesradio ) ) {
    84             foreach ( $custom_servicesradio as $serviceradio ) {
    85                 $service_keyradio = sanitize_title( $serviceradio['name'] );
    86                 if ( isset( $cart_item[ $service_keyradio ] ) ) {
    87                     $item_data[] = array(
    88                         'name'  => $serviceradio['name'],
    89                         'value' => get_woocommerce_currency_symbol() . esc_html( $cart_item[ $service_keyradio ] )
     47
     48        // Radio service (only one can be chosen).
     49        if ( isset( $_POST['icwcu_custom_radio_nonce'] )
     50            && is_string( $_POST['icwcu_custom_radio_nonce'] )
     51            && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['icwcu_custom_radio_nonce'] ) ), 'icwcu_custom_radio_nonce_action' )
     52            && ! empty( $_POST['icwcu_radio_service'] ) && is_string( $_POST['icwcu_radio_service'] ) ) {
     53
     54            $services = icwcu_get_product_services( $product_id, 'radio' );
     55            $key      = sanitize_title( wp_unslash( $_POST['icwcu_radio_service'] ) );
     56
     57            if ( isset( $services[ $key ] ) ) {
     58                $selected[] = array(
     59                    'name'  => $services[ $key ]['name'],
     60                    'price' => $services[ $key ]['price'],
     61                );
     62            }
     63        }
     64
     65        if ( ! empty( $selected ) ) {
     66            $cart_item_data['icwcu_services'] = $selected;
     67        }
     68
     69        return $cart_item_data;
     70    }
     71
     72    /**
     73     * Get the services selected for a cart item.
     74     *
     75     * @param array $cart_item Cart item.
     76     * @return array List of array( 'name' => string, 'price' => float ).
     77     */
     78    public static function get_cart_item_services( $cart_item ) {
     79        if ( ! empty( $cart_item['icwcu_services'] ) && is_array( $cart_item['icwcu_services'] ) ) {
     80            return array_values( array_filter( $cart_item['icwcu_services'], function( $service ) {
     81                return is_array( $service ) && isset( $service['name'], $service['price'] ) && is_string( $service['name'] ) && is_numeric( $service['price'] ) && is_finite( (float) $service['price'] ) && $service['price'] >= 0;
     82            } ) );
     83        }
     84
     85        // Items added to the cart by version 1.3 or older.
     86        $selected = array();
     87        if ( empty( $cart_item['product_id'] ) ) {
     88            return $selected;
     89        }
     90        foreach ( array( 'checkbox', 'radio' ) as $type ) {
     91            foreach ( icwcu_get_product_services( $cart_item['product_id'], $type ) as $key => $service ) {
     92                if ( isset( $cart_item[ $key ] ) && is_numeric( $cart_item[ $key ] ) ) {
     93                    $selected[] = array(
     94                        'name'  => $service['name'],
     95                        'price' => $service['price'],
    9096                    );
     97                    if ( 'radio' === $type ) {
     98                        break;
     99                    }
    91100                }
    92101            }
    93102        }
    94    
     103        return $selected;
     104    }
     105
     106    /**
     107     * Show the selected services under the product name in cart and checkout.
     108     */
     109    public function display_services_in_cart( $item_data, $cart_item ) {
     110        foreach ( self::get_cart_item_services( $cart_item ) as $service ) {
     111            $item_data[] = array(
     112                'name'  => $service['name'],
     113                'value' => icwcu_format_price( $service['price'] ),
     114            );
     115        }
    95116        return $item_data;
    96117    }
    97    
    98118
    99 
    100     private function adjust_cart_item_price_generic( $cart, $meta_key, $add = true ) {
    101         if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
     119    /**
     120     * Add the selected services to the item price.
     121     */
     122    public function adjust_cart_item_prices( $cart ) {
     123        if ( is_admin() && ! wp_doing_ajax() ) {
    102124            return;
    103125        }
    104         foreach ( $cart->get_cart() as $cart_item ) {
    105             $services = get_post_meta( $cart_item['product_id'], $meta_key, true );
    106             if ( ! is_array( $services ) ) {
    107                 $services = array();
     126
     127        foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
     128            $services = self::get_cart_item_services( $cart_item );
     129            if ( empty( $services ) || empty( $cart_item['data'] ) ) {
     130                continue;
    108131            }
    109             if ( ! empty( $services ) ) {
    110                 $additional_cost = 0;
    111                 foreach ( $services as $service ) {
    112                     $service_key = sanitize_title( $service['name'] );
    113                     if ( isset( $cart_item[ $service_key ] ) ) {
    114                         $additional_cost += floatval( $cart_item[ $service_key ] );
    115                     }
    116                 }
    117                 if ( $additional_cost > 0 ) {
    118                     $new_price = $add ?
    119                         $cart_item['data']->get_price() + $additional_cost :
    120                         $cart_item['data']->get_price() - $additional_cost;
    121                     $cart_item['data']->set_price( $new_price );
    122                 }
     132
     133            $extra = 0;
     134            foreach ( $services as $service ) {
     135                $extra += (float) $service['price'];
    123136            }
     137            if ( $extra <= 0 ) {
     138                continue;
     139            }
     140
     141            $product = $cart_item['data'];
     142            $id      = spl_object_hash( $product );
     143            if ( ! isset( $this->base_prices[ $id ] ) ) {
     144                $this->base_prices[ $id ] = (float) $product->get_price( 'edit' );
     145            }
     146
     147            $product->set_price( $this->base_prices[ $id ] + $extra );
    124148        }
    125149    }
    126    
    127     public function adjust_cart_item_price( $cart ) {
    128         $this->adjust_cart_item_price_generic( $cart, 'icwcu_custom_services', true );
    129     }
    130    
    131     public function adjustradio_cart_item_price( $cart ) {
    132         $this->adjust_cart_item_price_generic( $cart, 'icwcu_custom_servicesradio', true );
    133     }
    134    
    135     public function adjust_cart_item_priceless( $cart ) {
    136         $this->adjust_cart_item_price_generic( $cart, 'icwcu_custom_services', false );
    137     }
    138    
    139     public function adjustradio_cart_item_priceless( $cart ) {
    140         $this->adjust_cart_item_price_generic( $cart, 'icwcu_custom_servicesradio', false );
    141     }
    142 
    143         /********
    144         ** Substract from item price
    145         ****/
    146        
    147    
    148        
    149        
    150         public function icwcu_adjust_cart_total( $cart ) {
    151                 if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
    152                         return;
    153                 }
    154 
    155                 $additional_cost = 0;
    156 
    157                 foreach ( $cart->get_cart() as $cart_item ) {
    158                         $custom_services = get_post_meta( $cart_item['product_id'], 'icwcu_custom_services', true );
    159                         if ( is_array( $custom_services ) && ! empty( $custom_services ) ) {
    160                                 foreach ( $custom_services as $service ) {
    161                                         $service_key = sanitize_title( $service['name'] );
    162                                         if ( isset( $cart_item[ $service_key ] ) ) {
    163                                                 $additional_cost += floatval( $cart_item[ $service_key ] );
    164                                         }
    165                                 }
    166                         }
    167 
    168                         $custom_servicesradio = get_post_meta( $cart_item['product_id'], 'icwcu_custom_servicesradio', true );
    169                         if ( is_array( $custom_servicesradio ) && ! empty( $custom_servicesradio ) ) {
    170                                 foreach ( $custom_servicesradio as $serviceradio ) {
    171                                         $service_keyradio = sanitize_title( $serviceradio['name'] );
    172                                         if ( isset( $cart_item[ $service_keyradio ] ) ) {
    173                                                 $additional_cost += floatval( $cart_item[ $service_keyradio ] );
    174                                         }
    175                                 }
    176                         }
    177                 }
    178 
    179                 if ( $additional_cost > 0 ) {
    180                         WC()->cart->add_fee( __( 'Additional Services', 'custom-checkbox-ultimate-for-woocommerce' ), $additional_cost );
    181                 }
    182         }
    183 
    184150}
  • custom-checkbox-ultimate-for-woocommerce/trunk/includes/class-icwcu-init.php

    r3264079 r3759386  
    77    public function __construct() {
    88        // Include required class files and helpers
    9         require_once 'class-icwcu-meta-box.php';
    10         require_once 'class-icwcu-cart.php';
    11         require_once 'class-icwcu-order.php';
    12         require_once 'class-icwcu-settings.php';
    13         require_once 'helpers.php';
     9        require_once __DIR__ . '/class-icwcu-meta-box.php';
     10        require_once __DIR__ . '/class-icwcu-cart.php';
     11        require_once __DIR__ . '/class-icwcu-order.php';
     12        require_once __DIR__ . '/class-icwcu-settings.php';
     13        require_once __DIR__ . '/helpers.php';
    1414       
    1515        // Instantiate the classes to initialize functionality
  • custom-checkbox-ultimate-for-woocommerce/trunk/includes/class-icwcu-meta-box.php

    r3264079 r3759386  
    11<?php
    2 if ( ! defined( 'ABSPATH' ) ) {
    3     exit;
    4 }
     2defined( 'ABSPATH' ) || exit;
    53
     4/** Product editor and classic product form fields. */
    65class ICWCU_Meta_Box {
    76    public function __construct() {
    87        add_action( 'add_meta_boxes', array( $this, 'add_custom_services_meta_box' ) );
    9         add_action( 'save_post', array( $this, 'save_custom_services_meta_box_data' ) );
     8        add_action( 'save_post_product', array( $this, 'save_custom_services_meta_box_data' ) );
    109        add_action( 'admin_enqueue_scripts', array( $this, 'meta_box_scripts' ) );
    1110        add_action( 'woocommerce_before_add_to_cart_button', array( $this, 'add_custom_fields' ) );
    1211    }
    13    
    1412    public function add_custom_services_meta_box() {
    15         add_meta_box(
    16             'icwcu_custom_services_meta_box',
    17             icwcu_get_custom_svg_icon() . 'Custom Services - IC Woo Ultimate',
    18             array( $this, 'display_custom_services_meta_box' ),
    19             'product',
    20             'normal',
    21             'high'
    22         );
     13        add_meta_box( 'icwcu_custom_services_meta_box', __( 'Custom Services - IC Woo Ultimate', 'custom-checkbox-ultimate-for-woocommerce' ), array( $this, 'display_custom_services_meta_box' ), 'product', 'normal', 'high' );
    2314    }
    24    
    2515    public function display_custom_services_meta_box( $post ) {
    26         $custom_services = get_post_meta( $post->ID, 'icwcu_custom_services', true );
    27         if ( ! is_array( $custom_services ) ) {
    28             $custom_services = array();
     16        wp_nonce_field( 'icwcu_custom_services_meta_box_nonce', 'icwcu_custom_services_meta_box_nonce' );
     17        $help_url = current_user_can( 'manage_woocommerce' ) ? ICWCU_Help::url() : ICWCU_DOCUMENTATION_URL;
     18        echo '<p>' . esc_html__( 'Add optional services per product unit. Use checkboxes for multiple choices and radio buttons for one choice.', 'custom-checkbox-ultimate-for-woocommerce' ) . ' <a href="' . esc_url( $help_url ) . '">' . esc_html__( 'Setup guide', 'custom-checkbox-ultimate-for-woocommerce' ) . '</a></p>';
     19        echo '<input class="icwcu-tab-radio" id="icwcu-tab-checkbox" name="icwcu-editor-tab" type="radio" checked><label class="icwcu-tab" for="icwcu-tab-checkbox">' . esc_html__( 'Checkbox Type', 'custom-checkbox-ultimate-for-woocommerce' ) . '</label>';
     20        echo '<input class="icwcu-tab-radio" id="icwcu-tab-radio" name="icwcu-editor-tab" type="radio"><label class="icwcu-tab" for="icwcu-tab-radio">' . esc_html__( 'Radio Type', 'custom-checkbox-ultimate-for-woocommerce' ) . '</label>';
     21        foreach ( array( 'checkbox', 'radio' ) as $type ) {
     22            $meta = 'radio' === $type ? 'icwcu_custom_servicesradio' : 'icwcu_custom_services';
     23            $title_meta = 'radio' === $type ? 'icwcu_custom_radioservices_title' : 'icwcu_custom_services_title';
     24            $saved = get_post_meta( $post->ID, $meta, true );
     25            $services = is_array( $saved ) ? array_values( array_filter( $saved, 'is_array' ) ) : array();
     26            echo '<div class="icwcu-editor-panel" id="icwcu-panel-' . esc_attr( $type ) . '">';
     27            echo '<p><label for="' . esc_attr( $title_meta ) . '">' . esc_html__( 'Custom Services Title:', 'custom-checkbox-ultimate-for-woocommerce' ) . '</label> <input type="text" id="' . esc_attr( $title_meta ) . '" name="' . esc_attr( $title_meta ) . '" value="' . esc_attr( get_post_meta( $post->ID, $title_meta, true ) ) . '"></p>';
     28            echo '<div class="icwcu-service-fields" data-meta="' . esc_attr( $meta ) . '">';
     29            for ( $i = 0; $i < max( 5, count( $services ) ); $i++ ) {
     30                $service = isset( $services[ $i ] ) ? $services[ $i ] : array();
     31                echo '<div class="icwcu-custom-service-field" data-index="' . esc_attr( $i + 1 ) . '">';
     32                foreach ( self::labels() as $key => $label ) {
     33                    $field = $meta . '[' . ( $i + 1 ) . '][' . $key . ']';
     34                    $id = $meta . '-' . ( $i + 1 ) . '-' . $key;
     35                    $value = isset( $service[ $key ] ) && is_scalar( $service[ $key ] ) ? $service[ $key ] : '';
     36                    echo '<p><label for="' . esc_attr( $id ) . '">' . esc_html( $label ) . '</label> ';
     37                    if ( 'desc' === $key ) {
     38                        echo '<textarea id="' . esc_attr( $id ) . '" name="' . esc_attr( $field ) . '">' . esc_textarea( $value ) . '</textarea>';
     39                    } else {
     40                        echo '<input type="' . ( 'price' === $key ? 'number' : 'text' ) . '" ' . ( 'price' === $key ? 'min="0" step="any" ' : '' ) . 'id="' . esc_attr( $id ) . '" name="' . esc_attr( $field ) . '" value="' . esc_attr( $value ) . '">';
     41                    }
     42                    echo '</p>';
     43                }
     44                echo '</div>';
     45            }
     46            echo '</div><button type="button" class="button icwcu-add-fields">+ ' . esc_html__( 'Add More Fields', 'custom-checkbox-ultimate-for-woocommerce' ) . '</button></div>';
    2947        }
    30         $custom_servicesradio = get_post_meta( $post->ID, 'icwcu_custom_servicesradio', true );
    31         if ( ! is_array( $custom_servicesradio ) ) {
    32             $custom_servicesradio = array();
    33         }
    34    
    35         wp_nonce_field( 'icwcu_custom_services_meta_box_nonce', 'icwcu_custom_services_meta_box_nonce' );
    36         $custom_services_title = get_post_meta( $post->ID, 'icwcu_custom_services_title', true );
    37         $custom_radioservices_title = get_post_meta( $post->ID, 'icwcu_custom_radioservices_title', true );
    38    
    39         echo '<input class="radio icwcu-radio" id="one" name="group" type="radio" checked>
    40               <input class="radio icwcu-radio" id="two" name="group" type="radio">
    41               <div class="icwcu-tabs">
    42                   <label class="icwcu-tab" id="one-tab" for="one">'.esc_html__( 'Checkbox Type', 'custom-checkbox-ultimate-for-woocommerce' ).'</label>
    43                   <label class="icwcu-tab" id="two-tab" for="two">'.esc_html__( 'Radio Type', 'custom-checkbox-ultimate-for-woocommerce' ).'</label>
    44               </div>';
    45    
    46         echo '<div class="icwcu-panels">';
    47    
    48         // Checkbox Type Panel
    49         echo '<div class="icwcu-panel" id="one-panel">';
    50         echo '<div id="icwcu-custom-services-title">';
    51         echo '<p><label for="icwcu_custom_services_title">' . esc_html__( 'Custom Services Title:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    52         echo '<input type="text" id="icwcu_custom_services_title" name="icwcu_custom_services_title" value="' . esc_attr( sanitize_text_field($custom_services_title) ) . '" /></p>';
    53         echo '</div>';
    54    
    55         $default_fields = 5;
    56         $field_count = max( count( $custom_services ), $default_fields );
    57    
    58         echo '<div id="icwcu-custom-services-fields">';
    59         for ( $i = 1; $i <= $field_count; $i++ ) {
    60             $service_name = isset( $custom_services[$i]['name'] ) ? $custom_services[$i]['name'] : '';
    61            // $service_price = isset( $custom_services[$i]['price'] ) ? $custom_services[$i]['price'] : '';
    62            $service_price = isset( $custom_services[$i]['price'] ) ? floatval( $custom_services[$i]['price'] ) : 0;
    63             $service_desc = isset( $custom_services[$i]['desc'] ) ? $custom_services[$i]['desc'] : '';
    64 
    65             echo '<div class="icwcu-custom-service-field" data-index="' . esc_html( $i ) . '">';
    66             echo '<p><label for="service_name_' . esc_html( $i ) . '">' . esc_html( $i ) . '. ' . esc_html__( 'Service Name:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    67             echo '<input type="text" id="service_name_' . esc_html( $i ) . '" name="icwcu_custom_services[' . esc_html( $i ) . '][name]" value="' . esc_attr(sanitize_text_field( $service_name) ) . '" />';
    68             echo '<label for="service_price_' . esc_html( $i ) . '">' . esc_html__( 'Service Price', 'custom-checkbox-ultimate-for-woocommerce' ) . ' ' . esc_html( get_woocommerce_currency_symbol() ) . ' </label>';
    69             echo '<input type="number" step="0.01" id="service_price_' . esc_html( $i ) . '" name="icwcu_custom_services[' . esc_html( $i ) . '][price]" value="' . esc_attr( $service_price ) . '" />';
    70             echo '<label for="service_desc_' . esc_html( $i ) . '">' . esc_html__( 'Service Description:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    71             echo '<textarea id="service_desc_' . esc_html( $i ) . '" name="icwcu_custom_services[' . esc_html( $i ) . '][desc]">' . esc_textarea( sanitize_textarea_field($service_desc) ) . '</textarea></p>';
    72             echo '</div>';
    73         }
    74         echo '</div>';
    75         echo '<button type="button" id="icwcu-add-more-fields" class="button">+ ' . esc_html__( 'Add More Fields', 'custom-checkbox-ultimate-for-woocommerce' ) . '</button>';
    76         echo '</div>';
    77    
    78         // Radio Type Panel
    79         echo '<div class="icwcu-panel" id="two-panel">';
    80         echo '<div id="icwcu-custom-services-title">';
    81         echo '<p><label for="icwcu_custom_radioservices_title">' . esc_html__( 'Custom Radio Services Title:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    82         echo '<input type="text" id="icwcu_custom_radioservices_title" name="icwcu_custom_radioservices_title" value="' . esc_attr( sanitize_text_field($custom_radioservices_title) ) . '" /></p>';
    83         echo '</div>';
    84    
    85         $default_fieldsradio = 5;
    86         $field_countradio = max( count( $custom_servicesradio ), $default_fieldsradio );
    87    
    88         echo '<div id="icwcu-custom-radio-services-fields">';
    89         for ( $i = 1; $i <= $field_countradio; $i++ ) {
    90             $radioservice_name = isset( $custom_servicesradio[$i]['name'] ) ? $custom_servicesradio[$i]['name'] : '';
    91           //  $radioservice_price = isset( $custom_servicesradio[$i]['price'] ) ? $custom_servicesradio[$i]['price'] : '';
    92             $radioservice_price = isset( $custom_servicesradio[$i]['price'] ) ? floatval( $custom_servicesradio[$i]['price'] ) : 0;
    93             $radioservice_desc = isset( $custom_servicesradio[$i]['desc'] ) ? $custom_servicesradio[$i]['desc'] : '';
    94             echo '<div class="icwcu-custom-service-field" data-index="' . esc_html( $i ) . '">';
    95             echo '<p><label for="radioservice_name_' . esc_html( $i ) . '">' . esc_html( $i ) . '. ' . esc_html__( 'Service Name:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    96             echo '<input type="text" id="radioservice_name_' . esc_html( $i ) . '" name="icwcu_custom_servicesradio[' . esc_html( $i ) . '][name]" value="' . esc_attr( sanitize_text_field($radioservice_name) ) . '" />';
    97             echo '<label for="radioservice_price_' . esc_html( $i ) . '">' . esc_html__( 'Service Price', 'custom-checkbox-ultimate-for-woocommerce' ) . ' ' . esc_html( get_woocommerce_currency_symbol() ) . ' </label>';
    98             echo '<input type="number" step="0.01" id="radioservice_price_' . esc_html( $i ) . '" name="icwcu_custom_servicesradio[' . esc_html( $i ) . '][price]" value="' . esc_attr(  $radioservice_price  )  . '" />';
    99             echo '<label for="radioservice_desc_' . esc_html( $i ) . '">' . esc_html__( 'Service Description:', 'custom-checkbox-ultimate-for-woocommerce' ) . ' </label>';
    100             echo '<textarea id="radioservice_desc_' . esc_html( $i ) . '" name="icwcu_custom_servicesradio[' . esc_html( $i ) . '][desc]">' . esc_textarea( sanitize_textarea_field($radioservice_desc )) . '</textarea></p>';
    101             echo '</div>';
    102         }
    103         echo '</div>';
    104         echo '<button type="button" id="icwcu-add-moreradio-fields" class="button">+ ' . esc_html__( 'Add More Fields', 'custom-checkbox-ultimate-for-woocommerce' ) . '</button>';
    105         echo '</div>';
    106    
    107         echo '</div>'; // End of panels
    10848    }
    109    
     49    public static function labels() {
     50        return array( 'name' => __( 'Service Name:', 'custom-checkbox-ultimate-for-woocommerce' ), 'price' => __( 'Service Price:', 'custom-checkbox-ultimate-for-woocommerce' ), 'desc' => __( 'Service Description:', 'custom-checkbox-ultimate-for-woocommerce' ) );
     51    }
    11052    public function meta_box_scripts( $hook ) {
    111         if ( 'post.php' != $hook && 'post-new.php' != $hook ) {
     53        $screen = get_current_screen();
     54        if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) || ! $screen || 'product' !== $screen->post_type ) {
    11255            return;
    11356        }
    114    
    115         wp_enqueue_script( 'icwcu-custom-services-meta-box', plugin_dir_url( __FILE__ ) . '../assets/js/custom-services-meta-box.js', array( 'jquery' ), ICWCU_PLUGIN_VERSION, true );
    116         $initial_field_count = max( 5, count( (array) get_post_meta( get_the_ID(), 'icwcu_custom_services', true ) ) );
    117         $initial_radiofield_count = max( 5, count( (array) get_post_meta( get_the_ID(), 'icwcu_custom_servicesradio', true ) ) );
    118         wp_localize_script( 'icwcu-custom-services-meta-box', 'customServicesMetaBox', array(
    119             'initialFieldCount' => $initial_field_count,
    120             'currencySymbol'    => get_woocommerce_currency_symbol(),
    121         ) );
    122         wp_localize_script( 'icwcu-custom-services-meta-box', 'customRadioServicesMetaBox', array(
    123             'initialRadioFieldCount' => $initial_radiofield_count
    124         ) );
     57        wp_enqueue_script( 'icwcu-custom-services-meta-box', plugins_url( 'assets/js/custom-services-meta-box.js', ICWCU_PLUGIN_FILE ), array(), ICWCU_PLUGIN_VERSION, true );
     58        wp_localize_script( 'icwcu-custom-services-meta-box', 'icwcuFields', self::labels() );
    12559    }
    126    
    12760    public function save_custom_services_meta_box_data( $post_id ) {
    128         if ( ! isset( $_POST['icwcu_custom_services_meta_box_nonce'] ) ||
    129             ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['icwcu_custom_services_meta_box_nonce'] ) ), 'icwcu_custom_services_meta_box_nonce' ) ) {
     61        if ( ! isset( $_POST['icwcu_custom_services_meta_box_nonce'] ) || ! is_string( $_POST['icwcu_custom_services_meta_box_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['icwcu_custom_services_meta_box_nonce'] ) ), 'icwcu_custom_services_meta_box_nonce' ) ) {
    13062            return;
    13163        }
    132         if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
     64        if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $post_id ) || 'product' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
    13365            return;
    13466        }
    135         if ( isset( $_POST['post_type'] ) && 'product' === $_POST['post_type'] ) {
    136             if ( ! current_user_can( 'edit_post', $post_id ) ) {
    137                 return;
     67        foreach ( array( 'icwcu_custom_services', 'icwcu_custom_servicesradio' ) as $meta ) {
     68            if ( isset( $_POST[ $meta ] ) ) {
     69                update_post_meta( $post_id, $meta, icwcu_sanitize_services( wp_unslash( $_POST[ $meta ] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- icwcu_sanitize_services validates each row and sanitizes all allowed fields.
    13870            }
    139         } else {
     71        }
     72        foreach ( array( 'icwcu_custom_services_title', 'icwcu_custom_radioservices_title' ) as $meta ) {
     73            if ( isset( $_POST[ $meta ] ) && is_string( $_POST[ $meta ] ) ) {
     74                update_post_meta( $post_id, $meta, sanitize_text_field( wp_unslash( $_POST[ $meta ] ) ) );
     75            }
     76        }
     77    }
     78    public function add_custom_fields() {
     79        global $product;
     80        if ( ! $product instanceof WC_Product ) {
    14081            return;
    14182        }
    142    
    143         if ( isset( $_POST['icwcu_custom_services'] ) ) {
    144             $custom_services = wp_unslash( $_POST['icwcu_custom_services'] );
    145             if ( is_array( $custom_services ) ) {
    146                 array_walk_recursive( $custom_services, 'icwcu_sanitize_service_item' );
    147                 // Filter out empty services
    148                 $custom_services = array_filter( $custom_services, function( $service ) {
    149                     return ! empty( $service['name'] );
    150                 } );
    151             } else {
    152                 $custom_services = array();
     83        foreach ( array( 'checkbox', 'radio' ) as $type ) {
     84            $services = icwcu_get_product_services( $product->get_id(), $type );
     85            if ( ! $services ) {
     86                continue;
    15387            }
    154             update_post_meta( $post_id, 'icwcu_custom_services', $custom_services );
    155         }
    156        
    157         if ( isset( $_POST['icwcu_custom_servicesradio'] ) ) {
    158             $custom_servicesradio = wp_unslash( $_POST['icwcu_custom_servicesradio'] );
    159             if ( is_array( $custom_servicesradio ) ) {
    160                 array_walk_recursive( $custom_servicesradio, 'icwcu_sanitize_service_item' );
    161                 $custom_servicesradio = array_filter( $custom_servicesradio, function( $service ) {
    162                     return ! empty( $service['name'] );
    163                 } );
    164             } else {
    165                 $custom_servicesradio = array();
     88            $nonce = 'icwcu_custom_' . $type . '_nonce';
     89            wp_nonce_field( $nonce . '_action', $nonce, false );
     90            $title_key = 'radio' === $type ? 'icwcu_custom_radioservices_title' : 'icwcu_custom_services_title';
     91            $title = get_post_meta( $product->get_id(), $title_key, true );
     92            echo '<fieldset class="icwcu-custom-services"><legend>' . esc_html( $title ? $title : __( 'Additional Services', 'custom-checkbox-ultimate-for-woocommerce' ) ) . '</legend>';
     93            if ( 'radio' === $type ) {
     94                echo '<label class="icwcu-service"><input type="radio" name="icwcu_radio_service" value="" checked> ' . esc_html__( 'No additional radio service', 'custom-checkbox-ultimate-for-woocommerce' ) . '</label>';
    16695            }
    167             update_post_meta( $post_id, 'icwcu_custom_servicesradio', $custom_servicesradio );
    168         }
    169        
    170    
    171         if ( isset( $_POST['icwcu_custom_services_title'] ) ) {
    172             update_post_meta(
    173                 $post_id,
    174                 'icwcu_custom_services_title',
    175                 sanitize_text_field( wp_unslash( $_POST['icwcu_custom_services_title'] ) )
    176             );
    177         }
    178         if ( isset( $_POST['icwcu_custom_radioservices_title'] ) ) {
    179             update_post_meta(
    180                 $post_id,
    181                 'icwcu_custom_radioservices_title',
    182                 sanitize_text_field( wp_unslash( $_POST['icwcu_custom_radioservices_title'] ) )
    183             );
    184         }
    185     }
    186    
    187     public function add_custom_fields() {
    188         global $post;
    189         $custom_services = get_post_meta( $post->ID, 'icwcu_custom_services', true );
    190         $custom_servicesradio = get_post_meta( $post->ID, 'icwcu_custom_servicesradio', true );
    191        
    192         wp_nonce_field( 'icwcu_custom_checkbox_nonce_action', 'icwcu_custom_checkbox_nonce' );
    193         wp_nonce_field( 'icwcu_custom_radio_nonce_action', 'icwcu_custom_radio_nonce' );
    194         if ( ! is_array( $custom_services ) ) {
    195             $custom_services = array();
    196         }
    197         if ( ! is_array( $custom_servicesradio ) ) {
    198             $custom_servicesradio = array();
    199         }
    200    
    201         if ( ! empty( $custom_services ) ) {
    202             echo '<div class="icwcu-custom-services">';
    203             $custom_services_title = get_post_meta( $post->ID, 'icwcu_custom_services_title', true );
    204             if ( ! empty( $custom_services_title ) ) {
    205                 echo '<h4 class="icwcu-checkbox-title">' . esc_html( $custom_services_title ) . '</h4><table>';
    206             } else {
    207                 echo '<h4 class="icwcu-checkbox-title">' . esc_html__( 'Additional Services', 'custom-checkbox-ultimate-for-woocommerce' ) . '</h4><table>';
     96            foreach ( $services as $key => $service ) {
     97                $id = 'icwcu-' . $type . '-' . $key;
     98                $name = 'radio' === $type ? 'icwcu_radio_service' : 'icwcu_services[]';
     99                echo '<div class="icwcu-service"><label for="' . esc_attr( $id ) . '"><input id="' . esc_attr( $id ) . '" type="' . esc_attr( $type ) . '" name="' . esc_attr( $name ) . '" value="' . esc_attr( $key ) . '" aria-describedby="' . esc_attr( $id . '-description' ) . '"> ' . esc_html( $service['name'] ) . '</label> <span class="icwcu-currency">' . wp_kses_post( wc_price( wc_get_price_to_display( $product, array( 'price' => $service['price'] ) ) ) ) . '</span><small id="' . esc_attr( $id . '-description' ) . '" class="icwcu-service-description">' . esc_html( $service['desc'] ) . '</small></div>';
    208100            }
    209             foreach ( $custom_services as $service ) {
    210                 if ( ! empty( $service['name'] ) && ! empty( $service['price'] ) ) {
    211                     echo '<tr><td><label><input type="checkbox" class="icwcu-checkbox" name="' . esc_html( $service['name'] ) . '" value="' . esc_attr( $service['price'] ) . '">' . esc_html( $service['name'] );
    212    
    213                     if ( ! empty( $service['desc'] ) ) {
    214             //            echo '<span class="icwcu-tooltip">'.wp_kses(icwcu_get_frontend_svg_icon()).'<span class="icwcu-tooltiptext">' . esc_html( $service['desc'] ) . '</span></span></td>';
    215             echo '<span class="icwcu-tooltip">' .
    216     wp_kses( icwcu_get_frontend_svg_icon(), array(
    217         'svg' => array(
    218             'xmlns' => array(),
    219             'width' => array(),
    220             'height' => array(),
    221             'fill' => array(),
    222             'class' => array(),
    223             'viewBox' => array(),
    224         ),
    225         'path' => array(
    226             'd' => array(),
    227         ),
    228     ) ) .
    229     '<span class="icwcu-tooltiptext">' . esc_html( $service['desc'] ) . '</span></span>';
    230 
    231                     }
    232    
    233                     echo '<td></td><td><span class="icwcu-currency">' . esc_html( get_woocommerce_currency_symbol() ) . esc_attr( $service['price'] ) . '</span></td>';
    234                     echo '</label></tr>';
    235                 }
    236             }
    237             echo '</table></div>';
    238         }
    239        
    240         if ( ! empty( $custom_servicesradio ) ) {
    241             echo '<div class="icwcu-custom-services">';
    242             $custom_services_title = get_post_meta( $post->ID, 'icwcu_custom_radioservices_title', true );
    243             if ( ! empty( $custom_services_title ) ) {
    244                 echo '<h4 class="icwcu-checkbox-title">' . esc_html( $custom_services_title ) . '</h4><table>';
    245             } else {
    246                 echo '<h4 class="icwcu-checkbox-title">' . esc_html__( 'Additional Services', 'custom-checkbox-ultimate-for-woocommerce' ) . '</h4><table>';
    247             }
    248             foreach ( $custom_servicesradio as $serviceradio ) {
    249                 if ( ! empty( $serviceradio['name'] ) && ! empty( $serviceradio['price'] ) ) {
    250                     echo '<tr><td><label><input type="radio" class="icwcu-radio" data-group="icwcupayment"  name="' . esc_html( sanitize_title( $serviceradio['name'] ) ) . '" value="' . esc_attr( $serviceradio['price'] ) . '">' . esc_html( $serviceradio['name'] );
    251    
    252                     if ( ! empty( $serviceradio['desc'] ) ) {
    253                       //  echo '<span class="icwcu-tooltip">'.wp_kses(icwcu_get_frontend_svg_icon()).'<span class="icwcu-tooltiptext">' . esc_html( $serviceradio['desc'] ) . '</span></span></td>';
    254                       echo '<span class="icwcu-tooltip">' .
    255                             wp_kses( icwcu_get_frontend_svg_icon(), array(
    256                                 'svg' => array(
    257                                     'xmlns' => array(),
    258                                     'width' => array(),
    259                                     'height' => array(),
    260                                     'fill' => array(),
    261                                     'class' => array(),
    262                                     'viewBox' => array(),
    263                                 ),
    264                                 'path' => array(
    265                                     'd' => array(),
    266                                 ),
    267                             ) ) .
    268                             '<span class="icwcu-tooltiptext">' . esc_html( $serviceradio['desc'] ) . '</span></span>';
    269 
    270                     }
    271    
    272                     echo '<td></td><td><span class="icwcu-currency">' . esc_html( get_woocommerce_currency_symbol() ) . esc_attr( $serviceradio['price'] ) . '</span></td>';
    273                     echo '</label></tr>';
    274                 }
    275             }
    276             echo '</table></div>';
     101            echo '</fieldset>';
    277102        }
    278103    }
  • custom-checkbox-ultimate-for-woocommerce/trunk/includes/class-icwcu-order.php

    r3264079 r3759386  
    66class ICWCU_Order {
    77    public function __construct() {
    8         add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'add_custom_fields_to_order_items' ), 10, 4 );
    9         add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'add_customradio_fields_to_order_items' ), 10, 4 );
     8        // Runs for both classic checkout and the Checkout block (Store API).
     9        add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'add_services_to_order_item' ), 10, 4 );
    1010    }
    11    
    12     public function add_custom_fields_to_order_items( $item, $cart_item_key, $values, $order ) {
    13         $custom_services = get_post_meta( $values['product_id'], 'icwcu_custom_services', true );
    14         if ( ! is_array( $custom_services ) ) {
    15             $custom_services = array();
    16         }
    17    
    18         if ( ! empty( $custom_services ) ) {
    19             foreach ( $custom_services as $service ) {
    20                 $service_key = sanitize_title( $service['name'] );
    21                 if ( isset( $values[ $service_key ] ) ) {
    22                     $item->add_meta_data( $service['name'], get_woocommerce_currency_symbol() . $values[ $service_key ] );
    23                 }
    24             }
    25         }
    26     }
    27    
    28     public function add_customradio_fields_to_order_items( $item, $cart_item_key, $values, $order ) {
    29         $custom_servicesradio = get_post_meta( $values['product_id'], 'icwcu_custom_servicesradio', true );
    30         if ( ! is_array( $custom_servicesradio ) ) {
    31             $custom_servicesradio = array();
    32         }
    33    
    34         if ( ! empty( $custom_servicesradio ) ) {
    35             foreach ( $custom_servicesradio as $serviceradio ) {
    36                 $service_keyradio = sanitize_title( $serviceradio['name'] );
    37                 if ( isset( $values[ $service_keyradio ] ) ) {
    38                     $item->add_meta_data( $serviceradio['name'], get_woocommerce_currency_symbol() . $values[ $service_keyradio ] );
    39                 }
    40             }
     11
     12    public function add_services_to_order_item( $item, $cart_item_key, $values, $order ) {
     13        foreach ( ICWCU_Cart::get_cart_item_services( $values ) as $service ) {
     14            $item->add_meta_data( $service['name'], icwcu_format_price( $service['price'] ) );
    4115        }
    4216    }
  • custom-checkbox-ultimate-for-woocommerce/trunk/includes/class-icwcu-settings.php

    r3264079 r3759386  
    4747        global $post;
    4848   
    49         if ( is_product() ) {
     49        if ( function_exists( 'is_product' ) && is_product() && $post instanceof WP_Post ) {
    5050            $custom_servicesradio = get_post_meta( $post->ID, 'icwcu_custom_servicesradio', true );
    5151   
    5252            if ( ! empty( $custom_servicesradio ) ) {
    53                 wp_enqueue_script( 'icwcu-radio-script', plugin_dir_url( __FILE__ ) . '../assets/js/custom-checkbox-frontend.js', array( 'jquery' ), ICWCU_PLUGIN_VERSION, true );
     53                wp_enqueue_script( 'icwcu-radio-script', plugin_dir_url( __FILE__ ) . '../assets/js/custom-checkbox-frontend.js', array(), ICWCU_PLUGIN_VERSION, array( 'in_footer' => true, 'strategy' => 'defer' ) );
    5454            }
    5555        }
  • custom-checkbox-ultimate-for-woocommerce/trunk/includes/enqueue.php

    r3264079 r3759386  
    11<?php
    2 /*
    3 Plugin Name: Custom Checkbox Ultimate for WooCommerce
    4 *
    5 */
     2defined( 'ABSPATH' ) || exit;
    63
    7 if ( ! defined( 'ABSPATH' ) ) {
    8     exit; // Exit if accessed directly
    9 }
    10 /*
    11 Enqueue Styles
    12 */
    13 // Enqueue custom SVG icon
    14 add_action('admin_enqueue_scripts', 'icwcu_enqueue_custom_svg_icon');
    15 function icwcu_enqueue_custom_svg_icon() {
    16     wp_enqueue_style('icwcu-custom-svg-icon', plugin_dir_url(__FILE__) . '../assets/css/icwcu-admin-styling.css', array(), ICWCU_PLUGIN_VERSION );
    17        
     4add_action( 'admin_enqueue_scripts', 'icwcu_enqueue_admin_styles' );
     5function icwcu_enqueue_admin_styles( $hook ) {
     6    $screen = get_current_screen();
     7    if ( 'toplevel_page_ic-woo-ultimate' === $hook || ( $screen && 'product' === $screen->post_type ) ) {
     8        wp_enqueue_style( 'icwcu-admin-style', plugins_url( 'assets/css/icwcu-admin-styling.css', ICWCU_PLUGIN_FILE ), array(), ICWCU_PLUGIN_VERSION );
     9    }
    1810}
    1911
    20 // Enqueue custom CSS for frontend product page
    21 add_action('wp_enqueue_scripts', 'icwcu_enqueue_custom_css');
     12add_action( 'wp_enqueue_scripts', 'icwcu_enqueue_custom_css' );
    2213function icwcu_enqueue_custom_css() {
    23     // Check if WooCommerce is active
    24     if (class_exists('WooCommerce') && function_exists('is_product') && is_product()) {
    25         wp_enqueue_style('icwcu-custom-services-style', plugin_dir_url(__FILE__) . '../assets/css/icwcu-services-style.css', array(),ICWCU_PLUGIN_VERSION );
     14    if ( function_exists( 'is_product' ) && is_product() ) {
     15        wp_enqueue_style( 'icwcu-custom-services-style', plugins_url( 'assets/css/icwcu-services-style.css', ICWCU_PLUGIN_FILE ), array(), ICWCU_PLUGIN_VERSION );
    2616    }
    2717}
    28 function icwcu_enqueue_admin_styles($hook) {
    29     // Check if we are on the plugin's dashboard page
    30     if ( 'toplevel_page_ic-woo-ultimate' != $hook ) {
    31         return;
    32     }
    33    
    34     // Enqueue the admin CSS file
    35     wp_enqueue_style(
    36         'icwcu-admin-style',
    37         plugin_dir_url(__FILE__) . '../assets/css/icwcu-admin-styling.css', // Path to your CSS file
    38         [],
    39         ICWCU_PLUGIN_VERSION // Version
    40     );
    41 }
    42 add_action( 'admin_enqueue_scripts', 'icwcu_enqueue_admin_styles' );
  • custom-checkbox-ultimate-for-woocommerce/trunk/includes/helpers.php

    r3264079 r3759386  
    11<?php
    2 /*
    3 * Plugin Name: Custom Checkbox Ultimate for WooCommerce
    4 *
    5 */
    62if ( ! defined( 'ABSPATH' ) ) {
    73    exit;
    … …  
    4945                }
    5046        }
     47
     48if ( ! function_exists( 'icwcu_get_product_services' ) ) {
     49        /**
     50         * Get the saved services of a product.
     51         *
     52         * @param int    $product_id Product ID.
     53         * @param string $type       'checkbox' or 'radio'.
     54         * @return array Services keyed by slug: array( 'name' => string, 'price' => float, 'desc' => string ).
     55         */
     56        function icwcu_get_product_services( $product_id, $type = 'checkbox' ) {
     57                $meta_key = ( 'radio' === $type ) ? 'icwcu_custom_servicesradio' : 'icwcu_custom_services';
     58                $services = get_post_meta( $product_id, $meta_key, true );
     59                $result   = array();
     60
     61                if ( ! is_array( $services ) ) {
     62                        return $result;
     63                }
     64
     65                foreach ( icwcu_sanitize_services( $services ) as $index => $service ) {
     66                        if ( ! is_array( $service ) || empty( $service['name'] ) ) {
     67                                continue;
     68                        }
     69                        $key = sanitize_title( $service['name'] );
     70                        if ( '' === $key ) {
     71                                continue;
     72                        }
     73                        if ( isset( $result[ $key ] ) ) {
     74                                $key .= '-' . sanitize_key( (string) $index );
     75                        }
     76                        $result[ $key ] = array(
     77                                'name'  => (string) $service['name'],
     78                                'price' => isset( $service['price'] ) ? max( 0, (float) $service['price'] ) : 0,
     79                                'desc'  => isset( $service['desc'] ) ? (string) $service['desc'] : '',
     80                        );
     81                }
     82
     83                return $result;
     84        }
     85}
     86
     87/** Normalize submitted rows, removing gaps and rejecting malformed fields. */
     88function icwcu_sanitize_services( $rows ) {
     89    $result = array();
     90    if ( ! is_array( $rows ) ) {
     91        return $result;
     92    }
     93    foreach ( $rows as $row ) {
     94        if ( ! is_array( $row ) || ! isset( $row['name'] ) || ! is_string( $row['name'] ) ) {
     95            continue;
     96        }
     97        $name = sanitize_text_field( $row['name'] );
     98        if ( '' === trim( $name ) ) {
     99            continue;
     100        }
     101        $price = isset( $row['price'] ) && is_scalar( $row['price'] ) ? (float) wc_format_decimal( $row['price'] ) : 0;
     102        $result[ count( $result ) + 1 ] = array(
     103            'name' => $name,
     104            'price' => is_finite( $price ) ? max( 0, $price ) : 0,
     105            'desc' => isset( $row['desc'] ) && is_string( $row['desc'] ) ? sanitize_textarea_field( $row['desc'] ) : '',
     106        );
     107    }
     108    return $result;
     109}
     110
     111if ( ! function_exists( 'icwcu_format_price' ) ) {
     112        /**
     113         * Format a price as plain text (safe for cart item data and order item meta).
     114         *
     115         * @param float $price Price.
     116         * @return string
     117         */
     118        function icwcu_format_price( $price ) {
     119                return html_entity_decode( wp_strip_all_tags( wc_price( $price ) ), ENT_QUOTES, 'UTF-8' );
     120        }
     121}
  • custom-checkbox-ultimate-for-woocommerce/trunk/includes/plugin-info.php

    r3264079 r3759386  
    11<?php
    2 if ( ! defined( 'ABSPATH' ) ) {
    3     exit;
    4 }
     2defined( 'ABSPATH' ) || exit;
    53?>
    6 <div class="wrap">
    7     <h1><?php esc_html_e( 'Custom Checkbox Ultimate for WooCommerce Documentation', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h1>
    8    
    9     <h2><?php esc_html_e( 'Introduction', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
    10     <p><?php esc_html_e( 'Custom Checkbox Ultimate for WooCommerce is a plugin that allows you to add customizable checkbox options on WooCommerce product pages for additional services and charges.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
    11    
    12     <h2><?php esc_html_e( 'Installation', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
     4<div class="wrap icwcu-guide">
     5    <h1><?php esc_html_e( 'Custom Checkbox Ultimate: setup guide', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h1>
     6    <p><?php esc_html_e( 'Offer optional services such as gift wrapping, assembly, or premium packaging. Configure each product in the classic WooCommerce product editor.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     7    <h2><?php esc_html_e( '1. Configure a product', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
    138    <ol>
    14         <li><?php esc_html_e( 'Navigate to Plugins > Add New in your WordPress dashboard.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    15         <li><?php esc_html_e( 'Click on "Upload Plugin" and select the plugin ZIP file.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    16         <li><?php esc_html_e( 'Click "Install Now" and then "Activate" to enable the plugin.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
     9        <li><?php esc_html_e( 'Install and activate WooCommerce, then open Products > All Products and edit a simple or variable product.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
     10        <li><?php esc_html_e( 'Find Custom Services - IC Woo Ultimate below the editor. If it is hidden, enable it in Screen Options.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
     11        <li><?php esc_html_e( 'In Checkbox Type, enter a group title and services customers can combine. In Radio Type, enter alternatives where customers choose at most one.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
     12        <li><?php esc_html_e( 'For each row, enter a service name, a non-negative price in the store currency, and an optional description. Zero-priced services are supported. Blank names are ignored.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
     13        <li><?php esc_html_e( 'Use Add More Fields for additional rows. To remove a service, clear its name. Update or publish the product.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    1714    </ol>
    18    
    19     <h2><?php esc_html_e( 'Setup', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
    20     <ol>
    21         <li><?php esc_html_e( 'Go to WooCommerce > Settings > Custom Checkbox Ultimate.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    22         <li><?php esc_html_e( 'Configure the default settings for your checkboxes, such as label text, default state, and additional costs.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    23         <li><?php esc_html_e( 'Save your settings.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    24     </ol>
    25    
    26     <h2><?php esc_html_e( 'Usage', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
    27     <ol>
    28         <li><?php esc_html_e( 'Edit the product where you want to add a custom checkbox.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    29         <li><?php esc_html_e( 'In the product data section, navigate to the "Custom Checkbox" tab.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    30         <li><?php esc_html_e( 'Enable the checkbox option and customize the label, description, and additional cost as needed.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    31         <li><?php esc_html_e( 'Update or publish the product to apply the changes.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></li>
    32     </ol>
    33    
    34     <h2><?php esc_html_e( 'Support', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
    35     <p><?php esc_html_e( 'For further assistance, please visit our support page or contact us at support@insertcart.com.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     15    <h2><?php esc_html_e( '2. Test the customer journey', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
     16    <p><?php esc_html_e( 'Open the product page, select services, choose a variation if applicable, and add the product to the cart. Radio services include a no-service option. Selections are optional and are not preselected.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     17    <p><?php esc_html_e( 'The product headline price does not change before adding to cart. In the cart, selected service costs are added to each product unit. For a product priced at 20 with a service priced at 5, quantity 2 has a subtotal of 50 before tax, discounts, and shipping. Taxes and coupons follow the product settings in WooCommerce.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     18    <h2><?php esc_html_e( '3. Review orders', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
     19    <p><?php esc_html_e( 'Selected service names and unit costs appear beneath the product in the cart and checkout. After checkout, find them on the order line item in WooCommerce > Orders. They also appear in standard WooCommerce order details and emails. Refunds use the product line amount; services are not separate fee lines.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     20    <h2><?php esc_html_e( 'Compatibility and limitations', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
     21    <p><?php esc_html_e( 'Variable products share services configured on the parent product. Existing selections work with classic and block cart/checkout and with HPOS. Service selection requires the standard WooCommerce product add-to-cart form; the block Add to Cart form and direct Store API selection are not provided. Shop-loop quick add does not collect services. Grouped, external, subscription, bundle, and custom product forms need separate testing.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     22    <h2><?php esc_html_e( 'Troubleshooting and privacy', 'custom-checkbox-ultimate-for-woocommerce' ); ?></h2>
     23    <p><?php esc_html_e( 'If options are missing, confirm that service names are saved and the theme renders the standard add-to-cart hook. If selections are missing from the cart, refresh the product page and exclude cart and checkout from full-page caching. Test theme and add-on conflicts on a staging site. After changing prices, remove the old cart line and add the product again.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     24    <p><?php esc_html_e( 'The plugin sends no analytics or service data to an external server. Configuration is stored with products; selections are stored in WooCommerce carts and order line metadata. Deactivation preserves configuration and orders.', 'custom-checkbox-ultimate-for-woocommerce' ); ?></p>
     25    <p><a class="button" href="<?php echo esc_url( admin_url( 'edit.php?post_type=product' ) ); ?>"><?php esc_html_e( 'Open products', 'custom-checkbox-ultimate-for-woocommerce' ); ?></a> <a class="button" href="<?php echo esc_url( ICWCU_DOCUMENTATION_URL ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Online documentation', 'custom-checkbox-ultimate-for-woocommerce' ); ?></a> <a href="https://wordpress.org/support/plugin/custom-checkbox-ultimate-for-woocommerce/"><?php esc_html_e( 'Support forum', 'custom-checkbox-ultimate-for-woocommerce' ); ?></a></p>
    3626</div>
    37 
  • custom-checkbox-ultimate-for-woocommerce/trunk/readme.txt

    r3264079 r3759386  
    11=== Custom Checkbox Ultimate for WooCommerce ===
    22Contributors: sandy786
    3 Tags: WooCommerce, checkbox, additional services, customizable options, e-commerce
    4 Plugin URI: https://www.insertcart.com/insertcart-custom-checkbox-ultimate-for-woocommerce-documentation/
    5 Author URI: https://www.insertcart.com/
     3Tags: woocommerce, checkbox, product add-ons, extra services, radio buttons
    64Requires at least: 6.3
    7 Tested up to: 6.7
     5Tested up to: 7.1
    86Requires PHP: 7.2
    9 Stable tag: 1.3
    10 License: GPL v2 or later
     7Requires Plugins: woocommerce
     8Stable tag: 1.4
     9License: GPLv2 or later
    1110License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1211
    13 Add customizable checkbox options on WooCommerce product pages for additional services and charges.
     12Add optional checkbox and radio services to WooCommerce products, with per-unit prices and service details in the cart and orders.
    1413
    1514== Description ==
    16 Custom Checkbox Ultimate for Woocommerce is a powerful WooCommerce extension that allows store owners to offer additional services and charges to customers through customizable checkboxes on product pages. Enhance your product offerings by enabling customers to select extra features, services, or options, seamlessly integrating additional costs into their purchases.
    1715
    18 == Key Features ==
     16Offer gift wrapping, assembly, premium packaging, and other optional services on WooCommerce product pages. Configure services for each product in the classic product editor.
    1917
    20 **Customizable Checkbox Options:** Easily add multiple checkbox fields to your WooCommerce product pages for additional services.
    21 **Dynamic Pricing:** Automatically adjust product prices based on selected additional services.
    22 **Service Descriptions:** Provide detailed descriptions for each additional service using tooltips for better customer understanding.
    23 **Admin-Friendly Interface:** Manage and configure additional services directly from the product edit screen with an intuitive meta box.
    24 **Order Integration:** Ensure that selected services and their costs are accurately reflected in the cart, checkout, and order details.
    25 **Flexible Field Management:** Add as many custom service fields as needed with ease.
     18* Checkbox services: customers can select several options.
     19* Radio services: customers choose at most one option or no additional service.
     20* Set a name, non-negative price, and description for each service. Free services are supported.
     21* Start with five rows per type and add more when needed.
     22* Service prices come from saved product settings, never customer-submitted prices.
     23* Selected services increase the product unit price and appear in the cart, checkout, order details, and standard emails.
     24* Works with simple products and parent-level services on variable products.
     25* Supports HPOS and classic or block cart/checkout for items added through the standard product form.
    2626
    27 Check out our [Documentation](https://www.insertcart.com/insertcart-custom-checkbox-ultimate-for-woocommerce-documentation/), [Support Ticket](https://www.insertcart.com/support-ticket/), and [Contact us](https://www.insertcart.com/contact-us/).
     27Descriptions are displayed beside the service options, including for keyboard and touch users. The product headline price does not update as options are selected; the adjusted price appears in the cart.
    2828
    29 ==Installation==
     29= Documentation and first setup =
    3030
    31 Upload the Plugin:
     31Open **IC Woo Ultimate** in the WordPress dashboard for the included setup guide. You can also use **Plugins > Setup guide** or the guide link inside the Custom Services product meta box. The included guide works without an external website.
    3232
    33 *   Download the plugin zip file.
    34 *   In your WordPress dashboard, navigate to Plugins > Add New.
    35 *   Click on the Upload Plugin button at the top.
    36 *   Choose the downloaded zip file and click Install Now.
    37 *   Activate the Plugin:
    38 *   After installation, click on Activate Plugin.
    39 *   Ensure WooCommerce is Active:
     33[Online documentation](https://www.insertcart.com/insertcart-custom-checkbox-ultimate-for-woocommerce-documentation/) | [Support forum](https://wordpress.org/support/plugin/custom-checkbox-ultimate-for-woocommerce/) | [Contact InsertCart](https://www.insertcart.com/contact-us/)
    4034
    41 This plugin requires WooCommerce to be installed and activated.
    42 If WooCommerce is not active, you will receive an admin notice prompting you to activate it.
     35= How pricing works =
    4336
    44 ==Quick Start==
     37Services are charged per product unit, in the store currency. A product priced at 20 with a service priced at 5 has a unit price of 25. Quantity 2 has a subtotal of 50 before taxes, discounts, and shipping. WooCommerce applies product taxes and coupons to the combined product price. Services are not separate cart fees.
    4538
    46 * Adding Custom Services to a Product
     39= Compatibility limits =
    4740
    48 Navigate to Products:
     41Service selection uses the standard WooCommerce product add-to-cart form. This release does not provide selection controls for the block Add to Cart form, direct Store API additions, shop-loop quick add, or third-party custom product forms. Variable products share the parent product's services. Test grouped, external, bundled, and subscription products separately.
    4942
    50 *   In your WordPress dashboard, go to Products > All Products.
    51 *   Click on the product you want to add additional services to, or create a new product.
    52 *   Locate the Custom Services Meta Box:
    53 *   Scroll down to find the Custom Services – IC Woo Ultimate meta box.
     43= Privacy =
    5444
    55 ==Configure the Services==
     45No analytics or service data is sent to an external server. Product settings are stored in WordPress product metadata. Selections are stored in WooCommerce carts and order item metadata. Deactivation preserves settings and historical orders.
    5646
    57 **Custom Services Title:** Enter a title that will appear above the checkbox options on the product page (e.g., "Add-Ons", "Extra Services").
    58 **Service Fields:** By default, five service fields are available. For each service, provide:
    59 **Service Name:** The label for the checkbox (e.g., "Gift Wrapping").
    60 **Service Price:** The additional cost associated with the service.
    61 **Service Description: (Optional)** A brief description that appears as a tooltip for more information.
    62 **Add More Fields:** Click the **+Add More Fields** button to add additional service options beyond the default five.
    63 **Save the Product:** Once configured, click Update or Publish to save your changes.
     47== Installation ==
    6448
    65 Displaying Custom Services on the Frontend
    66 On the product page, customers will see the Custom Services Title followed by a list of checkbox options with their respective prices.
    67 Selecting a checkbox will automatically update the product price in the cart to include the additional service cost.
    68 Service descriptions are available as tooltips for more detailed information.
    69 Managing Orders
    70 Selected additional services and their costs are displayed in the cart, checkout, and order details, ensuring transparency and accurate billing.
     491. Install and activate WooCommerce.
     502. Install Custom Checkbox Ultimate for WooCommerce from Plugins > Add New, or upload the plugin ZIP.
     513. Activate the plugin and open IC Woo Ultimate for the included guide.
     524. Edit a product and locate Custom Services - IC Woo Ultimate. If hidden, enable it in Screen Options.
     535. Configure Checkbox Type and/or Radio Type, then update the product.
     546. Test the product page, cart, checkout, and a sample order before using it on your live store.
    7155
     56== Frequently Asked Questions ==
    7257
     58= Where are the service settings? =
     59Edit a product in the classic product editor and use Custom Services - IC Woo Ultimate. There is no global WooCommerce settings tab for configuring service rows.
     60
     61= Can I add more than five services? =
     62Yes. Use Add More Fields in either type. Blank service names are ignored. Clear the name and update the product to remove a service.
     63
     64= Are services required or preselected? =
     65No. Checkboxes start unchecked. Radio services include a no-service choice.
     66
     67= Can services be free? =
     68Yes. Enter 0 as the service price. Negative prices are not supported.
     69
     70= Do services work with variations? =
     71Yes. Configure services on the parent product; they are added to the chosen variation's unit price. Per-variation service definitions are not provided.
     72
     73= Why is the product page price unchanged? =
     74This release shows each service price beside its option. The product's combined price is calculated after adding it to the cart.
     75
     76= Why are selections missing? =
     77Check that your theme renders the standard add-to-cart form and refresh the product page to obtain a fresh form nonce. Exclude cart and checkout from page caching. Shop-loop quick add and block product forms do not collect selections.
     78
     79= What happens when a service price changes? =
     80New additions use the saved product price. Existing 1.4 cart selections retain their recorded service amount; remove and re-add the product to use new settings. Legacy 1.3 cart entries are resolved against current settings. Historical orders retain their stored line item details.
    7381
    7482== Screenshots ==
    7583
    76 1. Checkbox you can add fields and services name with price and description.
    77 2. Radio button you can select only one of theme.
    78 3. Pricing will display on WooCommerce Product Page.
    79 4. Check screen option in product page to hide and show plugin settings
    80 
    81 
    82 ===Frequently Asked Questions===
    83 
    84 = Does this plugin work with all WooCommerce themes? =
    85 Yes, the plugin is designed to be compatible with most WooCommerce-compatible themes. However, styling may vary based on your theme. You can use custom CSS to adjust the appearance as needed.
    86 
    87 = Can I add more than five custom service fields? =
    88 Absolutely! While the plugin initializes with five fields by default, you can add as many additional service fields as required using the + Add More Fields button in the meta box.
    89 
    90 = Is it possible to set different prices for different user roles? =
    91 By default, the plugin does not support role-based pricing. However, with custom development or additional extensions, this functionality can be achieved.
    92 
    93 = How are the additional service costs handled during checkout? =
    94 Selected additional services are added to the cart item data, ensuring that the total price reflects the added costs. These details are visible in the cart, checkout, and order summaries.
    95 
    96 = What happens if WooCommerce is deactivated? =
    97 If WooCommerce is not active, the plugin will display an admin notice prompting you to activate WooCommerce, as it is a required dependency.
    98 
    99 = More Features =
    100 Working on more features and settings like Shortcodes, Styling options etc & more. [Contact us](https://www.insertcart.com/contact-us/) for more requests.
     841. Checkbox service fields with names, prices, and descriptions, rendered with a sample product.
     852. Radio service fields with alternative choices and an additional row.
     863. Product service options, descriptions, prices, and the no-service radio choice.
     874. The setup guide included in version 1.4. Screenshots use actual plugin output in a local test harness.
    10188
    10289== Changelog ==
    103 1.3
    104 Initial release of Custom Checkbox Ultimate for WooCommerce.
    105 Features include adding customizable checkbox options, dynamic pricing adjustments, service descriptions with tooltips, and order integration.
    106 Upgrade Notice
    107 1.0
    108 Initial release.
    109 License
    110 This plugin is licensed under the GPL v2 or later. You can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or any later version.
     90
     91= 1.4 =
     92* Security: use saved service prices instead of submitted form prices; validate malformed requests and product editor fields.
     93* Fix checkbox names containing spaces or capital letters and group radio choices correctly.
     94* Keep add-on prices on cart lines and avoid adding charges twice during repeated totals calculations.
     95* Preserve existing services when saved rows contain gaps and support free services.
     96* Improve accessible service descriptions and provide an explicit no-radio-service option.
     97* Add an accurate built-in setup guide, product help link, Plugins list links, and a one-time introduction for store managers.
     98* Guard against missing WooCommerce and limit admin styling to plugin/product screens.
     99* Declare HPOS and Cart/Checkout block compatibility for selections made on standard product forms.
     100* Verified locally with WordPress 7.1.3 and WooCommerce 10.3.5.
     101
     102= 1.3 =
     103* Initial release with checkbox and radio services, service descriptions, and cart/order integration.
    111104
    112105== Upgrade Notice ==
    113106
    114 No major requirements
     107= 1.4 =
     108Security and reliability update. Service prices now come from saved product settings. Includes a setup guide and improved cart/order handling. Test on staging before updating your live store.
Note: See TracChangeset for help on using the changeset viewer.