Changeset 3625812
- Timestamp:
- 07/28/2026 11:26:07 AM (3 months ago)
- Location:
- priceblueprint-for-woocommerce
- Files:
-
- 4 added
- 20 edited
- 1 copied
-
tags/1.7.3 (copied) (copied from priceblueprint-for-woocommerce/trunk )
-
tags/1.7.3/CLAUDE.md (added)
-
tags/1.7.3/assets/css/welcome-screen.css (modified) (3 diffs)
-
tags/1.7.3/priceblueprint-for-woocommerce.php (modified) (4 diffs)
-
tags/1.7.3/readme.txt (modified) (2 diffs)
-
tags/1.7.3/src/Admin/SaveHandler.php (modified) (1 diff)
-
tags/1.7.3/src/Ajax/ImportDemo.php (modified) (1 diff)
-
tags/1.7.3/src/CPT/Blueprint.php (modified) (3 diffs)
-
tags/1.7.3/src/Cart/CartItemMeta.php (modified) (2 diffs)
-
tags/1.7.3/src/Frontend/ProductPage.php (modified) (1 diff)
-
tags/1.7.3/templates/welcome-screen.php (modified) (1 diff)
-
tags/1.7.3/uninstall.php (modified) (1 diff)
-
tags/1.7.3/vendor/freemius/assets/img/priceblueprint-for-woocommerce.png (added)
-
trunk/CLAUDE.md (added)
-
trunk/assets/css/welcome-screen.css (modified) (3 diffs)
-
trunk/priceblueprint-for-woocommerce.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/src/Admin/SaveHandler.php (modified) (1 diff)
-
trunk/src/Ajax/ImportDemo.php (modified) (1 diff)
-
trunk/src/CPT/Blueprint.php (modified) (3 diffs)
-
trunk/src/Cart/CartItemMeta.php (modified) (2 diffs)
-
trunk/src/Frontend/ProductPage.php (modified) (1 diff)
-
trunk/templates/welcome-screen.php (modified) (1 diff)
-
trunk/uninstall.php (modified) (1 diff)
-
trunk/vendor/freemius/assets/img/priceblueprint-for-woocommerce.png (added)
Legend:
- Unmodified
- Added
- Removed
-
priceblueprint-for-woocommerce/tags/1.7.3/assets/css/welcome-screen.css
r3597567 r3625812 132 132 } 133 133 134 /* ── Actions ── */ 135 .prbp-welcome-actions { 136 display: flex; 137 align-items: center; 138 gap: 16px; 139 padding: 0 4px; 140 } 141 .prbp-welcome-cta { 134 /* ── Get started card (demo import = primary, create/skip = secondary) ── */ 135 .prbp-welcome-getstarted { 136 background: #fff; 137 border: 1px solid #E2E8F0; 138 border-radius: 12px; 139 padding: 32px 40px; 140 margin-bottom: 12px; 141 box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.04); 142 } 143 144 /* Shared button treatments */ 145 .prbp-welcome-btn-primary, 146 .prbp-welcome-btn-secondary { 142 147 display: inline-flex; 143 148 align-items: center; 144 149 gap: 6px; 145 background: #0D9488;146 color: #fff !important;147 border: none;148 150 border-radius: 6px; 149 padding: 10px 22px;150 font-size: 14px;151 151 font-weight: 600; 152 152 text-decoration: none !important; 153 153 cursor: pointer; 154 line-height: 1.4; 155 font-family: inherit; 156 white-space: nowrap; 157 } 158 .prbp-welcome-btn-primary { 159 flex-shrink: 0; 160 background: #0D9488; 161 color: #fff !important; 162 border: none; 163 padding: 10px 22px; 164 font-size: 14px; 154 165 transition: background .15s; 155 line-height: 1.4; 156 } 157 .prbp-welcome-cta:hover { 166 } 167 .prbp-welcome-btn-primary:hover { 158 168 background: #0a7a70; 159 169 color: #fff !important; 170 } 171 .prbp-welcome-btn-primary:disabled { 172 cursor: not-allowed; 173 opacity: 0.6; 174 } 175 .prbp-welcome-btn-secondary { 176 background: #fff; 177 color: #1d2327 !important; 178 border: 1px solid #c3c4c7; 179 padding: 9px 20px; 180 font-size: 13px; 181 font-weight: 500; 182 transition: border-color .15s, color .15s; 183 } 184 .prbp-welcome-btn-secondary:hover { 185 border-color: #0a7a70; 186 color: #0a7a70 !important; 160 187 } 161 188 .prbp-welcome-skip { … … 169 196 } 170 197 171 /* ── Demo Import ── */ 172 .prbp-welcome-demo { 173 background: #fff; 174 border: 1px solid #E2E8F0; 175 border-radius: 12px; 176 padding: 28px 40px; 177 margin-top: 12px; 178 box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.04); 179 } 198 /* ── Demo Import (primary action) ── */ 180 199 .prbp-welcome-demo .prbp-welcome-steps-title { 181 200 margin-bottom: 12px; … … 193 212 line-height: 1.5; 194 213 } 195 .prbp-welcome-demo-btn { 196 flex-shrink: 0; 197 display: inline-flex; 198 align-items: center; 199 gap: 8px; 200 background: #fff; 201 color: #1d2327; 202 border: 1px solid #c3c4c7; 203 border-radius: 6px; 204 padding: 9px 20px; 205 font-size: 13px; 206 font-weight: 500; 207 cursor: pointer; 208 transition: border-color .15s, color .15s; 209 line-height: 1.4; 210 font-family: inherit; 211 white-space: nowrap; 212 } 213 .prbp-welcome-demo-btn:hover { 214 border-color: #0a7a70; 215 color: #0a7a70; 216 } 217 .prbp-welcome-demo-btn:disabled { 218 cursor: not-allowed; 219 opacity: 0.6; 214 215 /* ── Divider between primary and secondary actions ── */ 216 .prbp-welcome-divider { 217 height: 1px; 218 background: #E2E8F0; 219 margin: 24px 0; 220 } 221 222 /* ── Secondary row: create-from-scratch + skip ── */ 223 .prbp-welcome-secondary-row { 224 display: flex; 225 align-items: center; 226 flex-wrap: wrap; 227 gap: 16px; 228 } 229 .prbp-welcome-secondary-label { 230 font-size: 13px; 231 color: #64748B; 232 margin-right: 4px; 220 233 } 221 234 @keyframes prbp-spin { -
priceblueprint-for-woocommerce/tags/1.7.3/priceblueprint-for-woocommerce.php
r3605639 r3625812 3 3 * Plugin Name: WooCommerce Attribute Pricing — PriceBlueprint 4 4 * Description: Reusable pricing blueprints for WooCommerce. Assign one blueprint to multiple products — define attribute-based pricing rules once, update everywhere instantly. 5 * Version: 1.7. 15 * Version: 1.7.3 6 6 * Requires at least: 6.0 7 7 * Requires PHP: 7.4 … … 43 43 'has_paid_plans' => false, 44 44 'is_org_compliant' => true, 45 'enable_anonymous' => true, 45 46 'menu' => array( 46 47 'first-path' => 'plugins.php', … … 50 51 ), 51 52 ) ); 53 54 // Reassuring, plain-language opt-in message so first-time activators 55 // understand what's shared (and that "Skip" is a perfectly fine choice). 56 $pfwp_fs->add_filter( 'connect_message', function () { 57 $plugin_name = esc_html__( 'PriceBlueprint', 'priceblueprint-for-woocommerce' ); 58 59 return sprintf( 60 /* translators: 1: opening <strong>PriceBlueprint</strong> tag, 2: plugin name */ 61 esc_html__( 'Opt in to get security & feature updates for %1$s, plus help us keep it compatible with your store by sharing basic, non-personal environment info (WordPress/PHP/WooCommerce version, site URL). No spam, we never sell your data, and you can change your mind anytime from Account settings. Rather not share anything? Click "Skip" below — %2$s works exactly the same either way.', 'priceblueprint-for-woocommerce' ), 62 '<strong>' . $plugin_name . '</strong>', 63 $plugin_name 64 ); 65 } ); 52 66 } 53 67 … … 70 84 // wp i18n make-pot . languages/priceblueprint.pot --domain=priceblueprint --exclude=vendor,node_modules 71 85 72 define( 'PRBP_VERSION', '1.7. 1' );86 define( 'PRBP_VERSION', '1.7.3' ); 73 87 define( 'PRBP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 74 88 define( 'PRBP_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); -
priceblueprint-for-woocommerce/tags/1.7.3/readme.txt
r3605639 r3625812 4 4 Requires at least: 6.0 5 5 Tested up to: 7.0 6 Stable tag: 1.7. 16 Stable tag: 1.7.3 7 7 Requires PHP: 7.4 8 8 License: GPL-2.0-or-later … … 84 84 == Changelog == 85 85 86 = 1.7.3 = 87 * Improved the Freemius connection prompt with a clearer explanation of what's shared and why. 88 * Reordered the welcome screen so trying the demo data comes first. 89 90 = 1.7.2 = 91 * Security: Add-to-cart validation now verifies every selected option against the blueprint's actual rule values — previously a tampered form submission could add a product to the cart without paying for its selected options. 92 * Fix: The plugin could not be deleted from the Plugins screen — uninstall.php referenced a Freemius helper that is unavailable during uninstall, causing a fatal error. Uninstall now also removes plugin options and correctly clears leftover transients. 93 * Fix: Fatal parse error on PHP 7.4 — a PHP 8-only union type in the gallery placeholder handler broke sites running the minimum supported PHP version. 94 * Fix: Pricing rules containing non-Latin characters (ö, ü, ł, Cyrillic, etc.) or quotes in term labels were corrupted on save due to missing slashing around the stored JSON. Affected blueprints need to be re-saved once after updating. 95 * Fix: Moving a blueprint to the Trash no longer wipes prices and attributes of linked products. Trashing now only unpublishes linked products (fully reversible); permanent deletion additionally unlinks them, leaving all other product data intact. 96 86 97 = 1.7.1 = 87 98 * Fix: "Import Demo Data" could fatal error (500) on some stores due to an unguarded wp_insert_term() call — now handled gracefully with a proper error message. -
priceblueprint-for-woocommerce/tags/1.7.3/src/Admin/SaveHandler.php
r3597567 r3625812 69 69 } 70 70 71 update_post_meta( $post_id, 'prbp_template_rules', wp_json_encode( self::sanitizeSections( $sections ) ) ); 71 // wp_slash() — update_post_meta() unslashes its value, which would strip the 72 // backslashes wp_json_encode() uses for \uXXXX and \" escapes and corrupt the JSON. 73 update_post_meta( $post_id, 'prbp_template_rules', wp_slash( wp_json_encode( self::sanitizeSections( $sections ) ) ) ); 72 74 RulesCache::flush( $post_id ); 73 75 } -
priceblueprint-for-woocommerce/tags/1.7.3/src/Ajax/ImportDemo.php
r3605639 r3625812 183 183 } 184 184 185 update_post_meta( $blueprint_id, 'prbp_template_rules', wp_json_encode( $rules ) ); 185 // wp_slash() — update_post_meta() unslashes, which would corrupt JSON escape sequences. 186 update_post_meta( $blueprint_id, 'prbp_template_rules', wp_slash( wp_json_encode( $rules ) ) ); 186 187 187 188 return $blueprint_id; -
priceblueprint-for-woocommerce/tags/1.7.3/src/CPT/Blueprint.php
r3552429 r3625812 16 16 public static function register(): void { 17 17 add_action( 'init', [ self::class, 'registerCPT' ], 10 ); 18 add_action( 'before_delete_post', [ self::class, 'handle Removal' ], 10, 1 );19 add_action( 'wp_trash_post', [ self::class, 'handle Removal' ],10, 1 );18 add_action( 'before_delete_post', [ self::class, 'handleDelete' ], 10, 1 ); 19 add_action( 'wp_trash_post', [ self::class, 'handleTrash' ], 10, 1 ); 20 20 } 21 21 … … 49 49 50 50 /** 51 * When a blueprint is trashed or permanently deleted, move all linked products 52 * to draft and clear their price, attributes, and blueprint reference. 51 * Trashing a blueprint is reversible, so it must not destroy product data: 52 * linked products are only unpublished (draft). Prices, attributes, and the 53 * prbp_template_id link stay intact — restoring the blueprint from Trash lets 54 * the merchant simply republish the products. 53 55 * 54 56 * @param int $post_id 55 57 */ 56 public static function handle Removal( int $post_id ): void {58 public static function handleTrash( int $post_id ): void { 57 59 if ( get_post_type( $post_id ) !== 'price_blueprint' ) { 58 60 return; 59 61 } 60 62 61 $linked_ids = get_posts( [ 63 foreach ( self::linkedProductIds( $post_id ) as $product_id ) { 64 self::unpublishProduct( $product_id ); 65 } 66 } 67 68 /** 69 * Permanent deletion: unpublish linked products and remove the blueprint 70 * reference. Prices and attributes are deliberately left untouched — they 71 * may include data the merchant set manually for other purposes. 72 * 73 * @param int $post_id 74 */ 75 public static function handleDelete( int $post_id ): void { 76 if ( get_post_type( $post_id ) !== 'price_blueprint' ) { 77 return; 78 } 79 80 foreach ( self::linkedProductIds( $post_id ) as $product_id ) { 81 self::unpublishProduct( $product_id ); 82 delete_post_meta( $product_id, 'prbp_template_id' ); 83 } 84 } 85 86 /** 87 * @param int $post_id Blueprint post ID. 88 * @return int[] Product IDs linked to this blueprint. 89 */ 90 private static function linkedProductIds( int $post_id ): array { 91 return get_posts( [ 62 92 'post_type' => 'product', 63 93 'post_status' => 'any', … … 72 102 ], 73 103 ] ); 104 } 74 105 75 foreach ( $linked_ids as $product_id ) { 76 $product = wc_get_product( $product_id ); 77 if ( ! $product ) { 78 continue; 79 } 106 /** 107 * Move a published product to draft without touching any of its other data. 108 * 109 * @param int $product_id 110 */ 111 private static function unpublishProduct( int $product_id ): void { 112 if ( 'publish' !== get_post_status( $product_id ) ) { 113 return; 114 } 80 115 81 $product->set_status( 'draft' ); 82 $product->set_regular_price( '' ); 83 $product->set_price( '' ); 84 $product->set_sale_price( '' ); 85 $product->set_attributes( [] ); 86 $product->save(); 116 $product = wc_get_product( $product_id ); 117 if ( ! $product ) { 118 return; 119 } 87 120 88 delete_post_meta( $product_id, 'prbp_template_id' );89 }121 $product->set_status( 'draft' ); 122 $product->save(); 90 123 } 91 124 } -
priceblueprint-for-woocommerce/tags/1.7.3/src/Cart/CartItemMeta.php
r3587316 r3625812 24 24 25 25 /** 26 * Server-side validation: all attributes from the template must be selected. 26 * Server-side validation: every attribute from the template must carry a 27 * selection that matches one of that attribute's rule value slugs. A bare 28 * "not empty" check is not enough — a tampered slug would silently skip 29 * rule matching in capture() and the option's price would never be added. 27 30 * 28 31 * @param bool $passed … … 52 55 53 56 foreach ( $attributes as $attr ) { 54 if ( empty( $selections[ $attr ] ) ) { 57 $slug = $selections[ $attr ] ?? ''; 58 $is_valid = false; 59 60 if ( '' !== $slug ) { 61 foreach ( $rules as $rule ) { 62 if ( $rule['attribute'] === $attr 63 && in_array( $slug, (array) ( $rule['value_slugs'] ?? [] ), true ) 64 ) { 65 $is_valid = true; 66 break; 67 } 68 } 69 } 70 71 if ( ! $is_valid ) { 55 72 wc_add_notice( 56 73 sprintf( -
priceblueprint-for-woocommerce/tags/1.7.3/src/Frontend/ProductPage.php
r3597567 r3625812 181 181 * inside .woocommerce-product-gallery__wrapper; it offsets flexslider's horizontal 182 182 * slide positions, making the main viewport appear blank. 183 */ 184 public static function suppressGalleryPlaceholder( string $html, int|string $attachment_id ): string { 183 * 184 * @param string $html 185 * @param int|string $attachment_id 186 */ 187 public static function suppressGalleryPlaceholder( string $html, $attachment_id ): string { 185 188 if ( $attachment_id ) { 186 189 return $html; -
priceblueprint-for-woocommerce/tags/1.7.3/templates/welcome-screen.php
r3561869 r3625812 47 47 </div> 48 48 49 <div class="prbp-welcome-actions"> 50 <a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=price_blueprint' ) ); ?>" 51 class="prbp-welcome-cta"> 52 <?php esc_html_e( 'Create your first Blueprint', 'priceblueprint-for-woocommerce' ); ?> → 53 </a> 54 <a href="<?php echo esc_url( admin_url( 'edit.php?post_type=price_blueprint' ) ); ?>" 55 class="prbp-welcome-skip"> 56 <?php esc_html_e( 'Skip for now', 'priceblueprint-for-woocommerce' ); ?> 57 </a> 58 </div> 49 <div class="prbp-welcome-getstarted"> 59 50 60 <div class="prbp-welcome-demo" id="prbp-demo-wrap"> 61 <p class="prbp-welcome-steps-title"><?php esc_html_e( 'Try it first', 'priceblueprint-for-woocommerce' ); ?></p> 62 <div class="prbp-welcome-demo-row"> 63 <p class="prbp-welcome-demo-desc"><?php esc_html_e( 'Import a sample blueprint and product to see how PriceBlueprint works before building your own.', 'priceblueprint-for-woocommerce' ); ?></p> 64 <button type="button" id="prbp-import-demo" class="prbp-welcome-demo-btn"> 65 <?php esc_html_e( 'Import Demo Data', 'priceblueprint-for-woocommerce' ); ?> 66 </button> 51 <div class="prbp-welcome-demo" id="prbp-demo-wrap"> 52 <p class="prbp-welcome-steps-title"><?php esc_html_e( 'Try it first', 'priceblueprint-for-woocommerce' ); ?></p> 53 <div class="prbp-welcome-demo-row"> 54 <p class="prbp-welcome-demo-desc"><?php esc_html_e( 'Import a sample blueprint and product to see how PriceBlueprint works before building your own — the fastest way to see a real price change on your storefront.', 'priceblueprint-for-woocommerce' ); ?></p> 55 <button type="button" id="prbp-import-demo" class="prbp-welcome-btn-primary"> 56 <?php esc_html_e( 'Import Demo Data', 'priceblueprint-for-woocommerce' ); ?> → 57 </button> 58 </div> 67 59 </div> 60 61 <div class="prbp-welcome-divider"></div> 62 63 <div class="prbp-welcome-secondary-row"> 64 <span class="prbp-welcome-secondary-label"><?php esc_html_e( 'Prefer to start from scratch?', 'priceblueprint-for-woocommerce' ); ?></span> 65 <a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=price_blueprint' ) ); ?>" 66 class="prbp-welcome-btn-secondary"> 67 <?php esc_html_e( 'Create your first Blueprint', 'priceblueprint-for-woocommerce' ); ?> 68 </a> 69 <a href="<?php echo esc_url( admin_url( 'edit.php?post_type=price_blueprint' ) ); ?>" 70 class="prbp-welcome-skip"> 71 <?php esc_html_e( 'Skip for now', 'priceblueprint-for-woocommerce' ); ?> 72 </a> 73 </div> 74 68 75 </div> 69 76 -
priceblueprint-for-woocommerce/tags/1.7.3/uninstall.php
r3557112 r3625812 28 28 delete_post_meta_by_key( 'prbp_template_id' ); 29 29 30 // Delete plugin options. 31 delete_option( 'prbp_show_welcome' ); 32 delete_option( 'prbp_demo_blueprint_id' ); 33 delete_option( 'prbp_demo_product_id' ); 34 30 35 // Clean up any lingering transients (save error transients use user IDs). 31 36 global $wpdb; 32 37 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 33 38 $wpdb->query( 34 "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient prbp_save_error_%' OR option_name LIKE '_transient_timeoutprbp_save_error_%'"39 "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_prbp_save_error_%' OR option_name LIKE '_transient_timeout_prbp_save_error_%'" 35 40 ); 36 41 37 // Not like register_uninstall_hook(), you do NOT have to use a static function. 38 pfwp_fs()->add_action('after_uninstall', 'pfwp_fs_uninstall_cleanup'); 42 // NOTE: Do not call pfwp_fs() here — the main plugin file is NOT loaded when 43 // WordPress runs uninstall.php, so the Freemius helper does not exist and the 44 // call would fatal, blocking plugin deletion. Freemius account data is shared 45 // SDK state and is left untouched intentionally. -
priceblueprint-for-woocommerce/trunk/assets/css/welcome-screen.css
r3597567 r3625812 132 132 } 133 133 134 /* ── Actions ── */ 135 .prbp-welcome-actions { 136 display: flex; 137 align-items: center; 138 gap: 16px; 139 padding: 0 4px; 140 } 141 .prbp-welcome-cta { 134 /* ── Get started card (demo import = primary, create/skip = secondary) ── */ 135 .prbp-welcome-getstarted { 136 background: #fff; 137 border: 1px solid #E2E8F0; 138 border-radius: 12px; 139 padding: 32px 40px; 140 margin-bottom: 12px; 141 box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.04); 142 } 143 144 /* Shared button treatments */ 145 .prbp-welcome-btn-primary, 146 .prbp-welcome-btn-secondary { 142 147 display: inline-flex; 143 148 align-items: center; 144 149 gap: 6px; 145 background: #0D9488;146 color: #fff !important;147 border: none;148 150 border-radius: 6px; 149 padding: 10px 22px;150 font-size: 14px;151 151 font-weight: 600; 152 152 text-decoration: none !important; 153 153 cursor: pointer; 154 line-height: 1.4; 155 font-family: inherit; 156 white-space: nowrap; 157 } 158 .prbp-welcome-btn-primary { 159 flex-shrink: 0; 160 background: #0D9488; 161 color: #fff !important; 162 border: none; 163 padding: 10px 22px; 164 font-size: 14px; 154 165 transition: background .15s; 155 line-height: 1.4; 156 } 157 .prbp-welcome-cta:hover { 166 } 167 .prbp-welcome-btn-primary:hover { 158 168 background: #0a7a70; 159 169 color: #fff !important; 170 } 171 .prbp-welcome-btn-primary:disabled { 172 cursor: not-allowed; 173 opacity: 0.6; 174 } 175 .prbp-welcome-btn-secondary { 176 background: #fff; 177 color: #1d2327 !important; 178 border: 1px solid #c3c4c7; 179 padding: 9px 20px; 180 font-size: 13px; 181 font-weight: 500; 182 transition: border-color .15s, color .15s; 183 } 184 .prbp-welcome-btn-secondary:hover { 185 border-color: #0a7a70; 186 color: #0a7a70 !important; 160 187 } 161 188 .prbp-welcome-skip { … … 169 196 } 170 197 171 /* ── Demo Import ── */ 172 .prbp-welcome-demo { 173 background: #fff; 174 border: 1px solid #E2E8F0; 175 border-radius: 12px; 176 padding: 28px 40px; 177 margin-top: 12px; 178 box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.04); 179 } 198 /* ── Demo Import (primary action) ── */ 180 199 .prbp-welcome-demo .prbp-welcome-steps-title { 181 200 margin-bottom: 12px; … … 193 212 line-height: 1.5; 194 213 } 195 .prbp-welcome-demo-btn { 196 flex-shrink: 0; 197 display: inline-flex; 198 align-items: center; 199 gap: 8px; 200 background: #fff; 201 color: #1d2327; 202 border: 1px solid #c3c4c7; 203 border-radius: 6px; 204 padding: 9px 20px; 205 font-size: 13px; 206 font-weight: 500; 207 cursor: pointer; 208 transition: border-color .15s, color .15s; 209 line-height: 1.4; 210 font-family: inherit; 211 white-space: nowrap; 212 } 213 .prbp-welcome-demo-btn:hover { 214 border-color: #0a7a70; 215 color: #0a7a70; 216 } 217 .prbp-welcome-demo-btn:disabled { 218 cursor: not-allowed; 219 opacity: 0.6; 214 215 /* ── Divider between primary and secondary actions ── */ 216 .prbp-welcome-divider { 217 height: 1px; 218 background: #E2E8F0; 219 margin: 24px 0; 220 } 221 222 /* ── Secondary row: create-from-scratch + skip ── */ 223 .prbp-welcome-secondary-row { 224 display: flex; 225 align-items: center; 226 flex-wrap: wrap; 227 gap: 16px; 228 } 229 .prbp-welcome-secondary-label { 230 font-size: 13px; 231 color: #64748B; 232 margin-right: 4px; 220 233 } 221 234 @keyframes prbp-spin { -
priceblueprint-for-woocommerce/trunk/priceblueprint-for-woocommerce.php
r3605639 r3625812 3 3 * Plugin Name: WooCommerce Attribute Pricing — PriceBlueprint 4 4 * Description: Reusable pricing blueprints for WooCommerce. Assign one blueprint to multiple products — define attribute-based pricing rules once, update everywhere instantly. 5 * Version: 1.7. 15 * Version: 1.7.3 6 6 * Requires at least: 6.0 7 7 * Requires PHP: 7.4 … … 43 43 'has_paid_plans' => false, 44 44 'is_org_compliant' => true, 45 'enable_anonymous' => true, 45 46 'menu' => array( 46 47 'first-path' => 'plugins.php', … … 50 51 ), 51 52 ) ); 53 54 // Reassuring, plain-language opt-in message so first-time activators 55 // understand what's shared (and that "Skip" is a perfectly fine choice). 56 $pfwp_fs->add_filter( 'connect_message', function () { 57 $plugin_name = esc_html__( 'PriceBlueprint', 'priceblueprint-for-woocommerce' ); 58 59 return sprintf( 60 /* translators: 1: opening <strong>PriceBlueprint</strong> tag, 2: plugin name */ 61 esc_html__( 'Opt in to get security & feature updates for %1$s, plus help us keep it compatible with your store by sharing basic, non-personal environment info (WordPress/PHP/WooCommerce version, site URL). No spam, we never sell your data, and you can change your mind anytime from Account settings. Rather not share anything? Click "Skip" below — %2$s works exactly the same either way.', 'priceblueprint-for-woocommerce' ), 62 '<strong>' . $plugin_name . '</strong>', 63 $plugin_name 64 ); 65 } ); 52 66 } 53 67 … … 70 84 // wp i18n make-pot . languages/priceblueprint.pot --domain=priceblueprint --exclude=vendor,node_modules 71 85 72 define( 'PRBP_VERSION', '1.7. 1' );86 define( 'PRBP_VERSION', '1.7.3' ); 73 87 define( 'PRBP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 74 88 define( 'PRBP_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); -
priceblueprint-for-woocommerce/trunk/readme.txt
r3605639 r3625812 4 4 Requires at least: 6.0 5 5 Tested up to: 7.0 6 Stable tag: 1.7. 16 Stable tag: 1.7.3 7 7 Requires PHP: 7.4 8 8 License: GPL-2.0-or-later … … 84 84 == Changelog == 85 85 86 = 1.7.3 = 87 * Improved the Freemius connection prompt with a clearer explanation of what's shared and why. 88 * Reordered the welcome screen so trying the demo data comes first. 89 90 = 1.7.2 = 91 * Security: Add-to-cart validation now verifies every selected option against the blueprint's actual rule values — previously a tampered form submission could add a product to the cart without paying for its selected options. 92 * Fix: The plugin could not be deleted from the Plugins screen — uninstall.php referenced a Freemius helper that is unavailable during uninstall, causing a fatal error. Uninstall now also removes plugin options and correctly clears leftover transients. 93 * Fix: Fatal parse error on PHP 7.4 — a PHP 8-only union type in the gallery placeholder handler broke sites running the minimum supported PHP version. 94 * Fix: Pricing rules containing non-Latin characters (ö, ü, ł, Cyrillic, etc.) or quotes in term labels were corrupted on save due to missing slashing around the stored JSON. Affected blueprints need to be re-saved once after updating. 95 * Fix: Moving a blueprint to the Trash no longer wipes prices and attributes of linked products. Trashing now only unpublishes linked products (fully reversible); permanent deletion additionally unlinks them, leaving all other product data intact. 96 86 97 = 1.7.1 = 87 98 * Fix: "Import Demo Data" could fatal error (500) on some stores due to an unguarded wp_insert_term() call — now handled gracefully with a proper error message. -
priceblueprint-for-woocommerce/trunk/src/Admin/SaveHandler.php
r3597567 r3625812 69 69 } 70 70 71 update_post_meta( $post_id, 'prbp_template_rules', wp_json_encode( self::sanitizeSections( $sections ) ) ); 71 // wp_slash() — update_post_meta() unslashes its value, which would strip the 72 // backslashes wp_json_encode() uses for \uXXXX and \" escapes and corrupt the JSON. 73 update_post_meta( $post_id, 'prbp_template_rules', wp_slash( wp_json_encode( self::sanitizeSections( $sections ) ) ) ); 72 74 RulesCache::flush( $post_id ); 73 75 } -
priceblueprint-for-woocommerce/trunk/src/Ajax/ImportDemo.php
r3605639 r3625812 183 183 } 184 184 185 update_post_meta( $blueprint_id, 'prbp_template_rules', wp_json_encode( $rules ) ); 185 // wp_slash() — update_post_meta() unslashes, which would corrupt JSON escape sequences. 186 update_post_meta( $blueprint_id, 'prbp_template_rules', wp_slash( wp_json_encode( $rules ) ) ); 186 187 187 188 return $blueprint_id; -
priceblueprint-for-woocommerce/trunk/src/CPT/Blueprint.php
r3552429 r3625812 16 16 public static function register(): void { 17 17 add_action( 'init', [ self::class, 'registerCPT' ], 10 ); 18 add_action( 'before_delete_post', [ self::class, 'handle Removal' ], 10, 1 );19 add_action( 'wp_trash_post', [ self::class, 'handle Removal' ],10, 1 );18 add_action( 'before_delete_post', [ self::class, 'handleDelete' ], 10, 1 ); 19 add_action( 'wp_trash_post', [ self::class, 'handleTrash' ], 10, 1 ); 20 20 } 21 21 … … 49 49 50 50 /** 51 * When a blueprint is trashed or permanently deleted, move all linked products 52 * to draft and clear their price, attributes, and blueprint reference. 51 * Trashing a blueprint is reversible, so it must not destroy product data: 52 * linked products are only unpublished (draft). Prices, attributes, and the 53 * prbp_template_id link stay intact — restoring the blueprint from Trash lets 54 * the merchant simply republish the products. 53 55 * 54 56 * @param int $post_id 55 57 */ 56 public static function handle Removal( int $post_id ): void {58 public static function handleTrash( int $post_id ): void { 57 59 if ( get_post_type( $post_id ) !== 'price_blueprint' ) { 58 60 return; 59 61 } 60 62 61 $linked_ids = get_posts( [ 63 foreach ( self::linkedProductIds( $post_id ) as $product_id ) { 64 self::unpublishProduct( $product_id ); 65 } 66 } 67 68 /** 69 * Permanent deletion: unpublish linked products and remove the blueprint 70 * reference. Prices and attributes are deliberately left untouched — they 71 * may include data the merchant set manually for other purposes. 72 * 73 * @param int $post_id 74 */ 75 public static function handleDelete( int $post_id ): void { 76 if ( get_post_type( $post_id ) !== 'price_blueprint' ) { 77 return; 78 } 79 80 foreach ( self::linkedProductIds( $post_id ) as $product_id ) { 81 self::unpublishProduct( $product_id ); 82 delete_post_meta( $product_id, 'prbp_template_id' ); 83 } 84 } 85 86 /** 87 * @param int $post_id Blueprint post ID. 88 * @return int[] Product IDs linked to this blueprint. 89 */ 90 private static function linkedProductIds( int $post_id ): array { 91 return get_posts( [ 62 92 'post_type' => 'product', 63 93 'post_status' => 'any', … … 72 102 ], 73 103 ] ); 104 } 74 105 75 foreach ( $linked_ids as $product_id ) { 76 $product = wc_get_product( $product_id ); 77 if ( ! $product ) { 78 continue; 79 } 106 /** 107 * Move a published product to draft without touching any of its other data. 108 * 109 * @param int $product_id 110 */ 111 private static function unpublishProduct( int $product_id ): void { 112 if ( 'publish' !== get_post_status( $product_id ) ) { 113 return; 114 } 80 115 81 $product->set_status( 'draft' ); 82 $product->set_regular_price( '' ); 83 $product->set_price( '' ); 84 $product->set_sale_price( '' ); 85 $product->set_attributes( [] ); 86 $product->save(); 116 $product = wc_get_product( $product_id ); 117 if ( ! $product ) { 118 return; 119 } 87 120 88 delete_post_meta( $product_id, 'prbp_template_id' );89 }121 $product->set_status( 'draft' ); 122 $product->save(); 90 123 } 91 124 } -
priceblueprint-for-woocommerce/trunk/src/Cart/CartItemMeta.php
r3587316 r3625812 24 24 25 25 /** 26 * Server-side validation: all attributes from the template must be selected. 26 * Server-side validation: every attribute from the template must carry a 27 * selection that matches one of that attribute's rule value slugs. A bare 28 * "not empty" check is not enough — a tampered slug would silently skip 29 * rule matching in capture() and the option's price would never be added. 27 30 * 28 31 * @param bool $passed … … 52 55 53 56 foreach ( $attributes as $attr ) { 54 if ( empty( $selections[ $attr ] ) ) { 57 $slug = $selections[ $attr ] ?? ''; 58 $is_valid = false; 59 60 if ( '' !== $slug ) { 61 foreach ( $rules as $rule ) { 62 if ( $rule['attribute'] === $attr 63 && in_array( $slug, (array) ( $rule['value_slugs'] ?? [] ), true ) 64 ) { 65 $is_valid = true; 66 break; 67 } 68 } 69 } 70 71 if ( ! $is_valid ) { 55 72 wc_add_notice( 56 73 sprintf( -
priceblueprint-for-woocommerce/trunk/src/Frontend/ProductPage.php
r3597567 r3625812 181 181 * inside .woocommerce-product-gallery__wrapper; it offsets flexslider's horizontal 182 182 * slide positions, making the main viewport appear blank. 183 */ 184 public static function suppressGalleryPlaceholder( string $html, int|string $attachment_id ): string { 183 * 184 * @param string $html 185 * @param int|string $attachment_id 186 */ 187 public static function suppressGalleryPlaceholder( string $html, $attachment_id ): string { 185 188 if ( $attachment_id ) { 186 189 return $html; -
priceblueprint-for-woocommerce/trunk/templates/welcome-screen.php
r3561869 r3625812 47 47 </div> 48 48 49 <div class="prbp-welcome-actions"> 50 <a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=price_blueprint' ) ); ?>" 51 class="prbp-welcome-cta"> 52 <?php esc_html_e( 'Create your first Blueprint', 'priceblueprint-for-woocommerce' ); ?> → 53 </a> 54 <a href="<?php echo esc_url( admin_url( 'edit.php?post_type=price_blueprint' ) ); ?>" 55 class="prbp-welcome-skip"> 56 <?php esc_html_e( 'Skip for now', 'priceblueprint-for-woocommerce' ); ?> 57 </a> 58 </div> 49 <div class="prbp-welcome-getstarted"> 59 50 60 <div class="prbp-welcome-demo" id="prbp-demo-wrap"> 61 <p class="prbp-welcome-steps-title"><?php esc_html_e( 'Try it first', 'priceblueprint-for-woocommerce' ); ?></p> 62 <div class="prbp-welcome-demo-row"> 63 <p class="prbp-welcome-demo-desc"><?php esc_html_e( 'Import a sample blueprint and product to see how PriceBlueprint works before building your own.', 'priceblueprint-for-woocommerce' ); ?></p> 64 <button type="button" id="prbp-import-demo" class="prbp-welcome-demo-btn"> 65 <?php esc_html_e( 'Import Demo Data', 'priceblueprint-for-woocommerce' ); ?> 66 </button> 51 <div class="prbp-welcome-demo" id="prbp-demo-wrap"> 52 <p class="prbp-welcome-steps-title"><?php esc_html_e( 'Try it first', 'priceblueprint-for-woocommerce' ); ?></p> 53 <div class="prbp-welcome-demo-row"> 54 <p class="prbp-welcome-demo-desc"><?php esc_html_e( 'Import a sample blueprint and product to see how PriceBlueprint works before building your own — the fastest way to see a real price change on your storefront.', 'priceblueprint-for-woocommerce' ); ?></p> 55 <button type="button" id="prbp-import-demo" class="prbp-welcome-btn-primary"> 56 <?php esc_html_e( 'Import Demo Data', 'priceblueprint-for-woocommerce' ); ?> → 57 </button> 58 </div> 67 59 </div> 60 61 <div class="prbp-welcome-divider"></div> 62 63 <div class="prbp-welcome-secondary-row"> 64 <span class="prbp-welcome-secondary-label"><?php esc_html_e( 'Prefer to start from scratch?', 'priceblueprint-for-woocommerce' ); ?></span> 65 <a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=price_blueprint' ) ); ?>" 66 class="prbp-welcome-btn-secondary"> 67 <?php esc_html_e( 'Create your first Blueprint', 'priceblueprint-for-woocommerce' ); ?> 68 </a> 69 <a href="<?php echo esc_url( admin_url( 'edit.php?post_type=price_blueprint' ) ); ?>" 70 class="prbp-welcome-skip"> 71 <?php esc_html_e( 'Skip for now', 'priceblueprint-for-woocommerce' ); ?> 72 </a> 73 </div> 74 68 75 </div> 69 76 -
priceblueprint-for-woocommerce/trunk/uninstall.php
r3557112 r3625812 28 28 delete_post_meta_by_key( 'prbp_template_id' ); 29 29 30 // Delete plugin options. 31 delete_option( 'prbp_show_welcome' ); 32 delete_option( 'prbp_demo_blueprint_id' ); 33 delete_option( 'prbp_demo_product_id' ); 34 30 35 // Clean up any lingering transients (save error transients use user IDs). 31 36 global $wpdb; 32 37 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 33 38 $wpdb->query( 34 "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient prbp_save_error_%' OR option_name LIKE '_transient_timeoutprbp_save_error_%'"39 "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_prbp_save_error_%' OR option_name LIKE '_transient_timeout_prbp_save_error_%'" 35 40 ); 36 41 37 // Not like register_uninstall_hook(), you do NOT have to use a static function. 38 pfwp_fs()->add_action('after_uninstall', 'pfwp_fs_uninstall_cleanup'); 42 // NOTE: Do not call pfwp_fs() here — the main plugin file is NOT loaded when 43 // WordPress runs uninstall.php, so the Freemius helper does not exist and the 44 // call would fatal, blocking plugin deletion. Freemius account data is shared 45 // SDK state and is left untouched intentionally.
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)