{"id":395035,"date":"2026-10-30T05:27:02","date_gmt":"2026-10-30T05:27:02","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/mix-and-max-box-builder\/"},"modified":"2026-10-30T11:36:10","modified_gmt":"2026-10-30T11:36:10","slug":"productboxes-mix-and-match-box-builder","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/productboxes-mix-and-match-box-builder\/","author":23571936,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.0.0","stable_tag":"1.0.0","tested":"7.1.4","requires":"6.6","requires_php":"7.4","requires_plugins":null,"header_name":"ProductBoxes - Mix & Match Box Builder for WooCommerce","header_author":"bestwebexpert","header_description":"Let shoppers pick products into a build-your-own box and unlock tiered discounts as the box fills. For WooCommerce.","assets_banners_color":"f2e9de","last_updated":"2026-10-30 11:36:10","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/bestwebexpert.com\/productboxes","header_author_uri":"https:\/\/bestwebexpert.com\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":64,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.0":{"tag":"1.0.0","author":"bestwpexpert","date":"2026-10-30 11:36:10","revision":3775095}},"upgrade_notice":{"1.0.0":"<p>Initial release.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.gif":{"filename":"icon-128x128.gif","revision":3774446,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.gif":{"filename":"icon-256x256.gif","revision":3774446,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3774427,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3774427,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{"blueprint.json":{"filename":"blueprint.json","revision":3775095,"resolution":false,"location":"assets","locale":"","contents":"{\"$schema\":\"https:\\\/\\\/playground.wordpress.net\\\/blueprint-schema.json\",\"meta\":{\"title\":\"ProductBoxes demo store\",\"description\":\"A WooCommerce store with three live box campaigns, illustrated products and 60 days of sample analytics. Use the orange toolbar button to switch between the storefront and the admin.\",\"author\":\"Razibul-Hasan\",\"categories\":[\"WooCommerce\"]},\"landingPage\":\"\\\/\",\"preferredVersions\":{\"php\":\"8.3\",\"wp\":\"latest\"},\"login\":true,\"steps\":[{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"woocommerce\"},\"options\":{\"activate\":true}},{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"productboxes-mix-and-match-box-builder\"},\"options\":{\"activate\":true}},{\"step\":\"mkdir\",\"path\":\"\\\/wordpress\\\/wp-content\\\/mu-plugins\"},{\"step\":\"writeFile\",\"path\":\"\\\/wordpress\\\/wp-content\\\/mu-plugins\\\/mmbb-demo-bar.php\",\"data\":\"<?php\\n\\\/**\\n * Plugin Name: ProductBoxes live preview switcher\\n * Description: Live preview only. A toolbar button that moves between the demo store and the ProductBoxes admin.\\n *\\n * The blueprint installs this as a must-use plugin. It is never shipped in the plugin zip.\\n *\\n * @package MixMax\\\\BoxBuilder\\n *\\\/\\n\\ndefined( 'ABSPATH' ) || exit;\\n\\nadd_action( 'admin_bar_menu', 'mmbb_demo_bar', 100 );\\nadd_action( 'wp_enqueue_scripts', 'mmbb_demo_bar_style' );\\nadd_action( 'admin_enqueue_scripts', 'mmbb_demo_bar_style' );\\n\\n\\\/**\\n * Add the switcher and its menu of demo screens.\\n *\\n * @param WP_Admin_Bar $bar Toolbar.\\n *\\\/\\nfunction mmbb_demo_bar( $bar ) {\\n\\tif ( ! current_user_can( 'manage_woocommerce' ) ) { \\\/\\\/ phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce's capability, the same one the plugin's screens use.\\n\\t\\treturn;\\n\\t}\\n\\t$links = wp_parse_args(\\n\\t\\tget_option( 'mmbb_demo_links', array() ),\\n\\t\\tarray(\\n\\t\\t\\t'holiday'  => 0,\\n\\t\\t\\t'spa'      => 0,\\n\\t\\t\\t'trio'     => 0,\\n\\t\\t\\t'campaign' => 0,\\n\\t\\t)\\n\\t);\\n\\n\\t$bar->add_node(\\n\\t\\tarray(\\n\\t\\t\\t'id'    => 'mmbb-demo',\\n\\t\\t\\t'title' => is_admin() ? 'View the demo store &rarr;' : 'Open the box builder admin &rarr;',\\n\\t\\t\\t'href'  => is_admin() ? home_url( '\\\/' ) : admin_url( 'admin.php?page=mmbb' ),\\n\\t\\t)\\n\\t);\\n\\n\\t$groups = array(\\n\\t\\t'mmbb-demo-store' => array(\\n\\t\\t\\tarray( 'holiday', 'Storefront: Holiday treat box (tiered discounts)', $links['holiday'] ? get_permalink( $links['holiday'] ) : '' ),\\n\\t\\t\\tarray( 'spa', 'Storefront: Spa kit (fixed sizes, dark theme)', $links['spa'] ? get_permalink( $links['spa'] ) : '' ),\\n\\t\\t\\tarray( 'trio', 'Storefront: Tea trio (bundle price)', $links['trio'] ? get_permalink( $links['trio'] ) : '' ),\\n\\t\\t\\tarray( 'cart', 'Cart', function_exists( 'wc_get_cart_url' ) ? wc_get_cart_url() : '' ),\\n\\t\\t),\\n\\t\\t'mmbb-demo-admin' => array(\\n\\t\\t\\tarray( 'dashboard', 'Admin: Dashboard', admin_url( 'admin.php?page=mmbb' ) ),\\n\\t\\t\\tarray( 'campaigns', 'Admin: Campaigns', admin_url( 'admin.php?page=mmbb-campaigns' ) ),\\n\\t\\t\\tarray( 'edit', 'Admin: Edit the holiday box', $links['campaign'] ? admin_url( 'admin.php?page=mmbb-campaigns&action=edit&id=' . (int) $links['campaign'] ) : '' ),\\n\\t\\t\\tarray( 'new', 'Admin: Create a campaign', admin_url( 'admin.php?page=mmbb-campaigns&action=new' ) ),\\n\\t\\t\\tarray( 'analytics', 'Admin: Analytics', admin_url( 'admin.php?page=mmbb-analytics' ) ),\\n\\t\\t\\tarray( 'settings', 'Admin: Settings', admin_url( 'admin.php?page=mmbb-settings' ) ),\\n\\t\\t),\\n\\t);\\n\\tforeach ( $groups as $group => $items ) {\\n\\t\\t$bar->add_group(\\n\\t\\t\\tarray(\\n\\t\\t\\t\\t'parent' => 'mmbb-demo',\\n\\t\\t\\t\\t'id'     => $group,\\n\\t\\t\\t\\t'meta'   => array( 'class' => 'mmbb-demo-admin' === $group ? 'ab-sub-secondary' : '' ),\\n\\t\\t\\t)\\n\\t\\t);\\n\\t\\tforeach ( $items as $item ) {\\n\\t\\t\\tif ( '' !== $item[2] ) {\\n\\t\\t\\t\\t$bar->add_node(\\n\\t\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t\\t'parent' => $group,\\n\\t\\t\\t\\t\\t\\t'id'     => 'mmbb-demo-' . $item[0],\\n\\t\\t\\t\\t\\t\\t'title'  => esc_html( $item[1] ),\\n\\t\\t\\t\\t\\t\\t'href'   => $item[2],\\n\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t);\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n}\\n\\n\\\/** Make the switcher stand out, on phones too. *\\\/\\nfunction mmbb_demo_bar_style() {\\n\\tif ( ! is_admin_bar_showing() ) {\\n\\t\\treturn;\\n\\t}\\n\\twp_add_inline_style(\\n\\t\\t'admin-bar',\\n\\t\\t'#wpadminbar #wp-admin-bar-mmbb-demo > .ab-item{background:#f04e12;color:#fff;font-weight:600;padding:0 14px}'\\n\\t\\t. '#wpadminbar #wp-admin-bar-mmbb-demo:hover > .ab-item,#wpadminbar #wp-admin-bar-mmbb-demo > .ab-item:focus{background:#c93d0b;color:#fff}'\\n\\t\\t. '@media screen and (max-width:782px){#wpadminbar li#wp-admin-bar-mmbb-demo{display:block}'\\n\\t\\t. '#wpadminbar #wp-admin-bar-mmbb-demo > .ab-item{width:auto;font-size:14px;line-height:46px;height:46px;padding:0 12px;text-indent:0}}'\\n\\t);\\n}\\n\"},{\"step\":\"writeFile\",\"path\":\"\\\/tmp\\\/mmbb-demo-content.php\",\"data\":\"<?php\\n\\\/**\\n * Demo store for the WordPress.org live preview (WordPress Playground).\\n *\\n * The blueprint built by tools\\\/blueprint\\\/build.mjs inlines this file, writes\\n * it to \\\/tmp and calls mmbb_demo_seed() once WooCommerce and the plugin are\\n * active. It is never shipped in the plugin zip.\\n *\\n * @package MixMax\\\\BoxBuilder\\n *\\\/\\n\\ndefined( 'ABSPATH' ) || exit;\\n\\n\\\/\\\/ phpcs:disable WordPress.DB.DirectDatabaseQuery -- Seeds the plugin's own analytics tables once, in a throwaway Playground site.\\n\\nuse MixMax\\\\BoxBuilder\\\\Analytics\\\\Query;\\nuse MixMax\\\\BoxBuilder\\\\Campaigns\\\\Campaign_Repository;\\nuse MixMax\\\\BoxBuilder\\\\Campaigns\\\\Config;\\nuse MixMax\\\\BoxBuilder\\\\Campaigns\\\\Publisher;\\n\\n\\\/** Build the whole demo: store settings, products, campaigns, pages and analytics. *\\\/\\nfunction mmbb_demo_seed() {\\n\\trequire_once ABSPATH . 'wp-admin\\\/includes\\\/file.php';\\n\\trequire_once ABSPATH . 'wp-admin\\\/includes\\\/media.php';\\n\\trequire_once ABSPATH . 'wp-admin\\\/includes\\\/image.php';\\n\\n\\t$admins = get_users(\\n\\t\\tarray(\\n\\t\\t\\t'role'   => 'administrator',\\n\\t\\t\\t'number' => 1,\\n\\t\\t\\t'fields' => 'ID',\\n\\t\\t)\\n\\t);\\n\\twp_set_current_user( $admins ? (int) $admins[0] : 1 );\\n\\n\\tmmbb_demo_store();\\n\\t$products  = mmbb_demo_products();\\n\\t$campaigns = mmbb_demo_campaigns( $products );\\n\\tmmbb_demo_navigation( $campaigns );\\n\\tmmbb_demo_analytics( $campaigns, $products );\\n\\n\\tupdate_option(\\n\\t\\t'mmbb_demo_links',\\n\\t\\tarray(\\n\\t\\t\\t'holiday'  => $campaigns['holiday']['page_id'],\\n\\t\\t\\t'spa'      => $campaigns['spa']['page_id'],\\n\\t\\t\\t'trio'     => $campaigns['trio']['page_id'],\\n\\t\\t\\t'campaign' => $campaigns['holiday']['id'],\\n\\t\\t),\\n\\t\\tfalse\\n\\t);\\n}\\n\\n\\\/** WooCommerce settings for a ready-to-browse store, and an empty site. *\\\/\\nfunction mmbb_demo_store() {\\n\\tglobal $wp_rewrite;\\n\\t$wp_rewrite->set_permalink_structure( '\\\/%postname%\\\/' );\\n\\tflush_rewrite_rules();\\n\\n\\t$options = array(\\n\\t\\t'blogname'                                 => 'Northwind Pantry',\\n\\t\\t'blogdescription'                          => 'Small-batch treats, tea and candles',\\n\\t\\t'woocommerce_store_address'                => '120 Harbor Street',\\n\\t\\t'woocommerce_store_city'                   => 'San Francisco',\\n\\t\\t'woocommerce_store_postcode'               => '94105',\\n\\t\\t'woocommerce_default_country'              => 'US:CA',\\n\\t\\t'woocommerce_currency'                     => 'USD',\\n\\t\\t'woocommerce_coming_soon'                  => 'no',\\n\\t\\t'woocommerce_store_pages_only'             => 'no',\\n\\t\\t'woocommerce_onboarding_profile'           => array(\\n\\t\\t\\t'skipped'   => true,\\n\\t\\t\\t'completed' => true,\\n\\t\\t),\\n\\t\\t'woocommerce_task_list_hidden'             => 'yes',\\n\\t\\t'woocommerce_task_list_complete'           => 'yes',\\n\\t\\t'woocommerce_show_marketplace_suggestions' => 'no',\\n\\t\\t'woocommerce_allow_tracking'               => 'no',\\n\\t\\t'woocommerce_manage_stock'                 => 'yes',\\n\\t\\t'woocommerce_cod_settings'                 => array(\\n\\t\\t\\t'enabled'     => 'yes',\\n\\t\\t\\t'title'       => 'Cash on delivery',\\n\\t\\t\\t'description' => 'Demo store: no payment is taken.',\\n\\t\\t),\\n\\t\\t'mmbb_setup_test_done'                     => 1,\\n\\t);\\n\\tforeach ( $options as $name => $value ) {\\n\\t\\tupdate_option( $name, $value );\\n\\t}\\n\\tdelete_transient( '_wc_activation_redirect' );\\n\\n\\tif ( class_exists( 'WC_Install' ) && wc_get_page_id( 'cart' ) < 1 ) {\\n\\t\\tWC_Install::create_pages();\\n\\t}\\n\\tif ( class_exists( 'WC_Admin_Notices' ) ) {\\n\\t\\tWC_Admin_Notices::remove_all_notices();\\n\\t}\\n\\n\\t\\\/\\\/ The default \\\"Hello world!\\\" post and \\\"Sample Page\\\".\\n\\tforeach ( array( 1, 2 ) as $post_id ) {\\n\\t\\tif ( get_post( $post_id ) ) {\\n\\t\\t\\twp_delete_post( $post_id, true );\\n\\t\\t}\\n\\t}\\n}\\n\\n\\\/**\\n * Create the categories and products, each with an illustrated image.\\n *\\n * @return array Product rows from mmbb_demo_catalog(), keyed by slug, with `id` added.\\n *\\\/\\nfunction mmbb_demo_products() {\\n\\t$categories = array();\\n\\tforeach ( array(\\n\\t\\t'sweets'  => 'Sweets',\\n\\t\\t'tea'     => 'Tea & honey',\\n\\t\\t'candles' => 'Candles & bath',\\n\\t) as $slug => $name ) {\\n\\t\\t$term                = wp_insert_term( $name, 'product_cat', array( 'slug' => $slug ) );\\n\\t\\t$categories[ $slug ] = is_wp_error( $term ) ? (int) $term->get_error_data( 'term_exists' ) : (int) $term['term_id'];\\n\\t}\\n\\n\\t$products = array();\\n\\tforeach ( mmbb_demo_catalog() as $slug => $row ) {\\n\\t\\t$product = new WC_Product_Simple();\\n\\t\\t$product->set_name( $row['name'] );\\n\\t\\t$product->set_slug( $slug );\\n\\t\\t$product->set_status( 'publish' );\\n\\t\\t$product->set_regular_price( $row['price'] );\\n\\t\\tif ( isset( $row['sale'] ) ) {\\n\\t\\t\\t$product->set_sale_price( $row['sale'] );\\n\\t\\t}\\n\\t\\t$product->set_short_description( $row['blurb'] );\\n\\t\\t$product->set_category_ids( array( $categories[ $row['cat'] ] ) );\\n\\t\\tif ( isset( $row['stock'] ) ) {\\n\\t\\t\\t$product->set_manage_stock( true );\\n\\t\\t\\t$product->set_stock_quantity( $row['stock'] );\\n\\t\\t\\t$product->set_stock_status( $row['stock'] > 0 ? 'instock' : 'outofstock' );\\n\\t\\t}\\n\\t\\t$image = mmbb_demo_image( $slug, $row['name'], $row['art'], isset( $row['swap'] ) ? $row['swap'] : array() );\\n\\t\\tif ( $image ) {\\n\\t\\t\\t$product->set_image_id( $image );\\n\\t\\t}\\n\\t\\t$row['id']         = $product->save();\\n\\t\\t$products[ $slug ] = $row;\\n\\t}\\n\\treturn $products;\\n}\\n\\n\\\/**\\n * The demo catalog. `art` names a ProductArt drawing; `swap` recolours it.\\n * `weight` is how often shoppers pick it in the seeded analytics.\\n *\\n * @return array[]\\n *\\\/\\nfunction mmbb_demo_catalog() {\\n\\treturn array(\\n\\t\\t'brown-butter-chip'      => array(\\n\\t\\t\\t'name'   => 'Brown Butter Chip',\\n\\t\\t\\t'cat'    => 'sweets',\\n\\t\\t\\t'art'    => 'cookie',\\n\\t\\t\\t'price'  => '4.50',\\n\\t\\t\\t'weight' => 10,\\n\\t\\t\\t'blurb'  => 'Chewy brown butter cookie with dark chocolate chips. 85 g.',\\n\\t\\t),\\n\\t\\t'raspberry-macaron-duo'  => array(\\n\\t\\t\\t'name'   => 'Raspberry Macaron Duo',\\n\\t\\t\\t'cat'    => 'sweets',\\n\\t\\t\\t'art'    => 'macaron',\\n\\t\\t\\t'price'  => '5.00',\\n\\t\\t\\t'weight' => 8,\\n\\t\\t\\t'blurb'  => 'Two almond shells filled with raspberry ganache.',\\n\\t\\t),\\n\\t\\t'glazed-ring'            => array(\\n\\t\\t\\t'name'   => 'Glazed Ring',\\n\\t\\t\\t'cat'    => 'sweets',\\n\\t\\t\\t'art'    => 'donut',\\n\\t\\t\\t'price'  => '3.50',\\n\\t\\t\\t'weight' => 6,\\n\\t\\t\\t'blurb'  => 'Brioche ring with a pink berry glaze and sprinkles.',\\n\\t\\t),\\n\\t\\t'dark-bar-70'            => array(\\n\\t\\t\\t'name'   => '70% Dark Bar',\\n\\t\\t\\t'cat'    => 'sweets',\\n\\t\\t\\t'art'    => 'chocolate',\\n\\t\\t\\t'price'  => '5.50',\\n\\t\\t\\t'weight' => 5,\\n\\t\\t\\t'blurb'  => 'Single-origin dark chocolate. 80 g.',\\n\\t\\t),\\n\\t\\t'double-chocolate-chunk' => array(\\n\\t\\t\\t'name'   => 'Double Chocolate Chunk',\\n\\t\\t\\t'cat'    => 'sweets',\\n\\t\\t\\t'art'    => 'cookie',\\n\\t\\t\\t'price'  => '4.75',\\n\\t\\t\\t'weight' => 7,\\n\\t\\t\\t'swap'   => array(\\n\\t\\t\\t\\t'#c98a4b' => '#6e4227',\\n\\t\\t\\t\\t'#5a3519' => '#f3e3cc',\\n\\t\\t\\t\\t'#f3e3cc' => '#ecdcd0',\\n\\t\\t\\t),\\n\\t\\t\\t'blurb'  => 'Cocoa dough with white chocolate chunks. 85 g.',\\n\\t\\t),\\n\\t\\t'pistachio-macaron-duo'  => array(\\n\\t\\t\\t'name'   => 'Pistachio Macaron Duo',\\n\\t\\t\\t'cat'    => 'sweets',\\n\\t\\t\\t'art'    => 'macaron',\\n\\t\\t\\t'price'  => '5.00',\\n\\t\\t\\t'weight' => 6,\\n\\t\\t\\t'swap'   => array(\\n\\t\\t\\t\\t'#e88aa6' => '#9fc48c',\\n\\t\\t\\t\\t'#f6dfe6' => '#e6efdc',\\n\\t\\t\\t),\\n\\t\\t\\t'blurb'  => 'Two almond shells filled with roasted pistachio cream.',\\n\\t\\t),\\n\\t\\t'strawberry-sprinkle'    => array(\\n\\t\\t\\t'name'   => 'Strawberry Sprinkle Ring',\\n\\t\\t\\t'cat'    => 'sweets',\\n\\t\\t\\t'art'    => 'donut',\\n\\t\\t\\t'price'  => '3.75',\\n\\t\\t\\t'weight' => 4,\\n\\t\\t\\t'swap'   => array(\\n\\t\\t\\t\\t'#d9577a' => '#f3a6bf',\\n\\t\\t\\t\\t'#fbe6d4' => '#fde8ee',\\n\\t\\t\\t),\\n\\t\\t\\t'blurb'  => 'Brioche ring with strawberry icing.',\\n\\t\\t),\\n\\t\\t'sea-salt-milk-bar'      => array(\\n\\t\\t\\t'name'   => 'Sea Salt Milk Bar',\\n\\t\\t\\t'cat'    => 'sweets',\\n\\t\\t\\t'art'    => 'chocolate',\\n\\t\\t\\t'price'  => '5.50',\\n\\t\\t\\t'sale'   => '4.40',\\n\\t\\t\\t'weight' => 4,\\n\\t\\t\\t'swap'   => array(\\n\\t\\t\\t\\t'#6b3e26' => '#a86b45',\\n\\t\\t\\t\\t'#4e2b19' => '#865230',\\n\\t\\t\\t\\t'#eadcd2' => '#f4e6da',\\n\\t\\t\\t),\\n\\t\\t\\t'blurb'  => 'Creamy milk chocolate with flakes of sea salt. 80 g.',\\n\\t\\t),\\n\\t\\t'smoky-earl-grey'        => array(\\n\\t\\t\\t'name'   => 'Smoky Earl Grey',\\n\\t\\t\\t'cat'    => 'tea',\\n\\t\\t\\t'art'    => 'tea',\\n\\t\\t\\t'price'  => '8.75',\\n\\t\\t\\t'sale'   => '7.00',\\n\\t\\t\\t'weight' => 7,\\n\\t\\t\\t'blurb'  => 'Lapsang and bergamot loose-leaf tea. 50 g tin.',\\n\\t\\t),\\n\\t\\t'wildflower-honey'       => array(\\n\\t\\t\\t'name'   => 'Wildflower Honey',\\n\\t\\t\\t'cat'    => 'tea',\\n\\t\\t\\t'art'    => 'honey',\\n\\t\\t\\t'price'  => '8.00',\\n\\t\\t\\t'stock'  => 5,\\n\\t\\t\\t'weight' => 6,\\n\\t\\t\\t'blurb'  => 'Raw spring honey from local hives. 250 g.',\\n\\t\\t),\\n\\t\\t'jasmine-green'          => array(\\n\\t\\t\\t'name'   => 'Jasmine Green',\\n\\t\\t\\t'cat'    => 'tea',\\n\\t\\t\\t'art'    => 'tea',\\n\\t\\t\\t'price'  => '7.50',\\n\\t\\t\\t'weight' => 5,\\n\\t\\t\\t'swap'   => array(\\n\\t\\t\\t\\t'#3f6b5c' => '#3d5a85',\\n\\t\\t\\t\\t'#2f5246' => '#2c4263',\\n\\t\\t\\t\\t'#efe8d8' => '#e6ebf3',\\n\\t\\t\\t),\\n\\t\\t\\t'blurb'  => 'Green tea scented with jasmine blossom. 50 g tin.',\\n\\t\\t),\\n\\t\\t'orange-blossom-honey'   => array(\\n\\t\\t\\t'name'   => 'Orange Blossom Honey',\\n\\t\\t\\t'cat'    => 'tea',\\n\\t\\t\\t'art'    => 'honey',\\n\\t\\t\\t'price'  => '8.50',\\n\\t\\t\\t'weight' => 4,\\n\\t\\t\\t'swap'   => array(\\n\\t\\t\\t\\t'#e6a52c' => '#ef8a3a',\\n\\t\\t\\t\\t'#fbefd0' => '#fde7d6',\\n\\t\\t\\t),\\n\\t\\t\\t'blurb'  => 'Light, floral honey from orange groves. 250 g.',\\n\\t\\t),\\n\\t\\t'fig-cedar-candle'       => array(\\n\\t\\t\\t'name'   => 'Fig & Cedar Candle',\\n\\t\\t\\t'cat'    => 'candles',\\n\\t\\t\\t'art'    => 'candle',\\n\\t\\t\\t'price'  => '12.00',\\n\\t\\t\\t'sale'   => '9.00',\\n\\t\\t\\t'stock'  => 3,\\n\\t\\t\\t'weight' => 6,\\n\\t\\t\\t'blurb'  => 'Soy candle with notes of green fig and cedarwood. 120 g.',\\n\\t\\t),\\n\\t\\t'sea-salt-candle'        => array(\\n\\t\\t\\t'name'   => 'Sea Salt Candle',\\n\\t\\t\\t'cat'    => 'candles',\\n\\t\\t\\t'art'    => 'candle',\\n\\t\\t\\t'price'  => '11.00',\\n\\t\\t\\t'weight' => 4,\\n\\t\\t\\t'swap'   => array(\\n\\t\\t\\t\\t'#7d9a7a' => '#6f9cc0',\\n\\t\\t\\t\\t'#e5ece4' => '#e4ecf2',\\n\\t\\t\\t),\\n\\t\\t\\t'blurb'  => 'Soy candle with sea salt and driftwood. 120 g.',\\n\\t\\t),\\n\\t\\t'oat-milk-soap'          => array(\\n\\t\\t\\t'name'   => 'Oat Milk Soap',\\n\\t\\t\\t'cat'    => 'candles',\\n\\t\\t\\t'art'    => 'soap',\\n\\t\\t\\t'price'  => '6.50',\\n\\t\\t\\t'weight' => 5,\\n\\t\\t\\t'blurb'  => 'Gentle bar soap with colloidal oats. 100 g.',\\n\\t\\t),\\n\\t\\t'lavender-soap'          => array(\\n\\t\\t\\t'name'   => 'Lavender Soap',\\n\\t\\t\\t'cat'    => 'candles',\\n\\t\\t\\t'art'    => 'soap',\\n\\t\\t\\t'price'  => '6.50',\\n\\t\\t\\t'weight' => 4,\\n\\t\\t\\t'swap'   => array(\\n\\t\\t\\t\\t'#a9c6dc' => '#b7a5d8',\\n\\t\\t\\t\\t'#d6e6f1' => '#e3dbf2',\\n\\t\\t\\t\\t'#e4ecf2' => '#eee8f6',\\n\\t\\t\\t),\\n\\t\\t\\t'blurb'  => 'Bar soap with French lavender oil. 100 g.',\\n\\t\\t),\\n\\t\\t'rose-clay-soap'         => array(\\n\\t\\t\\t'name'   => 'Rose Clay Soap',\\n\\t\\t\\t'cat'    => 'candles',\\n\\t\\t\\t'art'    => 'soap',\\n\\t\\t\\t'price'  => '6.50',\\n\\t\\t\\t'stock'  => 0,\\n\\t\\t\\t'weight' => 2,\\n\\t\\t\\t'swap'   => array(\\n\\t\\t\\t\\t'#a9c6dc' => '#e3a4a4',\\n\\t\\t\\t\\t'#d6e6f1' => '#f3d4d4',\\n\\t\\t\\t\\t'#e4ecf2' => '#f8e8e6',\\n\\t\\t\\t),\\n\\t\\t\\t'blurb'  => 'Bar soap with pink kaolin clay. 100 g.',\\n\\t\\t),\\n\\t);\\n}\\n\\n\\\/**\\n * Create the campaigns through the plugin's own repository and publisher, so\\n * they are exactly what the wizard would save.\\n *\\n * @param array $products Products from mmbb_demo_products().\\n * @return array Keyed by campaign: { id, page_id, config }.\\n *\\\/\\nfunction mmbb_demo_campaigns( array $products ) {\\n\\t$ids = static function ( $cats, $skip = array() ) use ( $products ) {\\n\\t\\t$list = array();\\n\\t\\tforeach ( $products as $slug => $row ) {\\n\\t\\t\\tif ( in_array( $row['cat'], (array) $cats, true ) && ! in_array( $slug, $skip, true ) ) {\\n\\t\\t\\t\\t$list[] = $row['id'];\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\treturn $list;\\n\\t};\\n\\t$day = static function ( $days ) {\\n\\t\\treturn gmdate( 'Y-m-d\\\\TH:i:s\\\\Z', time() + $days * DAY_IN_SECONDS );\\n\\t};\\n\\n\\t$plans = array(\\n\\t\\t'holiday' => array(\\n\\t\\t\\t'template' => 'holiday',\\n\\t\\t\\t'title'    => 'Holiday Treat Box',\\n\\t\\t\\t'slug'     => 'holiday-treat-box',\\n\\t\\t\\t'config'   => array(\\n\\t\\t\\t\\t'products' => array(\\n\\t\\t\\t\\t\\t'source' => 'manual',\\n\\t\\t\\t\\t\\t'ids'    => $ids( array( 'sweets', 'tea', 'candles' ) ),\\n\\t\\t\\t\\t\\t'order'  => 'manual',\\n\\t\\t\\t\\t),\\n\\t\\t\\t\\t'schedule' => array(\\n\\t\\t\\t\\t\\t'end'            => $day( 12 ),\\n\\t\\t\\t\\t\\t'countdown_days' => 14,\\n\\t\\t\\t\\t),\\n\\t\\t\\t\\t'extras'   => array( 'gift_message' => true ),\\n\\t\\t\\t\\t'design'   => array(\\n\\t\\t\\t\\t\\t'headline' => 'Mix 12 treats. Save 25%.',\\n\\t\\t\\t\\t\\t'intro'    => 'Pick anything below and we pack it in a gift-ready box. The more you add, the more you save. Discounts apply automatically.',\\n\\t\\t\\t\\t\\t'sticker'  => 'Holiday drop',\\n\\t\\t\\t\\t),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t\\t'spa'     => array(\\n\\t\\t\\t'template' => 'spa',\\n\\t\\t\\t'title'    => 'Build-a-Spa Kit',\\n\\t\\t\\t'slug'     => 'spa-kit',\\n\\t\\t\\t'config'   => array(\\n\\t\\t\\t\\t'products' => array(\\n\\t\\t\\t\\t\\t'source' => 'manual',\\n\\t\\t\\t\\t\\t'ids'    => $ids( array( 'candles', 'tea' ) ),\\n\\t\\t\\t\\t\\t'order'  => 'manual',\\n\\t\\t\\t\\t),\\n\\t\\t\\t\\t'design'   => array(\\n\\t\\t\\t\\t\\t'headline' => 'Build your own spa kit',\\n\\t\\t\\t\\t\\t'intro'    => 'Choose 4 or 6 calming favourites. The bigger kit saves you 15%, and we can gift-wrap it.',\\n\\t\\t\\t\\t\\t'sticker'  => 'Save up to 15%',\\n\\t\\t\\t\\t\\t'theme'    => 'dark',\\n\\t\\t\\t\\t),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t\\t'trio'    => array(\\n\\t\\t\\t'template' => 'snack',\\n\\t\\t\\t'title'    => 'Tea Lover Trio',\\n\\t\\t\\t'slug'     => 'tea-trio',\\n\\t\\t\\t'config'   => array(\\n\\t\\t\\t\\t'box'      => array(\\n\\t\\t\\t\\t\\t'capacity'         => 3,\\n\\t\\t\\t\\t\\t'min_items'        => 3,\\n\\t\\t\\t\\t\\t'allow_duplicates' => true,\\n\\t\\t\\t\\t\\t'max_per_product'  => 3,\\n\\t\\t\\t\\t),\\n\\t\\t\\t\\t'bundle'   => array(\\n\\t\\t\\t\\t\\t'qty'   => 3,\\n\\t\\t\\t\\t\\t'price' => 18,\\n\\t\\t\\t\\t),\\n\\t\\t\\t\\t'products' => array(\\n\\t\\t\\t\\t\\t'source' => 'manual',\\n\\t\\t\\t\\t\\t'ids'    => array_merge( $ids( 'tea' ), $ids( 'sweets', array( 'strawberry-sprinkle', 'sea-salt-milk-bar', 'glazed-ring' ) ) ),\\n\\t\\t\\t\\t\\t'order'  => 'manual',\\n\\t\\t\\t\\t),\\n\\t\\t\\t\\t'schedule' => array(\\n\\t\\t\\t\\t\\t'end'            => $day( 3 ),\\n\\t\\t\\t\\t\\t'countdown_days' => 7,\\n\\t\\t\\t\\t),\\n\\t\\t\\t\\t'design'   => array(\\n\\t\\t\\t\\t\\t'headline' => 'Any 3 for one bundle price',\\n\\t\\t\\t\\t\\t'intro'    => 'Two teas and a honey, or a cookie for every cup. Pick any three and the bundle price applies.',\\n\\t\\t\\t\\t\\t'sticker'  => 'Bundle deal',\\n\\t\\t\\t\\t),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t\\t'gift'    => array(\\n\\t\\t\\t'template' => 'gift',\\n\\t\\t\\t'title'    => 'Valentine\\u2019s Gift Box',\\n\\t\\t\\t'slug'     => '',\\n\\t\\t\\t'config'   => array(\\n\\t\\t\\t\\t'products' => array(\\n\\t\\t\\t\\t\\t'source' => 'manual',\\n\\t\\t\\t\\t\\t'ids'    => $ids( array( 'candles', 'tea' ) ),\\n\\t\\t\\t\\t\\t'order'  => 'manual',\\n\\t\\t\\t\\t),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t);\\n\\n\\t$repo      = new Campaign_Repository();\\n\\t$publisher = new Publisher( $repo );\\n\\t$campaigns = array();\\n\\t\\\/\\\/ Created oldest first, so the list (newest first) opens on the draft.\\n\\tforeach ( $plans as $key => $plan ) {\\n\\t\\t$campaign = $repo->create_from_template( $plan['template'], $plan['title'] );\\n\\t\\tif ( is_wp_error( $campaign ) ) {\\n\\t\\t\\tcontinue;\\n\\t\\t}\\n\\t\\t$campaign = $repo->save( $campaign['id'], $plan['config'] );\\n\\t\\t$page_id  = 0;\\n\\t\\tif ( '' !== $plan['slug'] && ! Config::validate( $campaign['config'] ) ) {\\n\\t\\t\\t$published = $publisher->publish(\\n\\t\\t\\t\\t$campaign['id'],\\n\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t'placement'  => 'new_page',\\n\\t\\t\\t\\t\\t'page_title' => $plan['title'],\\n\\t\\t\\t\\t\\t'page_slug'  => $plan['slug'],\\n\\t\\t\\t\\t)\\n\\t\\t\\t);\\n\\t\\t\\tif ( ! is_wp_error( $published ) ) {\\n\\t\\t\\t\\t$campaign = $published;\\n\\t\\t\\t\\t$page_id  = (int) $campaign['config']['placement']['page_id'];\\n\\t\\t\\t\\tmmbb_demo_wide_page( $page_id, $campaign['id'] );\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\t$campaigns[ $key ] = array(\\n\\t\\t\\t'id'      => (int) $campaign['id'],\\n\\t\\t\\t'page_id' => $page_id,\\n\\t\\t\\t'config'  => $campaign['config'],\\n\\t\\t);\\n\\t}\\n\\n\\tif ( $campaigns['holiday']['page_id'] ) {\\n\\t\\tupdate_option( 'show_on_front', 'page' );\\n\\t\\tupdate_option( 'page_on_front', $campaigns['holiday']['page_id'] );\\n\\t}\\n\\treturn $campaigns;\\n}\\n\\n\\\/**\\n * Show the builder wide, under the theme header but without a page title.\\n *\\n * @param int $page_id     Page id.\\n * @param int $campaign_id Campaign id.\\n *\\\/\\nfunction mmbb_demo_wide_page( $page_id, $campaign_id ) {\\n\\twp_update_post(\\n\\t\\tarray(\\n\\t\\t\\t'ID'           => $page_id,\\n\\t\\t\\t'post_content' => '<!-- wp:mmbb\\\/box-builder {\\\"campaignId\\\":' . (int) $campaign_id . ',\\\"align\\\":\\\"wide\\\"} \\\/-->',\\n\\t\\t)\\n\\t);\\n\\tif ( array_key_exists( 'page-no-title', wp_get_theme()->get_page_templates() ) ) {\\n\\t\\tupdate_post_meta( $page_id, '_wp_page_template', 'page-no-title' );\\n\\t}\\n}\\n\\n\\\/**\\n * A header menu with the three boxes. Block themes fall back to the newest menu.\\n *\\n * @param array $campaigns Campaigns from mmbb_demo_campaigns().\\n *\\\/\\nfunction mmbb_demo_navigation( array $campaigns ) {\\n\\t$links   = array(\\n\\t\\tarray( 'Shop', wc_get_page_id( 'shop' ) ),\\n\\t\\tarray( 'Holiday box', $campaigns['holiday']['page_id'] ),\\n\\t\\tarray( 'Spa kit', $campaigns['spa']['page_id'] ),\\n\\t\\tarray( 'Tea trio', $campaigns['trio']['page_id'] ),\\n\\t);\\n\\t$content = '';\\n\\tforeach ( $links as $link ) {\\n\\t\\tif ( $link[1] > 0 ) {\\n\\t\\t\\t$content .= '<!-- wp:navigation-link ' . wp_json_encode(\\n\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t'label' => $link[0],\\n\\t\\t\\t\\t\\t'type'  => 'page',\\n\\t\\t\\t\\t\\t'id'    => (int) $link[1],\\n\\t\\t\\t\\t\\t'url'   => get_permalink( $link[1] ),\\n\\t\\t\\t\\t\\t'kind'  => 'post-type',\\n\\t\\t\\t\\t)\\n\\t\\t\\t) . ' \\\/-->';\\n\\t\\t}\\n\\t}\\n\\twp_insert_post(\\n\\t\\tarray(\\n\\t\\t\\t'post_type'    => 'wp_navigation',\\n\\t\\t\\t'post_status'  => 'publish',\\n\\t\\t\\t'post_title'   => 'Store menu',\\n\\t\\t\\t'post_content' => wp_slash( $content ),\\n\\t\\t)\\n\\t);\\n}\\n\\n\\\/**\\n * Fill 60 days of sample analytics so the Dashboard and Analytics screens have\\n * something to show: daily funnel rows plus a sales ledger with made-up order ids.\\n *\\n * @param array $campaigns Campaigns from mmbb_demo_campaigns().\\n * @param array $products  Products from mmbb_demo_products().\\n *\\\/\\nfunction mmbb_demo_analytics( array $campaigns, array $products ) {\\n\\tglobal $wpdb;\\n\\n\\t$weights = array();\\n\\tforeach ( $products as $row ) {\\n\\t\\t$weights[ $row['id'] ] = $row['weight'];\\n\\t}\\n\\n\\t\\\/\\\/ Boxes a day at the start, growth over the period, conversion, average unit price, and how boxes split across tiers.\\n\\t$plans = array(\\n\\t\\t'holiday' => array( 6.5, 0.9, 0.085, 5.1, array( 3, 3.5, 3.5 ) ),\\n\\t\\t'spa'     => array( 3.5, 0.4, 0.072, 8.2, array( 5.5, 4.5 ) ),\\n\\t\\t'trio'    => array( 2.5, 0.2, 0.064, 7.4, array( 1 ) ),\\n\\t);\\n\\n\\t$days     = 60;\\n\\t$today    = new DateTimeImmutable( 'today', wp_timezone() );\\n\\t$order_id = 900000;\\n\\t$sold     = array();\\n\\tforeach ( $plans as $key => $plan ) {\\n\\t\\tif ( empty( $campaigns[ $key ]['page_id'] ) ) {\\n\\t\\t\\tcontinue;\\n\\t\\t}\\n\\t\\tlist( $base, $growth, $conversion, $unit, $split ) = $plan;\\n\\n\\t\\t$config = $campaigns[ $key ]['config'];\\n\\t\\t$tiers  = 'tiered' === $config['type'] ? $config['tiers'] : $config['sizes'];\\n\\t\\tif ( 'bundle' === $config['type'] ) {\\n\\t\\t\\t$tiers = array(\\n\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t'qty'           => $config['bundle']['qty'],\\n\\t\\t\\t\\t\\t'discount_type' => 'box_price',\\n\\t\\t\\t\\t\\t'value'         => $config['bundle']['price'],\\n\\t\\t\\t\\t),\\n\\t\\t\\t);\\n\\t\\t}\\n\\t\\t$pool = array_intersect_key( $weights, array_flip( $config['products']['ids'] ) );\\n\\n\\t\\tfor ( $i = $days; $i >= 0; $i-- ) {\\n\\t\\t\\t$date    = $today->modify( '-' . $i . ' days' );\\n\\t\\t\\t$weekend = (int) $date->format( 'N' ) >= 6 ? 1.3 : 1;\\n\\t\\t\\t\\\/\\\/ Today is still in progress.\\n\\t\\t\\t$share = 0 === $i ? 0.45 : 1;\\n\\t\\t\\t$boxes = (int) round( $base * ( 1 + $growth * ( $days - $i ) \\\/ $days ) * $weekend * $share * wp_rand( 75, 125 ) \\\/ 100 );\\n\\t\\t\\t$views = (int) round( max( 1, $boxes ) \\\/ ( $conversion * wp_rand( 85, 115 ) \\\/ 100 ) );\\n\\n\\t\\t\\t$picks = array();\\n\\t\\t\\t$left  = $boxes;\\n\\t\\t\\tforeach ( $tiers as $index => $tier ) {\\n\\t\\t\\t\\t$count = count( $tiers ) - 1 === $index ? $left : min( $left, (int) round( $boxes * $split[ $index ] \\\/ array_sum( $split ) ) );\\n\\t\\t\\t\\t$left -= $count;\\n\\t\\t\\t\\tif ( $count < 1 ) {\\n\\t\\t\\t\\t\\tcontinue;\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\t$items    = $count * (int) $tier['qty'];\\n\\t\\t\\t\\t$subtotal = $items * $unit * wp_rand( 92, 108 ) \\\/ 100;\\n\\t\\t\\t\\tif ( 'box_price' === $tier['discount_type'] ) {\\n\\t\\t\\t\\t\\t$value = $count * (float) $tier['value'];\\n\\t\\t\\t\\t} else {\\n\\t\\t\\t\\t\\t$value = $subtotal * ( 1 - (float) $tier['value'] \\\/ 100 );\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\t$wpdb->insert(\\n\\t\\t\\t\\t\\t$wpdb->prefix . 'mmbb_purchases',\\n\\t\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t\\t'order_id'    => ++$order_id,\\n\\t\\t\\t\\t\\t\\t'line_id'     => 1,\\n\\t\\t\\t\\t\\t\\t'campaign_id' => $campaigns[ $key ]['id'],\\n\\t\\t\\t\\t\\t\\t'day'         => $date->format( 'Y-m-d' ),\\n\\t\\t\\t\\t\\t\\t'boxes'       => $count,\\n\\t\\t\\t\\t\\t\\t'items'       => $items,\\n\\t\\t\\t\\t\\t\\t'tier'        => (int) $tier['qty'],\\n\\t\\t\\t\\t\\t\\t'tier_data'   => wp_json_encode(\\n\\t\\t\\t\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t\\t\\t\\t'qty'           => (int) $tier['qty'],\\n\\t\\t\\t\\t\\t\\t\\t\\t'discount_type' => $tier['discount_type'],\\n\\t\\t\\t\\t\\t\\t\\t\\t'value'         => $tier['value'],\\n\\t\\t\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t\\t\\t),\\n\\t\\t\\t\\t\\t\\t'value'       => round( $value, 2 ),\\n\\t\\t\\t\\t\\t\\t'discount'    => round( max( 0, $subtotal - $value ), 2 ),\\n\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t);\\n\\t\\t\\t\\tfor ( $n = 0; $n < $items; $n++ ) {\\n\\t\\t\\t\\t\\t$id           = mmbb_demo_weighted( $pool );\\n\\t\\t\\t\\t\\t$picks[ $id ] = isset( $picks[ $id ] ) ? $picks[ $id ] + 1 : 1;\\n\\t\\t\\t\\t\\t$sold[ $id ]  = isset( $sold[ $id ] ) ? $sold[ $id ] + 1 : 1;\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\t\\t\\tksort( $picks );\\n\\n\\t\\t\\t$started = (int) round( $views * wp_rand( 38, 48 ) \\\/ 100 );\\n\\t\\t\\t$wpdb->insert(\\n\\t\\t\\t\\t$wpdb->prefix . 'mmbb_stats_daily',\\n\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t'day'          => $date->format( 'Y-m-d' ),\\n\\t\\t\\t\\t\\t'campaign_id'  => $campaigns[ $key ]['id'],\\n\\t\\t\\t\\t\\t'views'        => $views,\\n\\t\\t\\t\\t\\t'started'      => $started,\\n\\t\\t\\t\\t\\t'tier_reached' => max( $boxes, (int) round( $started * wp_rand( 50, 62 ) \\\/ 100 ) ),\\n\\t\\t\\t\\t\\t'carts'        => (int) round( $boxes * wp_rand( 125, 145 ) \\\/ 100 ),\\n\\t\\t\\t\\t\\t'products'     => wp_json_encode( (object) $picks ),\\n\\t\\t\\t\\t)\\n\\t\\t\\t);\\n\\t\\t}\\n\\t}\\n\\n\\t\\\/\\\/ Shop pages sort by popularity, so match it to the picks.\\n\\tforeach ( $sold as $id => $count ) {\\n\\t\\t$product = wc_get_product( $id );\\n\\t\\tif ( $product ) {\\n\\t\\t\\t$product->set_total_sales( (int) round( $count \\\/ 3 ) );\\n\\t\\t\\t$product->save();\\n\\t\\t}\\n\\t}\\n\\tQuery::bust();\\n}\\n\\n\\\/**\\n * Pick a key at random, in proportion to its weight.\\n *\\n * @param array $weights Key => weight.\\n * @return int|string\\n *\\\/\\nfunction mmbb_demo_weighted( array $weights ) {\\n\\t$roll = wp_rand( 1, max( 1, (int) array_sum( $weights ) ) );\\n\\tforeach ( $weights as $key => $weight ) {\\n\\t\\t$roll -= $weight;\\n\\t\\tif ( $roll <= 0 ) {\\n\\t\\t\\treturn $key;\\n\\t\\t}\\n\\t}\\n\\treturn key( $weights );\\n}\\n\\n\\\/**\\n * Draw a product image and add it to the media library.\\n *\\n * @param string $slug  File name.\\n * @param string $title Attachment title.\\n * @param string $kind  Drawing from mmbb_demo_art().\\n * @param array  $swap  Colour replacements, '#from' => '#to'.\\n * @return int Attachment id, or 0 when GD is missing or the upload fails.\\n *\\\/\\nfunction mmbb_demo_image( $slug, $title, $kind, array $swap ) {\\n\\tif ( ! function_exists( 'imagecreatetruecolor' ) ) {\\n\\t\\treturn 0;\\n\\t}\\n\\t$art = mmbb_demo_art();\\n\\t$art = $art[ $kind ];\\n\\n\\t\\\/\\\/ Drawn at twice the size and scaled down, which smooths GD's hard edges.\\n\\t$scale = 12;\\n\\t$size  = 100 * $scale;\\n\\t$img   = imagecreatetruecolor( $size, $size );\\n\\t$color = static function ( $hex ) use ( $img, $swap ) {\\n\\t\\t$hex = isset( $swap[ $hex ] ) ? $swap[ $hex ] : $hex;\\n\\t\\treturn imagecolorallocate( $img, hexdec( substr( $hex, 1, 2 ) ), hexdec( substr( $hex, 3, 2 ) ), hexdec( substr( $hex, 5, 2 ) ) );\\n\\t};\\n\\t$px    = static function ( $value ) use ( $scale ) {\\n\\t\\treturn (int) round( $value * $scale );\\n\\t};\\n\\n\\timagefilledrectangle( $img, 0, 0, $size - 1, $size - 1, $color( $art['bg'] ) );\\n\\tforeach ( $art['shapes'] as $shape ) {\\n\\t\\t$fill = $color( end( $shape ) );\\n\\t\\tswitch ( $shape[0] ) {\\n\\t\\t\\tcase 'circle':\\n\\t\\t\\t\\timagefilledellipse( $img, $px( $shape[1] ), $px( $shape[2] ), $px( 2 * $shape[3] ), $px( 2 * $shape[3] ), $fill );\\n\\t\\t\\t\\tbreak;\\n\\t\\t\\tcase 'rect':\\n\\t\\t\\t\\tmmbb_demo_rounded( $img, $px( $shape[1] ), $px( $shape[2] ), $px( $shape[3] ), $px( $shape[4] ), $px( $shape[5] ), $fill );\\n\\t\\t\\t\\tbreak;\\n\\t\\t\\tcase 'line':\\n\\t\\t\\t\\t\\\/\\\/ Axis-aligned stroke: x1, y1, x2, y2, width.\\n\\t\\t\\t\\t$half = $shape[5] \\\/ 2;\\n\\t\\t\\t\\timagefilledrectangle( $img, $px( $shape[1] - ( $shape[1] === $shape[3] ? $half : 0 ) ), $px( $shape[2] - ( $shape[2] === $shape[4] ? $half : 0 ) ), $px( $shape[3] + ( $shape[1] === $shape[3] ? $half : 0 ) ), $px( $shape[4] + ( $shape[2] === $shape[4] ? $half : 0 ) ), $fill );\\n\\t\\t\\t\\tbreak;\\n\\t\\t\\tcase 'sprinkle':\\n\\t\\t\\t\\t\\\/\\\/ A rounded bar: x, y, width, height, rotated by an angle around a point.\\n\\t\\t\\t\\tlist( , $x, $y, $w, $h, $angle, $ox, $oy ) = $shape;\\n\\t\\t\\t\\t$ends                                      = array();\\n\\t\\t\\t\\tforeach ( array( $x + $h \\\/ 2, $x + $w - $h \\\/ 2 ) as $ex ) {\\n\\t\\t\\t\\t\\t$rad    = deg2rad( $angle );\\n\\t\\t\\t\\t\\t$ey     = $y + $h \\\/ 2;\\n\\t\\t\\t\\t\\t$ends[] = array(\\n\\t\\t\\t\\t\\t\\t$ox + ( $ex - $ox ) * cos( $rad ) - ( $ey - $oy ) * sin( $rad ),\\n\\t\\t\\t\\t\\t\\t$oy + ( $ex - $ox ) * sin( $rad ) + ( $ey - $oy ) * cos( $rad ),\\n\\t\\t\\t\\t\\t);\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\timagesetthickness( $img, $px( $h ) );\\n\\t\\t\\t\\timageline( $img, $px( $ends[0][0] ), $px( $ends[0][1] ), $px( $ends[1][0] ), $px( $ends[1][1] ), $fill );\\n\\t\\t\\t\\timagesetthickness( $img, 1 );\\n\\t\\t\\t\\tforeach ( $ends as $end ) {\\n\\t\\t\\t\\t\\timagefilledellipse( $img, $px( $end[0] ), $px( $end[1] ), $px( $h ), $px( $h ), $fill );\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\tbreak;\\n\\t\\t\\tcase 'flame':\\n\\t\\t\\t\\t$points = array();\\n\\t\\t\\t\\tforeach ( mmbb_demo_flame() as $point ) {\\n\\t\\t\\t\\t\\t$points[] = $px( $point[0] );\\n\\t\\t\\t\\t\\t$points[] = $px( $point[1] );\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\\/\\\/ Playground runs PHP 8, where the point count is optional (and deprecated since 8.1).\\n\\t\\t\\t\\timagefilledpolygon( $img, $points, $fill ); \\\/\\\/ phpcs:ignore PHPCompatibility.FunctionUse.RequiredToOptionalFunctionParameters\\n\\t\\t\\t\\tbreak;\\n\\t\\t}\\n\\t}\\n\\n\\t$out = imagecreatetruecolor( 600, 600 );\\n\\timagecopyresampled( $out, $img, 0, 0, 0, 0, 600, 600, $size, $size );\\n\\t$file = wp_tempnam( $slug . '.png' );\\n\\timagepng( $out, $file, 6 );\\n\\n\\t$id = media_handle_sideload(\\n\\t\\tarray(\\n\\t\\t\\t'name'     => $slug . '.png',\\n\\t\\t\\t'tmp_name' => $file,\\n\\t\\t),\\n\\t\\t0,\\n\\t\\t$title\\n\\t);\\n\\tif ( is_wp_error( $id ) ) {\\n\\t\\twp_delete_file( $file );\\n\\t\\treturn 0;\\n\\t}\\n\\tupdate_post_meta( $id, '_wp_attachment_image_alt', $title );\\n\\treturn (int) $id;\\n}\\n\\n\\\/**\\n * A filled rounded rectangle in pixels.\\n *\\n * @param resource|\\\\GdImage $img    Image.\\n * @param int               $x      Left.\\n * @param int               $y      Top.\\n * @param int               $w      Width.\\n * @param int               $h      Height.\\n * @param int               $r      Corner radius.\\n * @param int               $fill   Colour.\\n *\\\/\\nfunction mmbb_demo_rounded( $img, $x, $y, $w, $h, $r, $fill ) {\\n\\t$r = min( $r, (int) floor( $w \\\/ 2 ), (int) floor( $h \\\/ 2 ) );\\n\\tif ( $r < 1 ) {\\n\\t\\timagefilledrectangle( $img, $x, $y, $x + $w - 1, $y + $h - 1, $fill );\\n\\t\\treturn;\\n\\t}\\n\\timagefilledrectangle( $img, $x + $r, $y, $x + $w - $r - 1, $y + $h - 1, $fill );\\n\\timagefilledrectangle( $img, $x, $y + $r, $x + $w - 1, $y + $h - $r - 1, $fill );\\n\\tforeach ( array( array( $x + $r, $y + $r ), array( $x + $w - $r - 1, $y + $r ), array( $x + $r, $y + $h - $r - 1 ), array( $x + $w - $r - 1, $y + $h - $r - 1 ) ) as $corner ) {\\n\\t\\timagefilledellipse( $img, $corner[0], $corner[1], 2 * $r, 2 * $r, $fill );\\n\\t}\\n}\\n\\n\\\/**\\n * The candle flame from ProductArt, `M50 17c4 5 5 8 5 10a5 5 0 0 1-10 0c0-2 1-5 5-10z`, as points.\\n *\\n * @return array[] Points in the 100 \\u00d7 100 view box.\\n *\\\/\\nfunction mmbb_demo_flame() {\\n\\t$curve  = static function ( $p0, $p1, $p2, $p3, $t ) {\\n\\t\\t$u = 1 - $t;\\n\\t\\treturn array(\\n\\t\\t\\t$u * $u * $u * $p0[0] + 3 * $u * $u * $t * $p1[0] + 3 * $u * $t * $t * $p2[0] + $t * $t * $t * $p3[0],\\n\\t\\t\\t$u * $u * $u * $p0[1] + 3 * $u * $u * $t * $p1[1] + 3 * $u * $t * $t * $p2[1] + $t * $t * $t * $p3[1],\\n\\t\\t);\\n\\t};\\n\\t$points = array();\\n\\tfor ( $i = 0; $i <= 16; $i++ ) {\\n\\t\\t$points[] = $curve( array( 50, 17 ), array( 54, 22 ), array( 55, 25 ), array( 55, 27 ), $i \\\/ 16 );\\n\\t}\\n\\tfor ( $i = 1; $i < 16; $i++ ) {\\n\\t\\t$points[] = array( 50 + 5 * cos( M_PI * $i \\\/ 16 ), 27 + 5 * sin( M_PI * $i \\\/ 16 ) );\\n\\t}\\n\\tfor ( $i = 0; $i <= 16; $i++ ) {\\n\\t\\t$points[] = $curve( array( 45, 27 ), array( 45, 25 ), array( 46, 22 ), array( 50, 17 ), $i \\\/ 16 );\\n\\t}\\n\\treturn $points;\\n}\\n\\n\\\/**\\n * The drawings from src\\\/components\\\/ProductArt.js, in its 100 \\u00d7 100 view box.\\n * Shapes: circle (cx, cy, r), rect (x, y, w, h, radius), line (x1, y1, x2, y2, width),\\n * sprinkle (x, y, w, h, angle, origin x, origin y) and flame. The colour comes last.\\n *\\n * @return array[]\\n *\\\/\\nfunction mmbb_demo_art() {\\n\\treturn array(\\n\\t\\t'cookie'    => array(\\n\\t\\t\\t'bg'     => '#f3e3cc',\\n\\t\\t\\t'shapes' => array(\\n\\t\\t\\t\\tarray( 'circle', 50, 52, 27, '#c98a4b' ),\\n\\t\\t\\t\\tarray( 'circle', 41, 44, 3.5, '#5a3519' ),\\n\\t\\t\\t\\tarray( 'circle', 58, 47, 3, '#5a3519' ),\\n\\t\\t\\t\\tarray( 'circle', 47, 60, 3.5, '#5a3519' ),\\n\\t\\t\\t\\tarray( 'circle', 61, 61, 2.5, '#5a3519' ),\\n\\t\\t\\t\\tarray( 'circle', 37, 56, 2.2, '#5a3519' ),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t\\t'macaron'   => array(\\n\\t\\t\\t'bg'     => '#f6dfe6',\\n\\t\\t\\t'shapes' => array(\\n\\t\\t\\t\\tarray( 'rect', 26, 33, 48, 16, 8, '#e88aa6' ),\\n\\t\\t\\t\\tarray( 'rect', 29, 47, 42, 8, 4, '#fff3e8' ),\\n\\t\\t\\t\\tarray( 'rect', 26, 53, 48, 16, 8, '#e88aa6' ),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t\\t'candle'    => array(\\n\\t\\t\\t'bg'     => '#e5ece4',\\n\\t\\t\\t'shapes' => array(\\n\\t\\t\\t\\tarray( 'rect', 33, 38, 34, 36, 6, '#f7f3ea' ),\\n\\t\\t\\t\\tarray( 'rect', 33, 52, 34, 10, 0, '#7d9a7a' ),\\n\\t\\t\\t\\tarray( 'rect', 49, 30, 2, 9, 0, '#3b3b3b' ),\\n\\t\\t\\t\\tarray( 'flame', '#f2a541' ),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t\\t'soap'      => array(\\n\\t\\t\\t'bg'     => '#e4ecf2',\\n\\t\\t\\t'shapes' => array(\\n\\t\\t\\t\\tarray( 'rect', 24, 36, 52, 30, 12, '#a9c6dc' ),\\n\\t\\t\\t\\tarray( 'rect', 32, 42, 22, 6, 3, '#d6e6f1' ),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t\\t'tea'       => array(\\n\\t\\t\\t'bg'     => '#efe8d8',\\n\\t\\t\\t'shapes' => array(\\n\\t\\t\\t\\tarray( 'rect', 34, 30, 32, 44, 4, '#3f6b5c' ),\\n\\t\\t\\t\\tarray( 'rect', 32, 26, 36, 9, 3, '#2f5246' ),\\n\\t\\t\\t\\tarray( 'rect', 34, 46, 32, 14, 0, '#e8d9b5' ),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t\\t'donut'     => array(\\n\\t\\t\\t'bg'     => '#fbe6d4',\\n\\t\\t\\t'shapes' => array(\\n\\t\\t\\t\\tarray( 'circle', 50, 52, 26, '#e2a86b' ),\\n\\t\\t\\t\\tarray( 'circle', 50, 50, 22, '#d9577a' ),\\n\\t\\t\\t\\tarray( 'circle', 50, 50, 8, '#fbe6d4' ),\\n\\t\\t\\t\\tarray( 'sprinkle', 36, 38, 7, 2.5, -30, 39, 39, '#fff3e8' ),\\n\\t\\t\\t\\tarray( 'sprinkle', 58, 40, 7, 2.5, 40, 61, 41, '#ffd23f' ),\\n\\t\\t\\t\\tarray( 'sprinkle', 55, 60, 7, 2.5, -10, 58, 61, '#fff3e8' ),\\n\\t\\t\\t\\tarray( 'sprinkle', 37, 58, 7, 2.5, 25, 40, 59, '#ffd23f' ),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t\\t'chocolate' => array(\\n\\t\\t\\t'bg'     => '#eadcd2',\\n\\t\\t\\t'shapes' => array(\\n\\t\\t\\t\\tarray( 'rect', 28, 28, 44, 46, 4, '#6b3e26' ),\\n\\t\\t\\t\\tarray( 'line', 28, 43.3, 72, 43.3, 2, '#4e2b19' ),\\n\\t\\t\\t\\tarray( 'line', 28, 58.6, 72, 58.6, 2, '#4e2b19' ),\\n\\t\\t\\t\\tarray( 'line', 42.7, 28, 42.7, 74, 2, '#4e2b19' ),\\n\\t\\t\\t\\tarray( 'line', 57.3, 28, 57.3, 74, 2, '#4e2b19' ),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t\\t'honey'     => array(\\n\\t\\t\\t'bg'     => '#fbefd0',\\n\\t\\t\\t'shapes' => array(\\n\\t\\t\\t\\tarray( 'rect', 32, 36, 36, 38, 8, '#e6a52c' ),\\n\\t\\t\\t\\tarray( 'rect', 34, 28, 32, 10, 3, '#6b4a1f' ),\\n\\t\\t\\t\\tarray( 'rect', 38, 48, 24, 14, 3, '#fff7e0' ),\\n\\t\\t\\t),\\n\\t\\t),\\n\\t);\\n}\\n\"},{\"step\":\"runPHP\",\"code\":\"<?php require '\\\/wordpress\\\/wp-load.php'; require '\\\/tmp\\\/mmbb-demo-content.php'; mmbb_demo_seed();\"}]}"}},"all_blocks":{"mmbb\/box-builder":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"mmbb\/box-builder","title":"ProductBoxes Box Builder","category":"woocommerce","description":"Shows a box campaign: products, slots, tiers and the add-to-cart bar.","keywords":["box","bundle","gift","custom box"],"textdomain":"productboxes-mix-and-match-box-builder","attributes":{"campaignId":{"type":"number","default":0},"layout":{"type":"string","enum":["default","panel","bar","both"],"default":"default"},"columns":{"type":"number","default":0},"showHero":{"type":"boolean","default":true}},"supports":{"align":["wide","full"],"html":false},"editorScript":"file:.\/index.js","editorStyle":"file:.\/index.css","viewScript":"mmbb-storefront","style":"mmbb-storefront","render":"file:.\/render.php"}},"tagged_versions":["1.0.0"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3774415,"resolution":"1","location":"assets","locale":"","width":1600,"height":900},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3774415,"resolution":"2","location":"assets","locale":"","width":1600,"height":900},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3774415,"resolution":"3","location":"assets","locale":"","width":1600,"height":900},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3774415,"resolution":"4","location":"assets","locale":"","width":1600,"height":900},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3774415,"resolution":"5","location":"assets","locale":"","width":1600,"height":900},"screenshot-6.png":{"filename":"screenshot-6.png","revision":3774415,"resolution":"6","location":"assets","locale":"","width":1600,"height":900},"screenshot-7.png":{"filename":"screenshot-7.png","revision":3774415,"resolution":"7","location":"assets","locale":"","width":1600,"height":900},"screenshot-8.png":{"filename":"screenshot-8.png","revision":3774415,"resolution":"8","location":"assets","locale":"","width":1600,"height":900}},"screenshots":{"1":"WooCommerce mix and match box builder with tiered bundle discount progress.","2":"Gift box builder with box sizes, products, and gift messages.","3":"Fixed-price product bundles: select any three shirts for one price.","4":"Guided campaign creation with live shopper preview.","5":"Build your own box rules, item limits, discounts, and extras.","6":"Customize storefront layouts, labels, colors, and appearance.","7":"Product bundle analytics: revenue, funnel, and product activity.","8":"Campaign dashboard with statuses and performance."}},"plugin_section":[],"plugin_tags":[289638,266040,207261,263827,73298],"plugin_category":[45],"plugin_contributors":[284642],"plugin_business_model":[],"class_list":["post-395035","plugin","type-plugin","status-publish","hentry","plugin_tags-build-your-own-box","plugin_tags-bundle-discount","plugin_tags-gift-box","plugin_tags-mix-and-match","plugin_tags-product-bundles","plugin_category-ecommerce","plugin_contributors-bestwpexpert","plugin_committers-bestwpexpert"],"banners":{"banner":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/banner-772x250.png?rev=3774427","banner_2x":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/banner-1544x500.png?rev=3774427","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/icon-128x128.gif?rev=3774446","icon_2x":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/icon-256x256.gif?rev=3774446","generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/screenshot-1.png?rev=3774415","caption":"WooCommerce mix and match box builder with tiered bundle discount progress."},{"src":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/screenshot-2.png?rev=3774415","caption":"Gift box builder with box sizes, products, and gift messages."},{"src":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/screenshot-3.png?rev=3774415","caption":"Fixed-price product bundles: select any three shirts for one price."},{"src":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/screenshot-4.png?rev=3774415","caption":"Guided campaign creation with live shopper preview."},{"src":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/screenshot-5.png?rev=3774415","caption":"Build your own box rules, item limits, discounts, and extras."},{"src":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/screenshot-6.png?rev=3774415","caption":"Customize storefront layouts, labels, colors, and appearance."},{"src":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/screenshot-7.png?rev=3774415","caption":"Product bundle analytics: revenue, funnel, and product activity."},{"src":"https:\/\/ps.w.org\/productboxes-mix-and-match-box-builder\/assets\/screenshot-8.png?rev=3774415","caption":"Campaign dashboard with statuses and performance."}],"raw_content":"<!--section=description-->\n<p><strong>ProductBoxes - Mix &amp; Match Box Builder for WooCommerce<\/strong> is a free WooCommerce <strong>build your own box<\/strong> plugin. Let customers create personalized product bundles, fill a gift box with their favorite items, or choose a fixed-price box from products you select.<\/p>\n\n<p>Shoppers <strong>mix and match<\/strong> eligible products and variations, fill box slots, and add the box to their cart in one click. A live summary shows price, savings, and the next <strong>bundle discount<\/strong>. Prices are calculated on the server.<\/p>\n\n<p>Create snack boxes, bakery boxes, coffee samplers, beauty kits, or \"pick any 6\" offers. All features are free, with no premium upgrade, locked features, or external account.<\/p>\n\n<h4>Build your own box with three campaign types<\/h4>\n\n<ul>\n<li><strong>Tiered discount:<\/strong> 10% off 3 items, 15% off 5, or 25% off 12, with the next tier shown live.<\/li>\n<li><strong>Fixed box size:<\/strong> Offer boxes of 6, 12, or 24 items, each with its own discount or price.<\/li>\n<li><strong>Bundle price:<\/strong> Set one fixed price for an exact number of products, such as \"choose any 3 for 18.\"<\/li>\n<\/ul>\n\n<h4>Mix and match box builder features<\/h4>\n\n<ul>\n<li>Select eligible products manually, by category, or by product tag.<\/li>\n<li>Support WooCommerce simple products and individual product variations.<\/li>\n<li>Limit how many times a product may appear in the same box.<\/li>\n<li>Add an optional gift wrap fee and customer gift message.<\/li>\n<li>Set campaign start and end dates. A countdown appears only when a real end date is set.<\/li>\n<li>Show unavailable products as sold out or hide them from selection.<\/li>\n<li>Display a product grid with category filters, sorting, and a box panel or sticky summary bar.<\/li>\n<li>Show a mobile-friendly bottom sheet.<\/li>\n<li>Let shoppers edit their boxes from the cart.<\/li>\n<li>Support classic cart and checkout, plus WooCommerce Cart and Checkout blocks.<\/li>\n<\/ul>\n\n<h4>Manage product bundles and bundle discounts<\/h4>\n\n<ul>\n<li>Create campaigns using a guided editor with a live shopper preview.<\/li>\n<li>Add a box builder with a WordPress block or the <code>[mmbb_box id=\"12\"]<\/code> shortcode.<\/li>\n<li>Customize storefront layout, labels, colors, and appearance.<\/li>\n<li>View analytics for views, picks, conversions, revenue, discounts, and top products.<\/li>\n<li>Save box contents, discounts, and gift messages with WooCommerce orders and emails.<\/li>\n<li>Reduce stock for each item inside a purchased box when the order is paid.<\/li>\n<li>Support High-Performance Order Storage (HPOS).<\/li>\n<li>Keep optional analytics in your own WordPress database, without an external analytics service.<\/li>\n<\/ul>\n\n<h4>Watch the ProductBoxes video<\/h4>\n\n<p>https:\/\/www.youtube.com\/watch?v=7tlm_BwW9jI<\/p>\n\n<h4>Requirements<\/h4>\n\n<ul>\n<li>WordPress 6.6 or later<\/li>\n<li>WooCommerce 9.0 or later<\/li>\n<li>PHP 7.4 or later<\/li>\n<\/ul>\n\n<h3>External services<\/h3>\n\n<p>The plugin does not contact external services or use a CDN, remote fonts, telemetry, license checks, or update checks. Scripts, styles, and fonts are local. The YouTube video is embedded on the WordPress.org listing, not in the installed plugin.<\/p>\n\n<h3>Privacy<\/h3>\n\n<p>Optional builder analytics are stored in this site's database and record views, product picks, tier milestones, cart additions, and box purchases.<\/p>\n\n<ul>\n<li>A random session ID is stored in browser sessionStorage; no tracking cookie is set.<\/li>\n<li>Only a daily salted hash is stored; no IP or user agent is recorded. The changing daily salt prevents linking sessions across days.<\/li>\n<li>Settings and <code>mmbb_track_events<\/code> disable collection, including new purchases. Refund and cancellation corrections still apply.<\/li>\n<li>Raw events are deleted after the retention period (90 days by default). Daily totals and order-line sales records remain for reporting and late refunds.<\/li>\n<\/ul>\n\n<p>Gift messages are saved with WooCommerce order items. WordPress personal data tools export them and erase them when WooCommerce's order data removal setting permits.<\/p>\n\n<h3>Source code and build<\/h3>\n\n<p>JavaScript and CSS sources are in <code>src\/<\/code>, with build configuration in <code>package.json<\/code> and <code>webpack.config.js<\/code>. React and <code>@wordpress<\/code> packages are provided by WordPress and are not bundled.<\/p>\n\n<p>Build with Node.js 20.19 or later:<\/p>\n\n<pre><code>npm install\nnpm run build\n<\/code><\/pre>\n\n<p>Production assets are written to <code>build\/<\/code>. Composer is used only for development tools (coding standards and PHPUnit), not to run the plugin.<\/p>\n\n<p>Public repository: https:\/\/github.com\/Razibul-Hasan\/productboxes-mix-and-match-box-builder<\/p>\n\n<h3>Credits<\/h3>\n\n<p>The Figtree and Bricolage Grotesque fonts are bundled locally under the SIL Open Font License 1.1. License files: <code>assets\/fonts\/Figtree-OFL.txt<\/code> and <code>assets\/fonts\/BricolageGrotesque-OFL.txt<\/code>.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Install and activate WooCommerce 9.0 or later.<\/li>\n<li>Go to Plugins &gt; Add New, search for \"ProductBoxes\", and install it. Or upload <code>productboxes-mix-and-match-box-builder<\/code> to <code>\/wp-content\/plugins\/<\/code>.<\/li>\n<li>Activate ProductBoxes - Mix &amp; Match Box Builder for WooCommerce. Each multisite site receives its own defaults.<\/li>\n<li>Open ProductBoxes in the admin menu and select Create campaign.<\/li>\n<li>Publish a campaign on a page using the Box Builder block or <code>[mmbb_box id=\"12\"]<\/code> shortcode.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"how%20do%20i%20create%20a%20build%20your%20own%20box%20page%20in%20woocommerce%3F\"><h3>How do I create a build your own box page in WooCommerce?<\/h3><\/dt>\n<dd><p>Publish a campaign on a page with the Box Builder block or <code>[mmbb_box id=\"12\"]<\/code>. Shortcode options: <code>layout<\/code> (panel, bar, both), <code>columns<\/code> (3, 4, 5), and <code>hero<\/code> (true, false).<\/p><\/dd>\n<dt id=\"can%20customers%20mix%20and%20match%20products%20in%20the%20same%20box%3F\"><h3>Can customers mix and match products in the same box?<\/h3><\/dt>\n<dd><p>Yes. Shoppers can mix and match eligible simple products or variations, with optional per-product limits.<\/p><\/dd>\n<dt id=\"what%20product%20bundle%20and%20bundle%20discount%20types%20are%20supported%3F\"><h3>What product bundle and bundle discount types are supported?<\/h3><\/dt>\n<dd><p>Tiered discount, fixed box size, and fixed bundle price. Choose quantity-based discounts, selectable box sizes, or one price for an exact item count.<\/p><\/dd>\n<dt id=\"can%20i%20sell%20a%20gift%20box%20with%20wrapping%20and%20a%20message%3F\"><h3>Can I sell a gift box with wrapping and a message?<\/h3><\/dt>\n<dd><p>Yes. Campaigns can offer an optional gift wrap fee and gift message. The message appears with order details and emails.<\/p><\/dd>\n<dt id=\"can%20i%20sell%20a%20fixed-price%20mix%20and%20match%20box%2C%20such%20as%20any%203%20for%2018%3F\"><h3>Can I sell a fixed-price mix and match box, such as any 3 for 18?<\/h3><\/dt>\n<dd><p>Yes. Choose the bundle price campaign, set the item count and box price, and select eligible products.<\/p><\/dd>\n<dt id=\"does%20productboxes%20work%20with%20my%20woocommerce%20theme%3F\"><h3>Does ProductBoxes work with my WooCommerce theme?<\/h3><\/dt>\n<dd><p>The builder uses scoped styles and was tested with Twenty Twenty-Five and Storefront. On mobile, it uses two columns, a sticky summary bar, and a bottom sheet.<\/p><\/dd>\n<dt id=\"does%20it%20support%20woocommerce%20cart%20and%20checkout%20blocks%3F\"><h3>Does it support WooCommerce Cart and Checkout blocks?<\/h3><\/dt>\n<dd><p>Yes. Contents, discounts, messages, next-discount hints, and the Edit box link work in classic and block carts, with details saved on orders. Locked quantities in \"Box plus each item\" mode are fully supported in the classic cart.<\/p><\/dd>\n<dt id=\"does%20the%20box%20builder%20load%20on%20every%20page%3F\"><h3>Does the box builder load on every page?<\/h3><\/dt>\n<dd><p>By default, scripts and styles load only where a box builder is displayed. This can be changed under \"Load assets only on pages with a builder\" in Settings.<\/p><\/dd>\n<dt id=\"how%20are%20mixed%20tax%20classes%20handled%3F\"><h3>How are mixed tax classes handled?<\/h3><\/dt>\n<dd><p>By default, a box is one cart line taxed using the hidden box product's tax class (Standard unless changed). Choose \"Box plus each item\" under Cart settings when items need separate cart lines and their own tax classes.<\/p><\/dd>\n<dt id=\"what%20is%20the%20hidden%20box%20container%20product%3F\"><h3>What is the hidden Box container product?<\/h3><\/dt>\n<dd><p>Each published campaign has a hidden box product for the cart. ProductBoxes manages its name and price. Edit the campaign instead; deleting it also trashes the container product.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20woocommerce%20is%20inactive%3F\"><h3>What happens if WooCommerce is inactive?<\/h3><\/dt>\n<dd><p>ProductBoxes remains inactive without breaking the site. Users with plugin activation permissions see a dismissible admin notice.<\/p><\/dd>\n<dt id=\"does%20uninstalling%20delete%20campaigns%20or%20orders%3F\"><h3>Does uninstalling delete campaigns or orders?<\/h3><\/dt>\n<dd><p>Data remains after deactivation or uninstall unless \"Delete all plugin data\" is enabled in Settings. WooCommerce orders are never deleted. Multisite installations use separate settings per site, disabled by default.<\/p><\/dd>\n<dt id=\"does%20productboxes%20contact%20external%20services%20or%20track%20personal%20data%3F\"><h3>Does ProductBoxes contact external services or track personal data?<\/h3><\/dt>\n<dd><p>No. Optional analytics stay in your database, record no IP addresses, and can be disabled. See Privacy below.<\/p><\/dd>\n<dt id=\"is%20source%20code%20available%20for%20developers%3F\"><h3>Is source code available for developers?<\/h3><\/dt>\n<dd><p>Yes. See Source code and build below. Developer hooks use the <code>mmbb_<\/code> prefix:<\/p>\n\n<ul>\n<li><code>mmbb_campaign_config_defaults<\/code> (filter): default configuration for a blank campaign; sanitized again.<\/li>\n<li><code>mmbb_templates<\/code> (filter): campaign-picker templates with id, title, type_label, description, art, and partial config.<\/li>\n<li><code>mmbb_pricing_quote<\/code> (filter): calculated quote, campaign config, items, and options; amounts use integer minor units.<\/li>\n<li><code>mmbb_campaign_saved<\/code> (action): runs after save with campaign ID and sanitized config.<\/li>\n<li><code>mmbb_ensure_container_product<\/code> (filter): return a container product ID, <code>WP_Error<\/code> to stop publishing, or null; receives current value and campaign ID.<\/li>\n<li><code>mmbb_track_events<\/code> (filter): return false to disable analytics collection.<\/li>\n<\/ul><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release: tiered, fixed-size, and bundle-price campaigns; builder block and shortcode; classic and block cart\/checkout support; order details, emails, analytics, and settings.<\/li>\n<\/ul>","raw_excerpt":"Build your own box in WooCommerce with mix and match products, gift boxes, product bundles, and automatic bundle discounts.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/395035","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=395035"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/bestwpexpert"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=395035"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=395035"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=395035"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=395035"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=395035"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=395035"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}