Changeset 3741869
- Timestamp:
- 10/12/2026 02:32:26 PM (4 weeks ago)
- Location:
- giftvo
- Files:
-
- 4 edited
- 1 copied
-
tags/1.2.0 (copied) (copied from giftvo/trunk )
-
tags/1.2.0/giftvo.php (modified) (15 diffs)
-
tags/1.2.0/readme.txt (modified) (6 diffs)
-
trunk/giftvo.php (modified) (15 diffs)
-
trunk/readme.txt (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
giftvo/tags/1.2.0/giftvo.php
r3706274 r3741869 2 2 /** 3 3 * Plugin Name: Giftvo - Tiered Gifts for WooCommerce 4 * Description: Automatically adds free gift product(s) to the cart when the cart total reaches a threshold you configure. Unlimited tiers, "let the customer pick" gifts, colour/variation choice, a live progress bar with an optional free-shipping goal, and a real 0.00 line item that is safe for every payment gateway.5 * Version: 1. 1.04 * Description: Automatically adds free gift product(s) to the cart when the cart total reaches a threshold you configure. Unlimited tiers, "let the customer pick" gifts, colour/variation choice, a live progress bar with optional free-shipping and discount milestones, and a real 0.00 line item that is safe for every payment gateway. 5 * Version: 1.2.0 6 6 * Author: Serdar Keskin 7 7 * Author URI: https://zaundiy.de … … 26 26 const FLAG = 'wctfg_gift'; // cart item marker (= threshold value) 27 27 const EPS = 0.00001; 28 const VERSION = '1.2.0'; 29 // Milestones closer than this share of the bar's top amount are drawn as one marker. 30 const NEAR = 0.03; 28 31 29 32 private static $settings_cache = null; 30 33 31 34 public function __construct() { 32 // i18n — bundled /languages translations (loads on init per WP 6.7+ guidance).33 // A site-wide override in wp-content/languages/plugins/ still takes precedence.34 add_action('init', [$this, 'load_textdomain']);35 36 35 // Admin 37 36 add_action('admin_menu', [$this, 'admin_menu']); … … 60 59 add_action('woocommerce_before_cart', [$this, 'progress_on_cart']); 61 60 add_action('woocommerce_widget_shopping_cart_before_buttons', [$this, 'progress_on_minicart']); 62 }63 64 public function load_textdomain() {65 load_plugin_textdomain('giftvo', false, dirname(plugin_basename(__FILE__)) . '/languages');66 61 } 67 62 … … 85 80 'free_ship_amount' => '', 86 81 'discount_enable' => 0, 87 'discount_amount' => '', 88 'discount_percent' => '', 82 'discount_tiers' => [], // [['amount' => float, 'label' => string], …] 83 'discount_amount' => '', // legacy (≤ 1.1.0): single discount milestone 84 'discount_percent' => '', // legacy (≤ 1.1.0) 85 'bar_scale' => 'linear', // linear|steps 89 86 ]; 90 87 $s = get_option(self::SETTINGS, []); 91 self::$settings_cache = array_merge($defaults, is_array($s) ? $s : []); 88 $s = array_merge($defaults, is_array($s) ? $s : []); 89 90 // Upgrade path: a single 1.1.0 discount milestone becomes the first row of the list. 91 $s['discount_tiers'] = self::clean_discount_tiers($s['discount_tiers']); 92 if (!$s['discount_tiers'] && (float) $s['discount_amount'] > 0) { 93 $s['discount_tiers'] = self::clean_discount_tiers([['amount' => $s['discount_amount'], 'label' => $s['discount_percent']]]); 94 } 95 $s['bar_scale'] = ($s['bar_scale'] === 'steps') ? 'steps' : 'linear'; 96 97 self::$settings_cache = $s; 92 98 return self::$settings_cache; 99 } 100 101 /** Sanitise discount milestones: [['amount' => float > 0, 'label' => string], …] sorted by amount. */ 102 private static function clean_discount_tiers($rows) { 103 $out = []; 104 foreach ((array) $rows as $r) { 105 if (!is_array($r)) continue; 106 $amount = isset($r['amount']) ? (float) str_replace(',', '.', (string) $r['amount']) : 0; 107 if ($amount <= 0) continue; 108 $out[] = ['amount' => $amount, 'label' => isset($r['label']) ? sanitize_text_field((string) $r['label']) : '']; 109 } 110 usort($out, function ($a, $b) { return $a['amount'] <=> $b['amount']; }); 111 return $out; 93 112 } 94 113 … … 162 181 wp_enqueue_style('select2'); 163 182 164 wp_register_script('wctfg-admin', '', ['jquery', 'wc-enhanced-select'], '1.1.0', true);183 wp_register_script('wctfg-admin', '', ['jquery', 'wc-enhanced-select'], self::VERSION, true); 165 184 wp_enqueue_script('wctfg-admin'); 166 185 wp_localize_script('wctfg-admin', 'WctfgAdmin', [ … … 217 236 if ($('.wctfg-row').length > 1) $(this).closest('tr').remove(); 218 237 else { $(this).closest('tr').find('input').val(''); $(this).closest('tr').find('select.wc-product-search').val(null).trigger('change'); } 238 }); 239 $('#wctfg-disc-add').on('click', function(){ 240 var $row = $('#wctfg-disc-table .wctfg-disc-row:first').clone(); 241 $row.find('input').val(''); 242 $('#wctfg-disc-table tbody').append($row); 243 }); 244 $('#wctfg-disc-table').on('click', '.wctfg-disc-del', function(){ 245 if ($('#wctfg-disc-table .wctfg-disc-row').length > 1) $(this).closest('tr').remove(); 246 else $(this).closest('tr').find('input').val(''); 219 247 }); 220 248 }); … … 338 366 </tr> 339 367 <tr> 340 <th scope="row"><?php echo esc_html__('Discount amount', 'giftvo'); ?></th>368 <th scope="row"><?php echo esc_html__('Discount milestones', 'giftvo'); ?></th> 341 369 <td> 342 <input type="number" step="0.01" min="0" name="discount_amount" value="<?php echo esc_attr($s['discount_amount']); ?>" style="width:150px"> 343 <p class="description"><?php echo esc_html__('Cart amount that unlocks the discount. Uses the same threshold basis as gifts.', 'giftvo'); ?></p> 370 <?php $disc_rows = $s['discount_tiers'] ? $s['discount_tiers'] : [['amount' => '', 'label' => '']]; ?> 371 <table class="widefat striped" id="wctfg-disc-table" style="max-width:460px"> 372 <thead> 373 <tr> 374 <th><?php echo esc_html__('Cart amount', 'giftvo'); ?></th> 375 <th><?php echo esc_html__('Label', 'giftvo'); ?></th> 376 <th style="width:50px"></th> 377 </tr> 378 </thead> 379 <tbody> 380 <?php foreach ($disc_rows as $d): ?> 381 <tr class="wctfg-disc-row"> 382 <td><input type="number" step="0.01" min="0" name="disc_amount[]" value="<?php echo esc_attr($d['amount']); ?>" style="width:150px"></td> 383 <td><input type="text" name="disc_label[]" value="<?php echo esc_attr($d['label']); ?>" placeholder="5%" style="width:120px"></td> 384 <td><button type="button" class="button wctfg-disc-del" title="<?php echo esc_attr__('Remove row', 'giftvo'); ?>">✕</button></td> 385 </tr> 386 <?php endforeach; ?> 387 </tbody> 388 </table> 389 <p><button type="button" class="button" id="wctfg-disc-add">+ <?php echo esc_html__('Add discount milestone', 'giftvo'); ?></button></p> 390 <p class="description" style="max-width:640px"><?php echo esc_html__('One marker per row, e.g. 1000 → “5%”, 2000 → “6%”. The label is shown on the marker and in the message. Uses the same threshold basis as gifts. Giftvo only displays these milestones — set up the discount itself in your pricing or discount plugin.', 'giftvo'); ?></p> 344 391 </td> 345 392 </tr> 346 393 <tr> 347 <th scope="row"><?php echo esc_html__(' Discount label', 'giftvo'); ?></th>394 <th scope="row"><?php echo esc_html__('Progress bar scale', 'giftvo'); ?></th> 348 395 <td> 349 < input type="text" name="discount_percent" value="<?php echo esc_attr($s['discount_percent']); ?>" placeholder="5%" style="width:120px">350 < p class="description"><?php echo esc_html__('Shown on the marker and in the message, e.g. “5%”.', 'giftvo'); ?></p>396 <label><input type="radio" name="bar_scale" value="linear" <?php checked($s['bar_scale'], 'linear'); ?>> <?php echo esc_html__('Proportional — each marker sits at its amount', 'giftvo'); ?></label><br> 397 <label><input type="radio" name="bar_scale" value="steps" <?php checked($s['bar_scale'], 'steps'); ?>> <?php echo esc_html__('Evenly spaced — every milestone gets the same room (best for many milestones)', 'giftvo'); ?></label> 351 398 </td> 352 399 </tr> … … 414 461 usort($tiers, function ($a, $b) { return $a['threshold'] <=> $b['threshold']; }); 415 462 update_option(self::OPTION, $tiers, false); 463 464 // Discount milestones: two parallel lists, one entry per table row. 465 $d_amounts = isset($_POST['disc_amount']) ? array_map('sanitize_text_field', wp_unslash((array) $_POST['disc_amount'])) : []; 466 $d_labels = isset($_POST['disc_label']) ? array_values(array_map('sanitize_text_field', wp_unslash((array) $_POST['disc_label']))) : []; 467 $disc = []; 468 foreach (array_values($d_amounts) as $i => $amount) { 469 $disc[] = ['amount' => $amount, 'label' => $d_labels[$i] ?? '']; 470 } 416 471 417 472 // Settings … … 434 489 'free_ship_amount' => isset($_POST['free_ship_amount']) ? max(0, (float) str_replace(',', '.', sanitize_text_field(wp_unslash($_POST['free_ship_amount'])))) : 0, 435 490 'discount_enable' => !empty($_POST['discount_enable']) ? 1 : 0, 436 'discount_amount' => isset($_POST['discount_amount']) ? max(0, (float) str_replace(',', '.', sanitize_text_field(wp_unslash($_POST['discount_amount'])))) : 0, 437 'discount_percent' => isset($_POST['discount_percent']) ? sanitize_text_field(wp_unslash($_POST['discount_percent'])) : '', 491 'discount_tiers' => self::clean_discount_tiers($disc), 492 'discount_amount' => '', // legacy single milestone now lives in discount_tiers 493 'discount_percent' => '', 494 'bar_scale' => (isset($_POST['bar_scale']) && sanitize_key(wp_unslash($_POST['bar_scale'])) === 'steps') ? 'steps' : 'linear', 438 495 ]; 439 496 update_option(self::SETTINGS, $settings, false); … … 473 530 // Re-run through the same whitelist as save. 474 531 $in = $data['settings']; 532 // Accept the 1.2.0 list as well as a 1.1.0 export with a single discount milestone. 533 $disc = self::clean_discount_tiers($in['discount_tiers'] ?? []); 534 if (!$disc && isset($in['discount_amount']) && (float) str_replace(',', '.', (string) $in['discount_amount']) > 0) { 535 $disc = self::clean_discount_tiers([['amount' => $in['discount_amount'], 'label' => $in['discount_percent'] ?? '']]); 536 } 475 537 $accent = isset($in['accent']) ? trim((string) $in['accent']) : ''; 476 538 update_option(self::SETTINGS, [ … … 489 551 'free_ship_amount' => isset($in['free_ship_amount']) ? max(0, (float) str_replace(',', '.', (string) $in['free_ship_amount'])) : 0, 490 552 'discount_enable' => !empty($in['discount_enable']) ? 1 : 0, 491 'discount_amount' => isset($in['discount_amount']) ? max(0, (float) str_replace(',', '.', (string) $in['discount_amount'])) : 0, 492 'discount_percent' => isset($in['discount_percent']) ? sanitize_text_field((string) $in['discount_percent']) : '', 553 'discount_tiers' => $disc, 554 'discount_amount' => '', 555 'discount_percent' => '', 556 'bar_scale' => (($in['bar_scale'] ?? '') === 'steps') ? 'steps' : 'linear', 493 557 ], false); 494 558 } … … 1020 1084 $accent = $this->accent(); 1021 1085 1022 // Two independent, separately-toggled milestone sources feed one bar: 1023 // gift tiers (🎁, gated by "progress_bar") and a free-shipping goal (🚚, 1024 // gated by "free_ship_enable"). Either source alone renders the bar. 1086 // Three separately-toggled milestone sources feed one bar: gift tiers (🎁, 1087 // gated by "progress_bar"), a free-shipping goal (🚚, "free_ship_enable") and 1088 // any number of discount milestones (%, "discount_enable"). Any one alone 1089 // renders the bar. 1025 1090 $show_gifts = !empty($s['progress_bar']); 1026 1091 $ship_amt = (float) $s['free_ship_amount']; 1027 1092 $show_ship = !empty($s['free_ship_enable']) && $ship_amt > 0; 1028 $disc_amt = (float) $s['discount_amount']; 1029 $disc_txt = trim((string) $s['discount_percent']); 1030 if ($disc_txt === '') { $disc_txt = '%'; } 1031 $show_disc = !empty($s['discount_enable']) && $disc_amt > 0; 1093 $show_disc = !empty($s['discount_enable']) && !empty($s['discount_tiers']); 1032 1094 1033 1095 $milestones = []; 1034 1096 if ($show_gifts) { 1035 1097 foreach ($this->normalized_tiers() as $t) { 1036 $milestones[] = ['threshold' => (float) $t['threshold'], 'type' => 'gift' ];1098 $milestones[] = ['threshold' => (float) $t['threshold'], 'type' => 'gift', 'label' => '']; 1037 1099 } 1038 1100 } 1039 1101 if ($show_ship) { 1040 $milestones[] = ['threshold' => $ship_amt, 'type' => 'ship' ];1102 $milestones[] = ['threshold' => $ship_amt, 'type' => 'ship', 'label' => '']; 1041 1103 } 1042 1104 if ($show_disc) { 1043 $milestones[] = ['threshold' => $disc_amt, 'type' => 'disc']; 1105 foreach ($s['discount_tiers'] as $d) { 1106 $milestones[] = ['threshold' => (float) $d['amount'], 'type' => 'disc', 'label' => ($d['label'] !== '' ? $d['label'] : '%')]; 1107 } 1044 1108 } 1045 1109 if (empty($milestones)) return ''; … … 1047 1111 1048 1112 $subtotal = $this->base_subtotal(WC()->cart); 1049 1050 $max = 0.0; 1051 foreach ($milestones as $m) $max = max($max, $m['threshold']); 1052 $pct = $max > 0 ? min(100, ($subtotal / $max) * 100) : 0; 1053 1054 // Next unreached milestone (smallest threshold still above the subtotal). 1113 $max = (float) $milestones[count($milestones) - 1]['threshold']; 1114 1115 // Group milestones that are (nearly) at the same amount so their icons never 1116 // overlap, e.g. free shipping at 999 and a discount at 1000. A group is drawn 1117 // at its highest amount, so a marker never promises a reward too early. 1118 $groups = []; 1119 foreach ($milestones as $m) { 1120 $last = count($groups) - 1; 1121 if ($last >= 0 && ($m['threshold'] - $groups[$last]['min']) <= $max * self::NEAR) { 1122 $groups[$last]['items'][] = $m; 1123 $groups[$last]['max'] = $m['threshold']; 1124 } else { 1125 $groups[] = ['min' => $m['threshold'], 'max' => $m['threshold'], 'items' => [$m]]; 1126 } 1127 } 1128 $n = count($groups); 1129 $steps = ($s['bar_scale'] === 'steps'); 1130 1131 // Marker position and bar fill. "linear": proportional to the amount. 1132 // "steps": every group gets the same room; the fill moves proportionally 1133 // inside the step the customer is currently in. 1134 foreach ($groups as $i => $g) { 1135 $groups[$i]['left'] = $steps ? (($i + 1) / $n) * 100 : ($max > 0 ? ($g['max'] / $max) * 100 : 0); 1136 } 1137 if ($steps) { 1138 $pct = 100; 1139 $prev = 0.0; 1140 foreach ($groups as $i => $g) { 1141 if ($subtotal + self::EPS < $g['max']) { 1142 $span = max(self::EPS, $g['max'] - $prev); 1143 $pct = (($i + max(0, min(1, ($subtotal - $prev) / $span))) / $n) * 100; 1144 break; 1145 } 1146 $prev = $g['max']; 1147 } 1148 } else { 1149 $pct = $max > 0 ? min(100, ($subtotal / $max) * 100) : 0; 1150 } 1151 1152 // Message: the next group not yet fully reached and everything it still unlocks. 1055 1153 $next = null; 1056 foreach ($ milestones as $m) { if ($m['threshold'] > $subtotal + self::EPS) { $next = $m; break; } }1154 foreach ($groups as $g) { if ($g['max'] > $subtotal + self::EPS) { $next = $g; break; } } 1057 1155 1058 1156 if ($next) { 1059 $remaining = $next[' threshold'] - $subtotal;1157 $remaining = $next['max'] - $subtotal; 1060 1158 $amount_html = '<b style="color:' . esc_attr($accent) . '">' . wp_kses_post(wc_price($remaining)) . '</b>'; 1061 $next_gift = false; $next_ship = false; $next_disc = false; 1062 foreach ($milestones as $mm) { 1063 if (abs($mm['threshold'] - $next['threshold']) < self::EPS) { 1064 if ($mm['type'] === 'ship') { $next_ship = true; } 1065 elseif ($mm['type'] === 'disc') { $next_disc = true; } 1066 else { $next_gift = true; } 1067 } 1068 } 1069 if ($next_gift && $next_ship) { 1159 $next_gift = false; $next_ship = false; $disc_txt = ''; 1160 foreach ($next['items'] as $mm) { 1161 if ($mm['threshold'] <= $subtotal + self::EPS) continue; // already unlocked 1162 if ($mm['type'] === 'ship') { $next_ship = true; } 1163 elseif ($mm['type'] === 'disc') { $disc_txt = $mm['label']; } // highest discount in the group wins 1164 else { $next_gift = true; } 1165 } 1166 $disc_html = '<b style="color:' . esc_attr($accent) . '">' . esc_html($disc_txt) . '</b>'; 1167 if ($next_gift && $next_ship && $disc_txt !== '') { 1168 /* translators: %1$s = formatted price amount, %2$s = discount label such as "5%" */ 1169 $msg = sprintf(__('Spend %1$s more for a free gift, free shipping + %2$s off!', 'giftvo'), $amount_html, $disc_html); 1170 } elseif ($next_gift && $next_ship) { 1070 1171 /* translators: %s = formatted price amount */ 1071 1172 $msg = sprintf(__('Spend %s more for a free gift + free shipping!', 'giftvo'), $amount_html); 1173 } elseif ($next_gift && $disc_txt !== '') { 1174 /* translators: %1$s = formatted price amount, %2$s = discount label such as "5%" */ 1175 $msg = sprintf(__('Spend %1$s more for a free gift + %2$s off!', 'giftvo'), $amount_html, $disc_html); 1176 } elseif ($next_ship && $disc_txt !== '') { 1177 /* translators: %1$s = formatted price amount, %2$s = discount label such as "5%" */ 1178 $msg = sprintf(__('Spend %1$s more for free shipping + %2$s off!', 'giftvo'), $amount_html, $disc_html); 1072 1179 } elseif ($next_ship) { 1073 1180 /* translators: %s = formatted price amount */ 1074 1181 $msg = sprintf(__('Spend %s more for free shipping!', 'giftvo'), $amount_html); 1075 } elseif ($ next_disc && !$next_gift) {1182 } elseif ($disc_txt !== '') { 1076 1183 /* translators: %1$s = amount, %2$s = discount label such as "5%" */ 1077 $msg = sprintf(__('Spend %1$s more for %2$s off!', 'giftvo'), $amount_html, '<b style="color:' . esc_attr($accent) . '">' . esc_html($disc_txt) . '</b>');1184 $msg = sprintf(__('Spend %1$s more for %2$s off!', 'giftvo'), $amount_html, $disc_html); 1078 1185 } else { 1079 1186 /* translators: %s = formatted price amount */ … … 1094 1201 } 1095 1202 1096 // One marker per milestone. Markers live in a layer that spans exactly the 1097 // track width; the bar is inset by half an icon (9px) so the first/last 1098 // marker sits flush with the track ends without the icon overflowing. 1099 // Group milestones that share a threshold so their icons never overlap 1100 // (e.g. a gift tier and the free-shipping goal both set to the same amount). 1101 $groups = []; 1102 foreach ($milestones as $m) { 1103 $key = number_format($m['threshold'], 2, '.', ''); 1104 if (!isset($groups[$key])) $groups[$key] = ['threshold' => $m['threshold'], 'gift' => false, 'ship' => false, 'disc' => false]; 1105 $groups[$key][$m['type']] = true; 1106 } 1107 1203 // One marker per group. Markers live in a layer that spans exactly the track 1204 // width; the bar is inset by half an icon (9px) so the first/last marker sits 1205 // flush with the track ends without the icon overflowing. 1108 1206 $show_amt = !empty($s['show_amounts']); 1109 1207 $markers = ''; 1110 1208 foreach ($groups as $g) { 1111 $left = $max > 0 ? min(100, max(0, ($g['threshold'] / $max) * 100)) : 0; 1112 $reached = ($subtotal + self::EPS) >= $g['threshold']; 1113 $ico = $reached ? 'opacity:1' : 'filter:grayscale(1);opacity:.45'; 1114 // Connector colour: a reached milestone's line turns gold so it pops 1115 // against the green progress bar; unreached stays grey. Applies to plain 1209 $left = min(100, max(0, $g['left'])); 1210 $reached = ($subtotal + self::EPS) >= $g['max']; 1211 // Connector colour: a fully reached group's line turns gold so it pops 1212 // against the green progress bar; otherwise it stays grey. Applies to plain 1116 1213 // single stems and the joined U-bracket alike. 1117 1214 $conn = $reached ? '#e0a516' : '#c7d1cb'; 1118 1215 $amt_html = ''; 1119 1216 if ($show_amt) { 1120 $amount = wp_strip_all_tags(wc_price($g[' threshold'], ['decimals' => 0]));1217 $amount = wp_strip_all_tags(wc_price($g['max'], ['decimals' => 0])); 1121 1218 $acol = $reached ? $accent : '#9aa39c'; 1122 1219 $amt_html = '<span style="font-size:10.5px;line-height:1;white-space:nowrap;margin-bottom:2px;font-weight:600;color:' . esc_attr($acol) . '">' . esc_html($amount) . '</span>'; 1123 1220 } 1124 // Icon(s) for whatever this threshold unlocks; 2+ get a U-shaped bracket. 1221 // Icon(s) for whatever this group unlocks, each lit once its own amount is 1222 // reached; 2+ icons get a U-shaped bracket. 1125 1223 $spans = []; 1126 if ($g['gift']) { $spans[] = '<span style="font-size:13px;line-height:1;' . $ico . '">🎁</span>'; } 1127 if ($g['ship']) { $spans[] = '<span style="font-size:13px;line-height:1;' . $ico . '">🚚</span>'; } 1128 if ($g['disc']) { $spans[] = '<span style="display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:16px;padding:0 3px;border:1.5px solid ' . esc_attr($conn) . ';border-radius:8px;color:' . esc_attr($conn) . ';font-size:9px;font-weight:800;line-height:1">' . esc_html($disc_txt) . '</span>'; } 1224 foreach ($g['items'] as $mm) { 1225 $on = ($subtotal + self::EPS) >= $mm['threshold']; 1226 $ico = $on ? 'opacity:1' : 'filter:grayscale(1);opacity:.45'; 1227 if ($mm['type'] === 'gift') { 1228 $spans[] = '<span style="font-size:13px;line-height:1;' . $ico . '">🎁</span>'; 1229 } elseif ($mm['type'] === 'ship') { 1230 $spans[] = '<span style="font-size:13px;line-height:1;' . $ico . '">🚚</span>'; 1231 } else { 1232 $dcol = $on ? '#e0a516' : '#c7d1cb'; 1233 $spans[] = '<span style="display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:16px;padding:0 3px;border:1.5px solid ' . esc_attr($dcol) . ';border-radius:8px;color:' . esc_attr($dcol) . ';font-size:9px;font-weight:800;line-height:1;white-space:nowrap">' . esc_html($mm['label']) . '</span>'; 1234 } 1235 } 1129 1236 $both = count($spans) > 1; 1130 1237 if ($both) { … … 1140 1247 $icons = $spans[0]; 1141 1248 } 1142 $markers .= '<div class="wctfg-mark" title="' . esc_attr(wp_strip_all_tags(wc_price($g[' threshold']))) . '"'1249 $markers .= '<div class="wctfg-mark" title="' . esc_attr(wp_strip_all_tags(wc_price($g['max']))) . '"' 1143 1250 . ' style="position:absolute;top:0;bottom:0;left:' . esc_attr(round($left, 2)) . '%;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;z-index:2">' 1144 1251 . $amt_html -
giftvo/tags/1.2.0/readme.txt
r3706274 r3741869 7 7 WC requires at least: 6.0 8 8 WC tested up to: 9.3 9 Stable tag: 1. 1.09 Stable tag: 1.2.0 10 10 License: GPLv2 or later 11 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 25 25 * **Cumulative or highest‑only** — stack the gifts of every reached tier, or give just the top one. 26 26 * **Live progress bar** — “Spend €X more to unlock a free gift!” with a gift marker for every threshold, on the cart, mini‑cart and via the `[gift_progress]` shortcode. 27 * **Free‑shipping milestone** — add an optional 🚚 goal to the progress bar so shoppers see how close they are to free shipping (uses the same threshold basis as gifts). 28 * **Discount milestone** — add an optional % goal marker to the progress bar. 27 * **Free‑shipping goal** — optionally add a 🚚 milestone to the same bar (“Spend €X more for free shipping!”). It’s a plain amount you set, independent of any shipping plugin, and can be switched off in Settings. 28 * **Discount milestones** — add as many % milestones as you like (e.g. 5% from €1,000, 6% from €2,000 …) so shoppers see every step of a tiered discount on the same bar (“Spend €X more for 6% off!”). Giftvo only shows the milestones; the discount itself comes from your pricing or discount plugin. 29 * **Proportional or evenly spaced bar** — place markers at their amount, or give every milestone the same room so many steps stay readable on phones. Milestones at almost the same amount (e.g. free shipping at €999 and a discount at €1,000) share one marker. 29 30 * **Crossed‑out value** — show the gift’s normal price struck through next to “Free”. 30 31 * **Celebration** — an optional confetti burst when a new gift is unlocked. … … 32 33 * **Gateway safe** — the gift is a genuine 0.00 line item; the basket total always matches the amount charged. 33 34 * **White‑label ready** — set the menu title, badge text, “Free” label and accent colour; import/export the whole configuration as JSON. 34 * **Translation ready** — ships with English, German and Turkish; text domain `giftvo`.35 * **Translation ready** — text domain `giftvo`; translations are delivered through translate.wordpress.org. 35 36 * **HPOS compatible** — declares compatibility with WooCommerce High‑Performance Order Storage. 36 37 … … 55 56 Yes. Set a tier’s mode to “Customer picks one”, add several products, and the shopper chooses one in the cart. Variable gifts also let them pick the colour/variation. 56 57 58 = Does Giftvo apply the discount milestones? = 59 No. Discount milestones are a display feature: they show shoppers how far they are from each discount step. Create the discount itself with your pricing or discount plugin and enter the same amounts and labels in Giftvo. 60 61 = How many tiers and milestones can I add? = 62 As many as you need — gift tiers and discount milestones are both unlimited. With many milestones, choose the “Evenly spaced” bar scale so the markers stay readable on small screens. 63 57 64 = Does the threshold include tax? = 58 65 Your choice. Under Settings you can base it on the subtotal incl. or excl. tax, before or after discounts. 66 67 == Upgrade Notice == 68 69 = 1.2.0 = 70 Unlimited discount milestones and an evenly spaced progress bar option. Existing settings are kept; a single discount milestone is moved into the new list automatically. 59 71 60 72 == Screenshots == … … 66 78 == Changelog == 67 79 80 = 1.2.0 = 81 * New: unlimited discount milestones on the progress bar — add one row per discount step (amount + label, e.g. 1000 → “5%”). A 1.1.0 single discount milestone is moved into the list automatically. 82 * New: “Evenly spaced” progress bar scale — every milestone gets the same room, so bars with many steps stay readable on phones. “Proportional” stays the default. 83 * Milestones at almost the same amount are drawn as one combined marker, and the message names everything the next step unlocks (e.g. “Spend €21 more for free shipping + 5% off!”). 84 * Translations now load from translate.wordpress.org (no bundled language files). 85 68 86 = 1.1.0 = 69 * New: optional free‑shipping milestone (🚚) on the progress bar, using the same threshold basis as gifts.70 * New: optional discount milestone (%) on the progress bar.71 * Minor UI and stability improvements.87 * New: optional free‑shipping goal on the progress bar — adds a 🚚 milestone (“Spend €X more for free shipping!”) alongside the gift markers. Set the amount and toggle it in Settings; it’s independent of any shipping plugin. 88 * Markers that share the same threshold (e.g. a gift tier and the free‑shipping goal set to the same amount) are merged into a single combined marker so the icons never overlap. 89 * Updated English, German and Turkish translations. 72 90 73 91 = 1.0.0 = … … 81 99 * White‑label options (menu title, badge, label, accent), JSON import/export, uninstall cleanup. 82 100 * English, German and Turkish translations. HPOS compatible. 83 84 == Upgrade Notice ==85 86 = 1.1.0 =87 Adds optional free‑shipping (🚚) and discount (%) milestones to the progress bar. -
giftvo/trunk/giftvo.php
r3706274 r3741869 2 2 /** 3 3 * Plugin Name: Giftvo - Tiered Gifts for WooCommerce 4 * Description: Automatically adds free gift product(s) to the cart when the cart total reaches a threshold you configure. Unlimited tiers, "let the customer pick" gifts, colour/variation choice, a live progress bar with an optional free-shipping goal, and a real 0.00 line item that is safe for every payment gateway.5 * Version: 1. 1.04 * Description: Automatically adds free gift product(s) to the cart when the cart total reaches a threshold you configure. Unlimited tiers, "let the customer pick" gifts, colour/variation choice, a live progress bar with optional free-shipping and discount milestones, and a real 0.00 line item that is safe for every payment gateway. 5 * Version: 1.2.0 6 6 * Author: Serdar Keskin 7 7 * Author URI: https://zaundiy.de … … 26 26 const FLAG = 'wctfg_gift'; // cart item marker (= threshold value) 27 27 const EPS = 0.00001; 28 const VERSION = '1.2.0'; 29 // Milestones closer than this share of the bar's top amount are drawn as one marker. 30 const NEAR = 0.03; 28 31 29 32 private static $settings_cache = null; 30 33 31 34 public function __construct() { 32 // i18n — bundled /languages translations (loads on init per WP 6.7+ guidance).33 // A site-wide override in wp-content/languages/plugins/ still takes precedence.34 add_action('init', [$this, 'load_textdomain']);35 36 35 // Admin 37 36 add_action('admin_menu', [$this, 'admin_menu']); … … 60 59 add_action('woocommerce_before_cart', [$this, 'progress_on_cart']); 61 60 add_action('woocommerce_widget_shopping_cart_before_buttons', [$this, 'progress_on_minicart']); 62 }63 64 public function load_textdomain() {65 load_plugin_textdomain('giftvo', false, dirname(plugin_basename(__FILE__)) . '/languages');66 61 } 67 62 … … 85 80 'free_ship_amount' => '', 86 81 'discount_enable' => 0, 87 'discount_amount' => '', 88 'discount_percent' => '', 82 'discount_tiers' => [], // [['amount' => float, 'label' => string], …] 83 'discount_amount' => '', // legacy (≤ 1.1.0): single discount milestone 84 'discount_percent' => '', // legacy (≤ 1.1.0) 85 'bar_scale' => 'linear', // linear|steps 89 86 ]; 90 87 $s = get_option(self::SETTINGS, []); 91 self::$settings_cache = array_merge($defaults, is_array($s) ? $s : []); 88 $s = array_merge($defaults, is_array($s) ? $s : []); 89 90 // Upgrade path: a single 1.1.0 discount milestone becomes the first row of the list. 91 $s['discount_tiers'] = self::clean_discount_tiers($s['discount_tiers']); 92 if (!$s['discount_tiers'] && (float) $s['discount_amount'] > 0) { 93 $s['discount_tiers'] = self::clean_discount_tiers([['amount' => $s['discount_amount'], 'label' => $s['discount_percent']]]); 94 } 95 $s['bar_scale'] = ($s['bar_scale'] === 'steps') ? 'steps' : 'linear'; 96 97 self::$settings_cache = $s; 92 98 return self::$settings_cache; 99 } 100 101 /** Sanitise discount milestones: [['amount' => float > 0, 'label' => string], …] sorted by amount. */ 102 private static function clean_discount_tiers($rows) { 103 $out = []; 104 foreach ((array) $rows as $r) { 105 if (!is_array($r)) continue; 106 $amount = isset($r['amount']) ? (float) str_replace(',', '.', (string) $r['amount']) : 0; 107 if ($amount <= 0) continue; 108 $out[] = ['amount' => $amount, 'label' => isset($r['label']) ? sanitize_text_field((string) $r['label']) : '']; 109 } 110 usort($out, function ($a, $b) { return $a['amount'] <=> $b['amount']; }); 111 return $out; 93 112 } 94 113 … … 162 181 wp_enqueue_style('select2'); 163 182 164 wp_register_script('wctfg-admin', '', ['jquery', 'wc-enhanced-select'], '1.1.0', true);183 wp_register_script('wctfg-admin', '', ['jquery', 'wc-enhanced-select'], self::VERSION, true); 165 184 wp_enqueue_script('wctfg-admin'); 166 185 wp_localize_script('wctfg-admin', 'WctfgAdmin', [ … … 217 236 if ($('.wctfg-row').length > 1) $(this).closest('tr').remove(); 218 237 else { $(this).closest('tr').find('input').val(''); $(this).closest('tr').find('select.wc-product-search').val(null).trigger('change'); } 238 }); 239 $('#wctfg-disc-add').on('click', function(){ 240 var $row = $('#wctfg-disc-table .wctfg-disc-row:first').clone(); 241 $row.find('input').val(''); 242 $('#wctfg-disc-table tbody').append($row); 243 }); 244 $('#wctfg-disc-table').on('click', '.wctfg-disc-del', function(){ 245 if ($('#wctfg-disc-table .wctfg-disc-row').length > 1) $(this).closest('tr').remove(); 246 else $(this).closest('tr').find('input').val(''); 219 247 }); 220 248 }); … … 338 366 </tr> 339 367 <tr> 340 <th scope="row"><?php echo esc_html__('Discount amount', 'giftvo'); ?></th>368 <th scope="row"><?php echo esc_html__('Discount milestones', 'giftvo'); ?></th> 341 369 <td> 342 <input type="number" step="0.01" min="0" name="discount_amount" value="<?php echo esc_attr($s['discount_amount']); ?>" style="width:150px"> 343 <p class="description"><?php echo esc_html__('Cart amount that unlocks the discount. Uses the same threshold basis as gifts.', 'giftvo'); ?></p> 370 <?php $disc_rows = $s['discount_tiers'] ? $s['discount_tiers'] : [['amount' => '', 'label' => '']]; ?> 371 <table class="widefat striped" id="wctfg-disc-table" style="max-width:460px"> 372 <thead> 373 <tr> 374 <th><?php echo esc_html__('Cart amount', 'giftvo'); ?></th> 375 <th><?php echo esc_html__('Label', 'giftvo'); ?></th> 376 <th style="width:50px"></th> 377 </tr> 378 </thead> 379 <tbody> 380 <?php foreach ($disc_rows as $d): ?> 381 <tr class="wctfg-disc-row"> 382 <td><input type="number" step="0.01" min="0" name="disc_amount[]" value="<?php echo esc_attr($d['amount']); ?>" style="width:150px"></td> 383 <td><input type="text" name="disc_label[]" value="<?php echo esc_attr($d['label']); ?>" placeholder="5%" style="width:120px"></td> 384 <td><button type="button" class="button wctfg-disc-del" title="<?php echo esc_attr__('Remove row', 'giftvo'); ?>">✕</button></td> 385 </tr> 386 <?php endforeach; ?> 387 </tbody> 388 </table> 389 <p><button type="button" class="button" id="wctfg-disc-add">+ <?php echo esc_html__('Add discount milestone', 'giftvo'); ?></button></p> 390 <p class="description" style="max-width:640px"><?php echo esc_html__('One marker per row, e.g. 1000 → “5%”, 2000 → “6%”. The label is shown on the marker and in the message. Uses the same threshold basis as gifts. Giftvo only displays these milestones — set up the discount itself in your pricing or discount plugin.', 'giftvo'); ?></p> 344 391 </td> 345 392 </tr> 346 393 <tr> 347 <th scope="row"><?php echo esc_html__(' Discount label', 'giftvo'); ?></th>394 <th scope="row"><?php echo esc_html__('Progress bar scale', 'giftvo'); ?></th> 348 395 <td> 349 < input type="text" name="discount_percent" value="<?php echo esc_attr($s['discount_percent']); ?>" placeholder="5%" style="width:120px">350 < p class="description"><?php echo esc_html__('Shown on the marker and in the message, e.g. “5%”.', 'giftvo'); ?></p>396 <label><input type="radio" name="bar_scale" value="linear" <?php checked($s['bar_scale'], 'linear'); ?>> <?php echo esc_html__('Proportional — each marker sits at its amount', 'giftvo'); ?></label><br> 397 <label><input type="radio" name="bar_scale" value="steps" <?php checked($s['bar_scale'], 'steps'); ?>> <?php echo esc_html__('Evenly spaced — every milestone gets the same room (best for many milestones)', 'giftvo'); ?></label> 351 398 </td> 352 399 </tr> … … 414 461 usort($tiers, function ($a, $b) { return $a['threshold'] <=> $b['threshold']; }); 415 462 update_option(self::OPTION, $tiers, false); 463 464 // Discount milestones: two parallel lists, one entry per table row. 465 $d_amounts = isset($_POST['disc_amount']) ? array_map('sanitize_text_field', wp_unslash((array) $_POST['disc_amount'])) : []; 466 $d_labels = isset($_POST['disc_label']) ? array_values(array_map('sanitize_text_field', wp_unslash((array) $_POST['disc_label']))) : []; 467 $disc = []; 468 foreach (array_values($d_amounts) as $i => $amount) { 469 $disc[] = ['amount' => $amount, 'label' => $d_labels[$i] ?? '']; 470 } 416 471 417 472 // Settings … … 434 489 'free_ship_amount' => isset($_POST['free_ship_amount']) ? max(0, (float) str_replace(',', '.', sanitize_text_field(wp_unslash($_POST['free_ship_amount'])))) : 0, 435 490 'discount_enable' => !empty($_POST['discount_enable']) ? 1 : 0, 436 'discount_amount' => isset($_POST['discount_amount']) ? max(0, (float) str_replace(',', '.', sanitize_text_field(wp_unslash($_POST['discount_amount'])))) : 0, 437 'discount_percent' => isset($_POST['discount_percent']) ? sanitize_text_field(wp_unslash($_POST['discount_percent'])) : '', 491 'discount_tiers' => self::clean_discount_tiers($disc), 492 'discount_amount' => '', // legacy single milestone now lives in discount_tiers 493 'discount_percent' => '', 494 'bar_scale' => (isset($_POST['bar_scale']) && sanitize_key(wp_unslash($_POST['bar_scale'])) === 'steps') ? 'steps' : 'linear', 438 495 ]; 439 496 update_option(self::SETTINGS, $settings, false); … … 473 530 // Re-run through the same whitelist as save. 474 531 $in = $data['settings']; 532 // Accept the 1.2.0 list as well as a 1.1.0 export with a single discount milestone. 533 $disc = self::clean_discount_tiers($in['discount_tiers'] ?? []); 534 if (!$disc && isset($in['discount_amount']) && (float) str_replace(',', '.', (string) $in['discount_amount']) > 0) { 535 $disc = self::clean_discount_tiers([['amount' => $in['discount_amount'], 'label' => $in['discount_percent'] ?? '']]); 536 } 475 537 $accent = isset($in['accent']) ? trim((string) $in['accent']) : ''; 476 538 update_option(self::SETTINGS, [ … … 489 551 'free_ship_amount' => isset($in['free_ship_amount']) ? max(0, (float) str_replace(',', '.', (string) $in['free_ship_amount'])) : 0, 490 552 'discount_enable' => !empty($in['discount_enable']) ? 1 : 0, 491 'discount_amount' => isset($in['discount_amount']) ? max(0, (float) str_replace(',', '.', (string) $in['discount_amount'])) : 0, 492 'discount_percent' => isset($in['discount_percent']) ? sanitize_text_field((string) $in['discount_percent']) : '', 553 'discount_tiers' => $disc, 554 'discount_amount' => '', 555 'discount_percent' => '', 556 'bar_scale' => (($in['bar_scale'] ?? '') === 'steps') ? 'steps' : 'linear', 493 557 ], false); 494 558 } … … 1020 1084 $accent = $this->accent(); 1021 1085 1022 // Two independent, separately-toggled milestone sources feed one bar: 1023 // gift tiers (🎁, gated by "progress_bar") and a free-shipping goal (🚚, 1024 // gated by "free_ship_enable"). Either source alone renders the bar. 1086 // Three separately-toggled milestone sources feed one bar: gift tiers (🎁, 1087 // gated by "progress_bar"), a free-shipping goal (🚚, "free_ship_enable") and 1088 // any number of discount milestones (%, "discount_enable"). Any one alone 1089 // renders the bar. 1025 1090 $show_gifts = !empty($s['progress_bar']); 1026 1091 $ship_amt = (float) $s['free_ship_amount']; 1027 1092 $show_ship = !empty($s['free_ship_enable']) && $ship_amt > 0; 1028 $disc_amt = (float) $s['discount_amount']; 1029 $disc_txt = trim((string) $s['discount_percent']); 1030 if ($disc_txt === '') { $disc_txt = '%'; } 1031 $show_disc = !empty($s['discount_enable']) && $disc_amt > 0; 1093 $show_disc = !empty($s['discount_enable']) && !empty($s['discount_tiers']); 1032 1094 1033 1095 $milestones = []; 1034 1096 if ($show_gifts) { 1035 1097 foreach ($this->normalized_tiers() as $t) { 1036 $milestones[] = ['threshold' => (float) $t['threshold'], 'type' => 'gift' ];1098 $milestones[] = ['threshold' => (float) $t['threshold'], 'type' => 'gift', 'label' => '']; 1037 1099 } 1038 1100 } 1039 1101 if ($show_ship) { 1040 $milestones[] = ['threshold' => $ship_amt, 'type' => 'ship' ];1102 $milestones[] = ['threshold' => $ship_amt, 'type' => 'ship', 'label' => '']; 1041 1103 } 1042 1104 if ($show_disc) { 1043 $milestones[] = ['threshold' => $disc_amt, 'type' => 'disc']; 1105 foreach ($s['discount_tiers'] as $d) { 1106 $milestones[] = ['threshold' => (float) $d['amount'], 'type' => 'disc', 'label' => ($d['label'] !== '' ? $d['label'] : '%')]; 1107 } 1044 1108 } 1045 1109 if (empty($milestones)) return ''; … … 1047 1111 1048 1112 $subtotal = $this->base_subtotal(WC()->cart); 1049 1050 $max = 0.0; 1051 foreach ($milestones as $m) $max = max($max, $m['threshold']); 1052 $pct = $max > 0 ? min(100, ($subtotal / $max) * 100) : 0; 1053 1054 // Next unreached milestone (smallest threshold still above the subtotal). 1113 $max = (float) $milestones[count($milestones) - 1]['threshold']; 1114 1115 // Group milestones that are (nearly) at the same amount so their icons never 1116 // overlap, e.g. free shipping at 999 and a discount at 1000. A group is drawn 1117 // at its highest amount, so a marker never promises a reward too early. 1118 $groups = []; 1119 foreach ($milestones as $m) { 1120 $last = count($groups) - 1; 1121 if ($last >= 0 && ($m['threshold'] - $groups[$last]['min']) <= $max * self::NEAR) { 1122 $groups[$last]['items'][] = $m; 1123 $groups[$last]['max'] = $m['threshold']; 1124 } else { 1125 $groups[] = ['min' => $m['threshold'], 'max' => $m['threshold'], 'items' => [$m]]; 1126 } 1127 } 1128 $n = count($groups); 1129 $steps = ($s['bar_scale'] === 'steps'); 1130 1131 // Marker position and bar fill. "linear": proportional to the amount. 1132 // "steps": every group gets the same room; the fill moves proportionally 1133 // inside the step the customer is currently in. 1134 foreach ($groups as $i => $g) { 1135 $groups[$i]['left'] = $steps ? (($i + 1) / $n) * 100 : ($max > 0 ? ($g['max'] / $max) * 100 : 0); 1136 } 1137 if ($steps) { 1138 $pct = 100; 1139 $prev = 0.0; 1140 foreach ($groups as $i => $g) { 1141 if ($subtotal + self::EPS < $g['max']) { 1142 $span = max(self::EPS, $g['max'] - $prev); 1143 $pct = (($i + max(0, min(1, ($subtotal - $prev) / $span))) / $n) * 100; 1144 break; 1145 } 1146 $prev = $g['max']; 1147 } 1148 } else { 1149 $pct = $max > 0 ? min(100, ($subtotal / $max) * 100) : 0; 1150 } 1151 1152 // Message: the next group not yet fully reached and everything it still unlocks. 1055 1153 $next = null; 1056 foreach ($ milestones as $m) { if ($m['threshold'] > $subtotal + self::EPS) { $next = $m; break; } }1154 foreach ($groups as $g) { if ($g['max'] > $subtotal + self::EPS) { $next = $g; break; } } 1057 1155 1058 1156 if ($next) { 1059 $remaining = $next[' threshold'] - $subtotal;1157 $remaining = $next['max'] - $subtotal; 1060 1158 $amount_html = '<b style="color:' . esc_attr($accent) . '">' . wp_kses_post(wc_price($remaining)) . '</b>'; 1061 $next_gift = false; $next_ship = false; $next_disc = false; 1062 foreach ($milestones as $mm) { 1063 if (abs($mm['threshold'] - $next['threshold']) < self::EPS) { 1064 if ($mm['type'] === 'ship') { $next_ship = true; } 1065 elseif ($mm['type'] === 'disc') { $next_disc = true; } 1066 else { $next_gift = true; } 1067 } 1068 } 1069 if ($next_gift && $next_ship) { 1159 $next_gift = false; $next_ship = false; $disc_txt = ''; 1160 foreach ($next['items'] as $mm) { 1161 if ($mm['threshold'] <= $subtotal + self::EPS) continue; // already unlocked 1162 if ($mm['type'] === 'ship') { $next_ship = true; } 1163 elseif ($mm['type'] === 'disc') { $disc_txt = $mm['label']; } // highest discount in the group wins 1164 else { $next_gift = true; } 1165 } 1166 $disc_html = '<b style="color:' . esc_attr($accent) . '">' . esc_html($disc_txt) . '</b>'; 1167 if ($next_gift && $next_ship && $disc_txt !== '') { 1168 /* translators: %1$s = formatted price amount, %2$s = discount label such as "5%" */ 1169 $msg = sprintf(__('Spend %1$s more for a free gift, free shipping + %2$s off!', 'giftvo'), $amount_html, $disc_html); 1170 } elseif ($next_gift && $next_ship) { 1070 1171 /* translators: %s = formatted price amount */ 1071 1172 $msg = sprintf(__('Spend %s more for a free gift + free shipping!', 'giftvo'), $amount_html); 1173 } elseif ($next_gift && $disc_txt !== '') { 1174 /* translators: %1$s = formatted price amount, %2$s = discount label such as "5%" */ 1175 $msg = sprintf(__('Spend %1$s more for a free gift + %2$s off!', 'giftvo'), $amount_html, $disc_html); 1176 } elseif ($next_ship && $disc_txt !== '') { 1177 /* translators: %1$s = formatted price amount, %2$s = discount label such as "5%" */ 1178 $msg = sprintf(__('Spend %1$s more for free shipping + %2$s off!', 'giftvo'), $amount_html, $disc_html); 1072 1179 } elseif ($next_ship) { 1073 1180 /* translators: %s = formatted price amount */ 1074 1181 $msg = sprintf(__('Spend %s more for free shipping!', 'giftvo'), $amount_html); 1075 } elseif ($ next_disc && !$next_gift) {1182 } elseif ($disc_txt !== '') { 1076 1183 /* translators: %1$s = amount, %2$s = discount label such as "5%" */ 1077 $msg = sprintf(__('Spend %1$s more for %2$s off!', 'giftvo'), $amount_html, '<b style="color:' . esc_attr($accent) . '">' . esc_html($disc_txt) . '</b>');1184 $msg = sprintf(__('Spend %1$s more for %2$s off!', 'giftvo'), $amount_html, $disc_html); 1078 1185 } else { 1079 1186 /* translators: %s = formatted price amount */ … … 1094 1201 } 1095 1202 1096 // One marker per milestone. Markers live in a layer that spans exactly the 1097 // track width; the bar is inset by half an icon (9px) so the first/last 1098 // marker sits flush with the track ends without the icon overflowing. 1099 // Group milestones that share a threshold so their icons never overlap 1100 // (e.g. a gift tier and the free-shipping goal both set to the same amount). 1101 $groups = []; 1102 foreach ($milestones as $m) { 1103 $key = number_format($m['threshold'], 2, '.', ''); 1104 if (!isset($groups[$key])) $groups[$key] = ['threshold' => $m['threshold'], 'gift' => false, 'ship' => false, 'disc' => false]; 1105 $groups[$key][$m['type']] = true; 1106 } 1107 1203 // One marker per group. Markers live in a layer that spans exactly the track 1204 // width; the bar is inset by half an icon (9px) so the first/last marker sits 1205 // flush with the track ends without the icon overflowing. 1108 1206 $show_amt = !empty($s['show_amounts']); 1109 1207 $markers = ''; 1110 1208 foreach ($groups as $g) { 1111 $left = $max > 0 ? min(100, max(0, ($g['threshold'] / $max) * 100)) : 0; 1112 $reached = ($subtotal + self::EPS) >= $g['threshold']; 1113 $ico = $reached ? 'opacity:1' : 'filter:grayscale(1);opacity:.45'; 1114 // Connector colour: a reached milestone's line turns gold so it pops 1115 // against the green progress bar; unreached stays grey. Applies to plain 1209 $left = min(100, max(0, $g['left'])); 1210 $reached = ($subtotal + self::EPS) >= $g['max']; 1211 // Connector colour: a fully reached group's line turns gold so it pops 1212 // against the green progress bar; otherwise it stays grey. Applies to plain 1116 1213 // single stems and the joined U-bracket alike. 1117 1214 $conn = $reached ? '#e0a516' : '#c7d1cb'; 1118 1215 $amt_html = ''; 1119 1216 if ($show_amt) { 1120 $amount = wp_strip_all_tags(wc_price($g[' threshold'], ['decimals' => 0]));1217 $amount = wp_strip_all_tags(wc_price($g['max'], ['decimals' => 0])); 1121 1218 $acol = $reached ? $accent : '#9aa39c'; 1122 1219 $amt_html = '<span style="font-size:10.5px;line-height:1;white-space:nowrap;margin-bottom:2px;font-weight:600;color:' . esc_attr($acol) . '">' . esc_html($amount) . '</span>'; 1123 1220 } 1124 // Icon(s) for whatever this threshold unlocks; 2+ get a U-shaped bracket. 1221 // Icon(s) for whatever this group unlocks, each lit once its own amount is 1222 // reached; 2+ icons get a U-shaped bracket. 1125 1223 $spans = []; 1126 if ($g['gift']) { $spans[] = '<span style="font-size:13px;line-height:1;' . $ico . '">🎁</span>'; } 1127 if ($g['ship']) { $spans[] = '<span style="font-size:13px;line-height:1;' . $ico . '">🚚</span>'; } 1128 if ($g['disc']) { $spans[] = '<span style="display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:16px;padding:0 3px;border:1.5px solid ' . esc_attr($conn) . ';border-radius:8px;color:' . esc_attr($conn) . ';font-size:9px;font-weight:800;line-height:1">' . esc_html($disc_txt) . '</span>'; } 1224 foreach ($g['items'] as $mm) { 1225 $on = ($subtotal + self::EPS) >= $mm['threshold']; 1226 $ico = $on ? 'opacity:1' : 'filter:grayscale(1);opacity:.45'; 1227 if ($mm['type'] === 'gift') { 1228 $spans[] = '<span style="font-size:13px;line-height:1;' . $ico . '">🎁</span>'; 1229 } elseif ($mm['type'] === 'ship') { 1230 $spans[] = '<span style="font-size:13px;line-height:1;' . $ico . '">🚚</span>'; 1231 } else { 1232 $dcol = $on ? '#e0a516' : '#c7d1cb'; 1233 $spans[] = '<span style="display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:16px;padding:0 3px;border:1.5px solid ' . esc_attr($dcol) . ';border-radius:8px;color:' . esc_attr($dcol) . ';font-size:9px;font-weight:800;line-height:1;white-space:nowrap">' . esc_html($mm['label']) . '</span>'; 1234 } 1235 } 1129 1236 $both = count($spans) > 1; 1130 1237 if ($both) { … … 1140 1247 $icons = $spans[0]; 1141 1248 } 1142 $markers .= '<div class="wctfg-mark" title="' . esc_attr(wp_strip_all_tags(wc_price($g[' threshold']))) . '"'1249 $markers .= '<div class="wctfg-mark" title="' . esc_attr(wp_strip_all_tags(wc_price($g['max']))) . '"' 1143 1250 . ' style="position:absolute;top:0;bottom:0;left:' . esc_attr(round($left, 2)) . '%;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;z-index:2">' 1144 1251 . $amt_html -
giftvo/trunk/readme.txt
r3706274 r3741869 7 7 WC requires at least: 6.0 8 8 WC tested up to: 9.3 9 Stable tag: 1. 1.09 Stable tag: 1.2.0 10 10 License: GPLv2 or later 11 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 25 25 * **Cumulative or highest‑only** — stack the gifts of every reached tier, or give just the top one. 26 26 * **Live progress bar** — “Spend €X more to unlock a free gift!” with a gift marker for every threshold, on the cart, mini‑cart and via the `[gift_progress]` shortcode. 27 * **Free‑shipping milestone** — add an optional 🚚 goal to the progress bar so shoppers see how close they are to free shipping (uses the same threshold basis as gifts). 28 * **Discount milestone** — add an optional % goal marker to the progress bar. 27 * **Free‑shipping goal** — optionally add a 🚚 milestone to the same bar (“Spend €X more for free shipping!”). It’s a plain amount you set, independent of any shipping plugin, and can be switched off in Settings. 28 * **Discount milestones** — add as many % milestones as you like (e.g. 5% from €1,000, 6% from €2,000 …) so shoppers see every step of a tiered discount on the same bar (“Spend €X more for 6% off!”). Giftvo only shows the milestones; the discount itself comes from your pricing or discount plugin. 29 * **Proportional or evenly spaced bar** — place markers at their amount, or give every milestone the same room so many steps stay readable on phones. Milestones at almost the same amount (e.g. free shipping at €999 and a discount at €1,000) share one marker. 29 30 * **Crossed‑out value** — show the gift’s normal price struck through next to “Free”. 30 31 * **Celebration** — an optional confetti burst when a new gift is unlocked. … … 32 33 * **Gateway safe** — the gift is a genuine 0.00 line item; the basket total always matches the amount charged. 33 34 * **White‑label ready** — set the menu title, badge text, “Free” label and accent colour; import/export the whole configuration as JSON. 34 * **Translation ready** — ships with English, German and Turkish; text domain `giftvo`.35 * **Translation ready** — text domain `giftvo`; translations are delivered through translate.wordpress.org. 35 36 * **HPOS compatible** — declares compatibility with WooCommerce High‑Performance Order Storage. 36 37 … … 55 56 Yes. Set a tier’s mode to “Customer picks one”, add several products, and the shopper chooses one in the cart. Variable gifts also let them pick the colour/variation. 56 57 58 = Does Giftvo apply the discount milestones? = 59 No. Discount milestones are a display feature: they show shoppers how far they are from each discount step. Create the discount itself with your pricing or discount plugin and enter the same amounts and labels in Giftvo. 60 61 = How many tiers and milestones can I add? = 62 As many as you need — gift tiers and discount milestones are both unlimited. With many milestones, choose the “Evenly spaced” bar scale so the markers stay readable on small screens. 63 57 64 = Does the threshold include tax? = 58 65 Your choice. Under Settings you can base it on the subtotal incl. or excl. tax, before or after discounts. 66 67 == Upgrade Notice == 68 69 = 1.2.0 = 70 Unlimited discount milestones and an evenly spaced progress bar option. Existing settings are kept; a single discount milestone is moved into the new list automatically. 59 71 60 72 == Screenshots == … … 66 78 == Changelog == 67 79 80 = 1.2.0 = 81 * New: unlimited discount milestones on the progress bar — add one row per discount step (amount + label, e.g. 1000 → “5%”). A 1.1.0 single discount milestone is moved into the list automatically. 82 * New: “Evenly spaced” progress bar scale — every milestone gets the same room, so bars with many steps stay readable on phones. “Proportional” stays the default. 83 * Milestones at almost the same amount are drawn as one combined marker, and the message names everything the next step unlocks (e.g. “Spend €21 more for free shipping + 5% off!”). 84 * Translations now load from translate.wordpress.org (no bundled language files). 85 68 86 = 1.1.0 = 69 * New: optional free‑shipping milestone (🚚) on the progress bar, using the same threshold basis as gifts.70 * New: optional discount milestone (%) on the progress bar.71 * Minor UI and stability improvements.87 * New: optional free‑shipping goal on the progress bar — adds a 🚚 milestone (“Spend €X more for free shipping!”) alongside the gift markers. Set the amount and toggle it in Settings; it’s independent of any shipping plugin. 88 * Markers that share the same threshold (e.g. a gift tier and the free‑shipping goal set to the same amount) are merged into a single combined marker so the icons never overlap. 89 * Updated English, German and Turkish translations. 72 90 73 91 = 1.0.0 = … … 81 99 * White‑label options (menu title, badge, label, accent), JSON import/export, uninstall cleanup. 82 100 * English, German and Turkish translations. HPOS compatible. 83 84 == Upgrade Notice ==85 86 = 1.1.0 =87 Adds optional free‑shipping (🚚) and discount (%) milestones to the progress bar.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)