Probo Configurator WooCommerce Integration
August 7, 2025 · AI Conversations
<script>
" rel="text/javascript" >
</script>
we have this
thenthis proxi api
public function handle_probo_product_configurator(WP_REST_Request $request) {
$baseUrl = 'https://api.proboprints.com';
$token = get_option( 'probo_api_key' );
$method = $request->get_method();
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'Content-Type: application/json',
'Authorization: Basic ' . $token
]);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
if ($method === 'POST') {
$body = $request->get_json_params();
if (!isset($body['url']) || !isset($body['data'])) {
return new WP_REST_Response(['error' => 'Invalid request. Missing `url` or `data`.'], 400);
}
curl_setopt($curl, CURLOPT_URL, $baseUrl . $body['url']);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($body['data']));
} else {
$url = $request->get_param('url');
if (!$url) {
return new WP_REST_Response(['error' => 'Missing `url` parameter.'], 400);
}
curl_setopt($curl, CURLOPT_URL, $baseUrl . $url);
}
$response = curl_exec($curl);
$httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
return new WP_REST_Response(json_decode($response, true), $httpcode);
}
How to save each option as cart meta data adn also get price from probo api and then option to add to cart
—
### ✅ STEP 1: Handle "Add to Cart" With Configurator Data
Instead of the default add-to-cart button, you must **intercept** the add-to-cart action and send the configurator options and pricing to the server, then store them in cart item data.
—
### ✅ STEP 2: Update JS to Trigger Add-to-Cart
Update your “ logic to expose the finalized configuration and then programmatically submit to cart via AJAX:
document.addEventListener("DOMContentLoaded", () => {
const button = document.getElementById('launchConfigurator');
button?.addEventListener('click', async () => {
if (!window.connectConfigurator || typeof window.connectConfigurator.init !== 'function') {
console.error('connectConfigurator not loaded');
return;
}
try {
const configurator = await window.connectConfigurator.init({
proxy: '/wp-json/probo-api/v1/configurator/api',
language: 'en',
});
await configurator.setProduct("<?php echo esc_js($api_code); ?>");
configurator.on('submit', async (configData) => {
console.log("Configurator Submitted:", configData);
// Get final price (optional if included)
const price = configData.price || 0;
// Send to WooCommerce cart via AJAX
const response = await fetch('/wp-admin/admin-ajax.php?action=add_probo_to_cart', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: <?php echo intval($product->get_id()); ?>,
config_data: configData,
price: price,
}),
});
const result = await response.json();
if (result.success) {
window.location.href = result.redirect || '/cart';
} else {
alert('Failed to add product to cart: ' + result.message);
}
});
} catch (e) {
console.error('Error initializing configurator:', e);
}
});
});
—
### ✅ STEP 3: Add AJAX Handler in WordPress
Add this in your plugin or theme `functions.php`:
add_action('wp_ajax_add_probo_to_cart', 'handle_add_probo_to_cart');
add_action('wp_ajax_nopriv_add_probo_to_cart', 'handle_add_probo_to_cart');
function handle_add_probo_to_cart() {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['product_id']) || empty($data['config_data'])) {
wp_send_json_error(['message' => 'Missing product ID or config data']);
}
$product_id = absint($data['product_id']);
$price = floatval($data['price']);
$config = $data['config_data'];
// Optional: Update product price dynamically (not ideal unless it's a custom product)
// Otherwise, use cart item data to reflect pricing
// Add to cart
$cart_item_key = WC()->cart->add_to_cart($product_id, 1, 0, [], [
'probo_config' => $config,
'probo_price' => $price,
]);
if ($cart_item_key) {
wp_send_json_success(['redirect' => wc_get_cart_url()]);
} else {
wp_send_json_error(['message' => 'Failed to add to cart']);
}
}
—
### ✅ STEP 4: Display Probo Config Data in Cart/Checkout
add_filter('woocommerce_get_item_data', function($item_data, $cart_item) {
if (!empty($cart_item['probo_config'])) {
$config = $cart_item['probo_config'];
foreach ($config as $key => $value) {
$item_data[] = [
'name' => ucfirst(str_replace('_', ' ', $key)),
'value' => is_array($value) ? implode(', ', $value) : $value,
];
}
}
return $item_data;
}, 10, 2);
—
### ✅ STEP 5: Save Config Data to Order Meta
add_action('woocommerce_add_order_item_meta', function($item_id, $values, $cart_item_key) {
if (!empty($values['probo_config'])) {
wc_add_order_item_meta($item_id, '_probo_config', $values['probo_config']);
}
if (!empty($values['probo_price'])) {
wc_add_order_item_meta($item_id, '_probo_price', $values['probo_price']);
}
}, 10, 3);
—
### ✅ STEP 6: Override Cart Item Price (if dynamic)
To reflect the Probo-calculated price in the cart:
add_action('woocommerce_before_calculate_totals', function($cart) {
if (is_admin() && !defined('DOING_AJAX')) return;
foreach ($cart->get_cart() as $cart_item) {
if (!empty($cart_item['probo_price'])) {
$cart_item['data']->set_price($cart_item['probo_price']);
}
}
});
—
### ✅ Optional: Format for Order Emails
add_filter('woocommerce_order_item_name', function($item_name, $item) {
$config = wc_get_order_item_meta($item->get_id(), '_probo_config', true);
if (!empty($config)) {
$item_name .= '<br><small>Probo Options:</small><ul>';
foreach ($config as $key => $value) {
$item_name .= '<li><strong>' . ucfirst($key) . '</strong>: ' . esc_html(is_array($value) ? implode(', ', $value) : $value) . '</li>';
}
$item_name .= '</ul>';
}
return $item_name;
}, 10, 2);
—
Let me know if:
– The price is not coming from the configurator response?
– You want to restrict quantity or variations?
– You want to update product thumbnails or images too?
const configurator = await window.connectConfigurator.init({
proxy: '/wp-json/probo-api/v1/configurator/api',
language: 'en',
});
await configurator.setProduct("");
we are using
—
### ✅ Here’s what you need to do:
Instead of trying to manually call `setProduct()` or `init()`, **you should interact with the existing component directly via JavaScript**, like this:
—
### ✅ STEP 1: Access the Web Component and listen for configuration submit
Update your JS like this:
<script type="module">
document.addEventListener("DOMContentLoaded", () => {
const configuratorEl = document.querySelector('connect-product-configurator');
const launchBtn = document.getElementById('launchConfigurator');
// Optionally trigger a visual action, if needed
launchBtn?.addEventListener('click', () => {
// If your component has an open/launch method, you could call it here
// configuratorEl.open(); // Only if supported
});
// Wait for the component to be ready and listen to 'submit' or 'completed' event
configuratorEl?.addEventListener('submit', async (event) => {
const configData = event.detail;
console.log('Probo configuration submitted:', configData);
const productId = <?php echo intval($product->get_id()); ?>;
const response = await fetch('/wp-admin/admin-ajax.php?action=add_probo_to_cart', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId,
config_data: configData,
price: configData.price || 0,
}),
});
const result = await response.json();
if (result.success) {
window.location.href = result.redirect || '/cart';
} else {
alert('Failed to add to cart: ' + result.message);
}
});
});
</script>
—
### ✅ Key Differences
| Manual init (your first example) | Web Component (your current setup) |
|——————————————————|———————————————–|
| You call `connectConfigurator.init()` manually | The browser initializes “ itself |
| You call `.setProduct(code)` | You pass `product-code="…"` as an attribute |
| You control the flow and UI | The Web Component provides its own UI |
| You listen to `.on('submit')` | You listen to `.addEventListener('submit')` |
—
### 🧠 Tip
Ensure your “ component dispatches the `submit` event with `event.detail` containing all the config info (including `price`). You can verify it via `console.log(event.detail)`.
—
Let me know if you also want to:
– Disable the native "Add to cart" button.
– Pre-fill some fields or load saved configurations.
– Handle language switching dynamically.
<script>
" rel="text/javascript" >
</script>
<script>
document.addEventListener("DOMContentLoaded", () => {
window.connectConfigurator.init({
proxy: '/wp-json/probo-api/v1/configurator/api',
language: 'nl',
productCode: "<?php echo esc_js($api_code); ?>",
});
});
</script>
whe we use js method to load configuration then its not load
You're **using the custom element “ in the HTML**, **and then trying to initialize it using** `window.connectConfigurator.init()` **via JavaScript.** The key issue here is:
> When you include the “ tag **manually in HTML**, you don’t need (and shouldn’t) call `connectConfigurator.init()` separately — **that’s meant for dynamic rendering via JS**, not static component tags.
—
### ✅ Solution Options
You must choose **either** of the following approaches — not both:
—
## ✅ Option 1: Static Web Component (Recommended)
Use the HTML tag **with attributes**, and it will auto-initialize:
<connect-product-configurator
proxy="/wp-json/probo-api/v1/configurator/api"
language="nl"
product-code="<?php echo esc_attr( $api_code ); ?>">
</connect-product-configurator>
👉 Remove this JS block entirely:
window.connectConfigurator.init({ ... });
The component itself handles loading config, language, and proxy internally based on attributes.
—
## ✅ Option 2: JavaScript-based Initialization (for dynamic injection)
If you **don’t** place the tag in your HTML and instead **want to inject and initialize it with JS**, you can do this:
### HTML:
<!-- Do NOT place the tag manually -->
<div id="probo-dynamic-placeholder"></div>
### JS (JavaScript module):
document.addEventListener("DOMContentLoaded", async () => {
const configurator = await window.connectConfigurator.init({
proxy: '/wp-json/probo-api/v1/configurator/api',
language: 'nl',
productCode: "<?php echo esc_js($api_code); ?>",
target: '#probo-dynamic-placeholder', // The container where it will inject
});
});
—
### ❌ Problem With Your Current Setup
You're doing this:
<connect-product-configurator></connect-product-configurator>
<script>
window.connectConfigurator.init({ ... });
</script>
That causes a **conflict**:
– The component already exists and tries to self-initialize.
– Then `connectConfigurator.init()` is trying to mount again → **double init fails silently**.
—
### ✅ Best Practice for You
Since you’re already outputting the HTML tag, **use this and only this**:
<connect-product-configurator
proxy="/wp-json/probo-api/v1/configurator/api"
language="nl"
product-code="<?php echo esc_attr( $api_code ); ?>">
</connect-product-configurator>
And **remove this part**:
window.connectConfigurator.init({ ... });
—
Let me know if:
– You want to listen for the configurator’s `submit` event to add to cart.
– You want to dynamically insert the configurator instead of using the HTML tag.
– You're still seeing blank / unresponsive component after fixing the init.
Uncaught TypeError: Cannot read properties of undefined (reading 'init')
means that `window.connectConfigurator` is `undefined`, so calling `window.connectConfigurator.init()` fails.
—
### ✅ Why This Is Happening
You're loading the component via this script:
<script type="module" src="<?php echo PC_PLUGIN_URL . 'assets/src/connect-product-configurator.js' ?>" rel="text/javascript"></script>
But then calling:
window.connectConfigurator.init(...)
Right after the DOM is ready — **before the JS module has fully loaded or exposed the `window.connectConfigurator` object.**
—
### ✅ How to Fix It
If you're using the **“ tag**, **you do NOT need to use** `connectConfigurator.init()` at all.
But if for some reason you want to access it, you must **wait for the JS module to load completely**.
—
### ✅ Fix Option A – Only use “ HTML tag (Recommended)
Just do:
<connect-product-configurator
proxy="/wp-json/probo-api/v1/configurator/api"
language="nl"
product-code="<?php echo esc_attr( $api_code ); ?>">
</connect-product-configurator>
➡️ Remove any JS that calls `connectConfigurator.init()`.
It will auto-initialize using the attributes.
—
### ✅ Fix Option B – Wait for the module to be defined
If you're going full JS and want to manually init it, **wait for the script to load**, like:
window.addEventListener('load', async () => {
if (!window.connectConfigurator || !window.connectConfigurator.init) {
console.error('connectConfigurator not ready');
return;
}
const configurator = await window.connectConfigurator.init({
proxy: '/wp-json/probo-api/v1/configurator/api',
language: 'nl',
productCode: "<?php echo esc_js($api_code); ?>",
target: '#probo-placeholder'
});
});
Or better yet, you can poll until it's defined:
const waitForConfigurator = async () => {
while (!window.connectConfigurator || !window.connectConfigurator.init) {
await new Promise(resolve => setTimeout(resolve, 100));
}
// Safe to init now
window.connectConfigurator.init({
proxy: '/wp-json/probo-api/v1/configurator/api',
language: 'nl',
productCode: "<?php echo esc_js($api_code); ?>",
target: '#probo-placeholder'
});
};
waitForConfigurator();
—
### 🧠 Summary
| If you're using… | Do this |
|——————–|———|
| “ in HTML | Remove `window.connectConfigurator.init()` completely |
| No tag in HTML (pure JS) | Use `connectConfigurator.init()` **only after the module is fully loaded** |
Let me know which approach you're going with and I can give the exact working code.
—
## 🔥 Problem Recap:
You're:
1. **Placing “ in the HTML**, AND
2. **Trying to call `window.connectConfigurator.init()`**, which is **meant for dynamically rendering** (when the HTML tag is not used).
As a result:
– The browser loads the HTML tag and tries to initialize it.
– Your JS tries to call `.init()` on something that doesn't exist yet → `connectConfigurator not ready`.
—
## ✅ Your Correct Setup (Use This and Remove JS `init()`)
If you are using the HTML tag, **do this**:
### ✅ 1. Use the tag **with attributes**:
<connect-product-configurator
proxy="/wp-json/probo-api/v1/configurator/api"
language="nl"
product-code="<?php echo esc_attr( $api_code ); ?>">
</connect-product-configurator>
✅ This **automatically initializes**.
—
### ✅ 2. Listen for the configurator's `submit` event (add to cart)
document.addEventListener("DOMContentLoaded", () => {
const configurator = document.querySelector("connect-product-configurator");
if (!configurator) {
console.error("Configurator component not found.");
return;
}
configurator.addEventListener("submit", async (event) => {
const configData = event.detail;
console.log("Config data submitted:", configData);
const productId = <?php echo intval($product->get_id()); ?>;
const response = await fetch("/wp-admin/admin-ajax.php?action=add_probo_to_cart", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
product_id: productId,
config_data: configData,
price: configData.price || 0,
}),
});
const result = await response.json();
if (result.success) {
window.location.href = result.redirect || "/cart";
} else {
alert("Failed to add to cart: " + result.message);
}
});
});
—
### ❌ DO NOT USE THIS ANYMORE:
window.connectConfigurator.init({...});
That’s only for dynamic usage if you did:
<div id="dynamic-placeholder"></div>
and then initialized like:
connectConfigurator.init({ productCode: '...', target: '#dynamic-placeholder' });
—
## ✅ Final Notes
– **If “ is in HTML**, remove all `window.connectConfigurator.init()` JS.
– Let the Web Component auto-initialize via attributes (`product-code`, `language`, `proxy`).
– Just listen for `submit` events and handle them.
—
Let me know if:
– The Web Component is still not displaying
– You want dynamic rendering instead of static tag
– You want custom loading behavior or integration into a popup/modal
public function handle_probo_product_configurator(WP_REST_Request $request) {
$baseUrl = 'https://api.proboprints.com';
$token = get_option( 'probo_api_key' );
$method = $request->get_method();
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'Content-Type: application/json',
'Authorization: Basic ' . $token
]);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
if ($method === 'POST') {
$body = $request->get_json_params();
if (!isset($body['url']) || !isset($body['data'])) {
return new WP_REST_Response(['error' => 'Invalid request. Missing `url` or `data`.'], 400);
}
curl_setopt($curl, CURLOPT_URL, $baseUrl . $body['url']);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($body['data']));
} else {
$url = $request->get_param('url');
if (!$url) {
return new WP_REST_Response(['error' => 'Missing `url` parameter.'], 400);
}
curl_setopt($curl, CURLOPT_URL, $baseUrl . $url);
}
$response = curl_exec($curl);
$httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
return new WP_REST_Response(json_decode($response, true), $httpcode);
}
this function so when we recieve calculation id then activate add to cart button and when click save each attribute
{
"status": "ok",
"code": 200,
"message": "Options calculated",
"products": [
{
"id": 1,
"code": "banner",
"can_order": true,
"amount": 2,
"width": 25,
"height": 15,
"length": 0,
"available_options": [],
"selected_options": [
{
"type_code": "width",
"name": "Breedte",
"label": null,
"description": null,
"value": "25",
"code": "width",
"default_value": null,
"min_value": 0,
"max_value": 99999,
"step_size": null,
"scale": 1,
"reversible": true,
"parent_code": "size",
"parent_name": "Afmeting",
"unit_code": "cm",
"images": []
},
{
"type_code": "height",
"name": "Hoogte",
"label": null,
"description": null,
"value": "15",
"code": "height",
"default_value": null,
"min_value": 0,
"max_value": 99999,
"step_size": null,
"scale": 1,
"reversible": true,
"parent_code": "size",
"parent_name": "Afmeting",
"unit_code": "cm",
"images": []
},
{
"type_code": "amount",
"name": "Hoeveelheid",
"label": null,
"description": null,
"value": "2",
"code": "amount",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "amount",
"parent_name": "Hoeveelheid",
"unit_code": "pc",
"images": []
},
{
"type_code": "radio",
"name": "Enkelzijdig",
"label": null,
"description": null,
"value": null,
"code": "single-sided",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "print-side",
"parent_name": "Print",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/122/1157641fbfa8a2d9.svg"
}
]
},
{
"type_code": "radio",
"name": "Soundmesh",
"label": null,
"description": "Lucht- en geluiddoorlatend PVC doek met een gewicht van 180 gr./m2.nMinimale afname van 10 m2",
"value": null,
"code": "soundmesh",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "material",
"parent_name": "Materiaal",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/202408/8805/8f3d55f833365265.jpg"
}
]
},
{
"type_code": "radio",
"name": "Voor alle zijden gelijk",
"label": null,
"description": "Alle zijden van het doek krijgen dezelfde afwerking.",
"value": null,
"code": "finishing-all-sides",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "finishing-per-side-all-sides",
"parent_name": "Afwerking voor alle zijden / per zijde",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/72/99e39932389f5591.svg"
}
]
},
{
"type_code": "radio",
"name": "Zomen en ringen",
"label": "Meest gekozen",
"description": "Voor de bevestiging krijgt het doek rondom een zoom van 4 cm en ringen van nikkel. De zoom wordt met hitte gelast.",
"value": null,
"code": "hem-and-rings",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "finishing",
"parent_name": "Gewenste afwerking",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/84/af0c678a9f4c4248.jpg"
}
]
},
{
"type_code": "radio",
"name": "In elke hoek",
"label": null,
"description": null,
"value": null,
"code": "every-corner",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "position-rings",
"parent_name": "Positie ringen",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/76/e437bd1efbb3afdd.svg"
}
]
},
{
"type_code": "cross_sell_pc",
"name": "Tiewrap – Wit – set van 20 stuks",
"label": null,
"description": null,
"value": "1",
"code": "tiewrap-white-20",
"default_value": "ceil(IA*(({AF-UV-RING:CA}/IA)/20))",
"min_value": null,
"max_value": null,
"step_size": 1,
"scale": null,
"reversible": null,
"parent_code": "accessories-cross-sell",
"parent_name": "Accessoires",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/202503/10350/da91c98b63e775f6.png"
}
]
}
],
"upload": true,
"uploaders": [
{
"type": "single",
"amount": 2,
"width": 25,
"height": 15,
"length": 0,
"minimal_dpi": 72,
"mirror_enabled": true,
"fill_enabled": true,
"rotation_enabled": true,
"file_limit": 500,
"require_white_spot": false,
"required_cut_names": [],
"optional_cut_names": [],
"tiling_enabled": false,
"tiling_mandatory": false,
"tiling_direction": null,
"max_width_excl_overlap": 490,
"max_height_excl_overlap": 2396,
"max_width_incl_overlap": 486,
"max_height_incl_overlap": 2392,
"bleed_top": 1,
"bleed_right": 1,
"bleed_bottom": 1,
"bleed_left": 1
}
],
"linked_products": [
{
"title": "Soundmesh",
"description": "Soundmesh",
"code": "soundmesh",
"article_group_code": "MA",
"unit_code": "m2",
"shipping_weight": 0.2
}
],
"prices_total": {
"24": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 40,
"purchase_price": 186.2,
"purchase_price_incl_vat": 225.3,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"48": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 30,
"purchase_price": 176.2,
"purchase_price_incl_vat": 213.2,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"72": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 15,
"purchase_price": 161.2,
"purchase_price_incl_vat": 195.05,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"96": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"120": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"144": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"168": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"192": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"216": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"240": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"264": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"288": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"312": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"336": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"360": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"384": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"408": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"432": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"456": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"480": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
}
},
"prices_per_product": {
"24": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 20,
"purchase_price": 93.1,
"purchase_price_incl_vat": 112.651,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"48": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 15,
"purchase_price": 88.1,
"purchase_price_incl_vat": 106.601,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"72": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 7.5,
"purchase_price": 80.6,
"purchase_price_incl_vat": 97.526,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"96": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"120": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"144": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"168": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"192": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"216": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"240": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"264": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"288": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"312": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"336": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"360": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"384": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"408": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"432": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"456": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"480": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
}
}
}
],
"calculation_id": 80386353
}
under selected_options to cart item meta data
—
## ✅ Step 1: Modify the Web Component `submit` Listener
### ✅ Updated JS
document.addEventListener("DOMContentLoaded", () => {
const configurator = document.querySelector("connect-product-configurator");
const addToCartBtn = document.getElementById("launchConfigurator");
addToCartBtn.disabled = true; // Disable initially
configurator?.addEventListener("submit", async (event) => {
const configData = event.detail;
// Ensure calculation_id is present
if (!configData?.calculation_id || !configData?.products?.[0]) {
alert("Invalid configurator response.");
return;
}
const selectedOptions = configData.products[0].selected_options;
const calculationId = configData.calculation_id;
console.log("Configurator Submitted:");
console.log("Calculation ID:", calculationId);
console.log("Selected Options:", selectedOptions);
// Enable Add to Cart button now
addToCartBtn.disabled = false;
// Store in window to use on click
window.proboCartData = {
product_id: <?php echo intval($product->get_id()); ?>,
calculation_id: calculationId,
selected_options: selectedOptions,
price: configData.products[0].prices_total['96']?.purchase_price_incl_vat || 0 // pick default delivery time
};
});
addToCartBtn?.addEventListener("click", async () => {
if (!window.proboCartData) {
alert("Please finish customization first.");
return;
}
const res = await fetch("/wp-admin/admin-ajax.php?action=add_probo_to_cart", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(window.proboCartData),
});
const result = await res.json();
if (result.success) {
window.location.href = result.redirect || "/cart";
} else {
alert("Failed to add to cart: " + result.message);
}
});
});
—
## ✅ Step 2: Handle Add to Cart in PHP (AJAX)
Add this to your plugin or theme `functions.php`:
add_action('wp_ajax_add_probo_to_cart', 'handle_add_probo_to_cart');
add_action('wp_ajax_nopriv_add_probo_to_cart', 'handle_add_probo_to_cart');
function handle_add_probo_to_cart() {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['product_id']) || empty($data['selected_options']) || empty($data['calculation_id'])) {
wp_send_json_error(['message' => 'Missing required configuration.']);
}
$product_id = absint($data['product_id']);
$selected_options = $data['selected_options'];
$calculation_id = sanitize_text_field($data['calculation_id']);
$price = floatval($data['price']);
$cart_item_data = [
'probo_selected_options' => $selected_options,
'probo_calculation_id' => $calculation_id,
'probo_price' => $price
];
$cart_item_key = WC()->cart->add_to_cart($product_id, 1, 0, [], $cart_item_data);
if ($cart_item_key) {
wp_send_json_success(['redirect' => wc_get_cart_url()]);
} else {
wp_send_json_error(['message' => 'Failed to add item to cart.']);
}
}
—
## ✅ Step 3: Show Selected Options in Cart/Checkout
add_filter('woocommerce_get_item_data', function($item_data, $cart_item) {
if (!empty($cart_item['probo_calculation_id'])) {
$item_data[] = [
'name' => 'Calculation ID',
'value' => esc_html($cart_item['probo_calculation_id']),
];
}
if (!empty($cart_item['probo_selected_options'])) {
foreach ($cart_item['probo_selected_options'] as $option) {
$name = $option['name'] ?? $option['code'];
$value = $option['value'] ?? '';
if ($name && $value) {
$item_data[] = [
'name' => esc_html($name),
'value' => esc_html($value),
];
}
}
}
return $item_data;
}, 10, 2);
—
## ✅ Step 4: Save to Order Metadata
add_action('woocommerce_add_order_item_meta', function($item_id, $values) {
if (!empty($values['probo_selected_options'])) {
wc_add_order_item_meta($item_id, '_probo_selected_options', $values['probo_selected_options']);
}
if (!empty($values['probo_calculation_id'])) {
wc_add_order_item_meta($item_id, '_probo_calculation_id', $values['probo_calculation_id']);
}
}, 10, 2);
—
## ✅ Step 5: Set Custom Price in Cart
add_action('woocommerce_before_calculate_totals', function($cart) {
if (is_admin() && !defined('DOING_AJAX')) return;
foreach ($cart->get_cart() as $item) {
if (!empty($item['probo_price'])) {
$item['data']->set_price($item['probo_price']);
}
}
});
—
## ✅ Bonus: Show in Order Email
add_filter('woocommerce_order_item_name', function($name, $item) {
$options = wc_get_order_item_meta($item->get_id(), '_probo_selected_options', true);
$calculation_id = wc_get_order_item_meta($item->get_id(), '_probo_calculation_id', true);
if ($calculation_id) {
$name .= '<br><strong>Calculation ID:</strong> ' . esc_html($calculation_id);
}
if ($options && is_array($options)) {
$name .= '<ul>';
foreach ($options as $opt) {
if (!empty($opt['name']) && isset($opt['value'])) {
$name .= '<li>' . esc_html($opt['name']) . ': ' . esc_html($opt['value']) . '</li>';
}
}
$name .= '</ul>';
}
return $name;
}, 10, 2);
—
Let me know if you'd also like to:
– Display option images in the cart.
– Support multiple quantities with dynamic recalculation.
– Include uploaded files via Probo's uploader in order metadata.
<script>
" rel="text/javascript" >
</script>
we are using like above configurator
Now your goal is:
1. ✅ **Listen for the configurator submission.**
2. ✅ **Extract `calculation_id` and `selected_options`.**
3. ✅ **Enable the "Customize product" button only after valid submission.**
4. ✅ **On button click, send everything to WooCommerce cart via AJAX.**
—
## ✅ Final Working Solution (Based on Your HTML)
### ✅ Add this script just below the component:
<script type="module">
document.addEventListener("DOMContentLoaded", () => {
const configurator = document.querySelector("connect-product-configurator");
const addToCartBtn = document.getElementById("launchConfigurator");
addToCartBtn.disabled = true; // Disable by default
let proboCartData = null;
configurator?.addEventListener("submit", async (event) => {
const detail = event.detail;
// Check for calculation ID and product data
if (!detail?.calculation_id || !detail?.products?.[0]) {
console.error("Invalid configurator response");
alert("Configuration is incomplete or invalid.");
return;
}
const productId = <?php echo intval($product->get_id()); ?>;
const selectedOptions = detail.products[0].selected_options || [];
const price = detail.products[0].prices_total?.["96"]?.purchase_price_incl_vat || 0;
// Store cart data for later use
proboCartData = {
product_id: productId,
calculation_id: detail.calculation_id,
selected_options: selectedOptions,
price: price
};
addToCartBtn.disabled = false; // Enable the button
});
addToCartBtn?.addEventListener("click", async () => {
if (!proboCartData) {
alert("Please finish customizing the product before adding to cart.");
return;
}
const res = await fetch("/wp-admin/admin-ajax.php?action=add_probo_to_cart", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(proboCartData)
});
const result = await res.json();
if (result.success) {
window.location.href = result.redirect || "/cart";
} else {
alert("Error: " + result.message);
}
});
});
</script>
—
## ✅ Add this in `functions.php` (or plugin)
### AJAX handler for `add_probo_to_cart`:
add_action('wp_ajax_add_probo_to_cart', 'handle_add_probo_to_cart');
add_action('wp_ajax_nopriv_add_probo_to_cart', 'handle_add_probo_to_cart');
function handle_add_probo_to_cart() {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['product_id']) || empty($data['selected_options']) || empty($data['calculation_id'])) {
wp_send_json_error(['message' => 'Missing data for cart.']);
}
$product_id = absint($data['product_id']);
$selected_options = $data['selected_options'];
$calculation_id = sanitize_text_field($data['calculation_id']);
$price = floatval($data['price']);
$cart_item_data = [
'probo_selected_options' => $selected_options,
'probo_calculation_id' => $calculation_id,
'probo_price' => $price
];
$cart_key = WC()->cart->add_to_cart($product_id, 1, 0, [], $cart_item_data);
if ($cart_key) {
wp_send_json_success(['redirect' => wc_get_cart_url()]);
} else {
wp_send_json_error(['message' => 'Could not add to cart.']);
}
}
—
## ✅ Show Probo Config in Cart + Order
### Cart/checkout display:
add_filter('woocommerce_get_item_data', function($item_data, $cart_item) {
if (!empty($cart_item['probo_calculation_id'])) {
$item_data[] = ['name' => 'Calculation ID', 'value' => $cart_item['probo_calculation_id']];
}
if (!empty($cart_item['probo_selected_options'])) {
foreach ($cart_item['probo_selected_options'] as $opt) {
if (!empty($opt['name']) && isset($opt['value'])) {
$item_data[] = [
'name' => esc_html($opt['name']),
'value' => esc_html($opt['value']),
];
}
}
}
return $item_data;
}, 10, 2);
### Save to order:
add_action('woocommerce_add_order_item_meta', function($item_id, $values) {
if (!empty($values['probo_calculation_id'])) {
wc_add_order_item_meta($item_id, '_probo_calculation_id', $values['probo_calculation_id']);
}
if (!empty($values['probo_selected_options'])) {
wc_add_order_item_meta($item_id, '_probo_selected_options', $values['probo_selected_options']);
}
}, 10, 2);
—
### Optional: Price override (if you're using dynamic pricing)
add_action('woocommerce_before_calculate_totals', function($cart) {
if (is_admin() && !defined('DOING_AJAX')) return;
foreach ($cart->get_cart() as $item) {
if (!empty($item['probo_price'])) {
$item['data']->set_price($item['probo_price']);
}
}
});
—
Let me know if you'd like:
– The selected option images shown in cart/checkout
– A delivery time selector linked to the correct price
– To disable native WooCommerce "Add to cart" button on product page
This setup ensures your `connect-product-configurator` behaves like a true product customizer.
data so why sibmit
You're absolutely right: the “ Web Component **already fetches the configuration and pricing via the proxy API**, and **you receive the full configuration data (including `calculation_id`, `selected_options`, and pricing)** without needing to hit the REST API again.
But here's the **important clarification**:
—
## 🧩 Why We Still Use the `submit` Event
Even though the Web Component internally calls the Probo REST API and gets the data:
### ✅ The `submit` event is the **only reliable moment** when:
– The user **completes the configuration**.
– The component **emits the full config result** including:
– `calculation_id`
– `products`
– `selected_options`
– `price`
– `uploaders` (if enabled)
> So the `submit` event is your trigger to collect the finished data and send it to your cart.
—
## 🧠 Think of It Like This
| Component Part | Responsibility |
|—————————-|———————————————-|
| `proxy`, `language`, `product-code` | Tells component where to fetch config data |
| Internal REST call | Gets options, renders configurator UI |
| `submit` event | Emits finalized configuration (user-confirmed) ✅ |
—
## ✅ Summary
– You don’t call the REST API again manually.
– The `submit` event gives you **finalized user choices**, not partial or intermediate values.
– That’s why you **wait for `submit`** → then send that data to WooCommerce via AJAX.
—
## 🚀 Your Flow Is Already Correct
So keep this:
configurator.addEventListener("submit", async (event) => {
const configData = event.detail;
// use configData.calculation_id, selected_options, price etc.
});
You are **not duplicating REST requests** — you’re just **listening for the result** of the component’s internal logic.
—
Let me know if you want to:
– Show a mini summary (width, height, material…) on the page after config.
– Capture uploaded file names from `configData.uploaders`.
– Automatically add to cart without button click (on submit).
"status": "ok",
"code": 200,
"message": "Options calculated",
"products": [
{
"id": 1,
"code": "banner",
"can_order": true,
"amount": 2,
"width": 25,
"height": 15,
"length": 0,
"available_options": [],
"selected_options": [
{
"type_code": "width",
"name": "Breedte",
"label": null,
"description": null,
"value": "25",
"code": "width",
"default_value": null,
"min_value": 0,
"max_value": 99999,
"step_size": null,
"scale": 1,
"reversible": true,
"parent_code": "size",
"parent_name": "Afmeting",
"unit_code": "cm",
"images": []
},
{
"type_code": "height",
"name": "Hoogte",
"label": null,
"description": null,
"value": "15",
"code": "height",
"default_value": null,
"min_value": 0,
"max_value": 99999,
"step_size": null,
"scale": 1,
"reversible": true,
"parent_code": "size",
"parent_name": "Afmeting",
"unit_code": "cm",
"images": []
},
{
"type_code": "amount",
"name": "Hoeveelheid",
"label": null,
"description": null,
"value": "2",
"code": "amount",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "amount",
"parent_name": "Hoeveelheid",
"unit_code": "pc",
"images": []
},
{
"type_code": "radio",
"name": "Enkelzijdig",
"label": null,
"description": null,
"value": null,
"code": "single-sided",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "print-side",
"parent_name": "Print",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/122/1157641fbfa8a2d9.svg"
}
]
},
{
"type_code": "radio",
"name": "Soundmesh",
"label": null,
"description": "Lucht- en geluiddoorlatend PVC doek met een gewicht van 180 gr./m2.nMinimale afname van 10 m2",
"value": null,
"code": "soundmesh",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "material",
"parent_name": "Materiaal",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/202408/8805/8f3d55f833365265.jpg"
}
]
},
{
"type_code": "radio",
"name": "Voor alle zijden gelijk",
"label": null,
"description": "Alle zijden van het doek krijgen dezelfde afwerking.",
"value": null,
"code": "finishing-all-sides",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "finishing-per-side-all-sides",
"parent_name": "Afwerking voor alle zijden / per zijde",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/72/99e39932389f5591.svg"
}
]
},
{
"type_code": "radio",
"name": "Zomen en ringen",
"label": "Meest gekozen",
"description": "Voor de bevestiging krijgt het doek rondom een zoom van 4 cm en ringen van nikkel. De zoom wordt met hitte gelast.",
"value": null,
"code": "hem-and-rings",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "finishing",
"parent_name": "Gewenste afwerking",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/84/af0c678a9f4c4248.jpg"
}
]
},
{
"type_code": "radio",
"name": "In elke hoek",
"label": null,
"description": null,
"value": null,
"code": "every-corner",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "position-rings",
"parent_name": "Positie ringen",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/76/e437bd1efbb3afdd.svg"
}
]
},
{
"type_code": "cross_sell_pc",
"name": "Tiewrap – Wit – set van 20 stuks",
"label": null,
"description": null,
"value": "1",
"code": "tiewrap-white-20",
"default_value": "ceil(IA*(({AF-UV-RING:CA}/IA)/20))",
"min_value": null,
"max_value": null,
"step_size": 1,
"scale": null,
"reversible": null,
"parent_code": "accessories-cross-sell",
"parent_name": "Accessoires",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/202503/10350/da91c98b63e775f6.png"
}
]
}
],
"upload": true,
"uploaders": [
{
"type": "single",
"amount": 2,
"width": 25,
"height": 15,
"length": 0,
"minimal_dpi": 72,
"mirror_enabled": true,
"fill_enabled": true,
"rotation_enabled": true,
"file_limit": 500,
"require_white_spot": false,
"required_cut_names": [],
"optional_cut_names": [],
"tiling_enabled": false,
"tiling_mandatory": false,
"tiling_direction": null,
"max_width_excl_overlap": 490,
"max_height_excl_overlap": 2396,
"max_width_incl_overlap": 486,
"max_height_incl_overlap": 2392,
"bleed_top": 1,
"bleed_right": 1,
"bleed_bottom": 1,
"bleed_left": 1
}
],
"linked_products": [
{
"title": "Soundmesh",
"description": "Soundmesh",
"code": "soundmesh",
"article_group_code": "MA",
"unit_code": "m2",
"shipping_weight": 0.2
}
],
"prices_total": {
"24": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 40,
"purchase_price": 186.2,
"purchase_price_incl_vat": 225.3,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"48": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 30,
"purchase_price": 176.2,
"purchase_price_incl_vat": 213.2,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"72": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 15,
"purchase_price": 161.2,
"purchase_price_incl_vat": 195.05,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"96": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"120": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"144": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"168": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"192": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"216": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"240": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"264": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"288": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"312": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"336": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"360": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"384": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"408": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"432": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"456": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"480": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
}
},
"prices_per_product": {
"24": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 20,
"purchase_price": 93.1,
"purchase_price_incl_vat": 112.651,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"48": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 15,
"purchase_price": 88.1,
"purchase_price_incl_vat": 106.601,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"72": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 7.5,
"purchase_price": 80.6,
"purchase_price_incl_vat": 97.526,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"96": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"120": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"144": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"168": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"192": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"216": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"240": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"264": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"288": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"312": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"336": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"360": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"384": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"408": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"432": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"456": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"480": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
}
}
}
],
"calculation_id": 80386353
}
what is price_per_product
### 🔍 `prices_per_product` in the Probo API response refers to:
> ✅ The **unit price** (price per single product) based on the selected **delivery time in hours**.
—
### 🧠 Key Difference:
| Field | What it means |
|————————–|——————————————-|
| `prices_total` | Total price for the **entire quantity** |
| `prices_per_product` | Price **per unit** of the product |
—
### 🔧 Example:
From your data (delivery time `96` hours):
"prices_total": {
"96": {
"purchase_base_price": 146.2,
"purchase_price_incl_vat": 176.9
}
},
"prices_per_product": {
"96": {
"purchase_base_price": 73.1,
"purchase_price_incl_vat": 88.451
}
}
#### Assuming:
– Quantity = 2
– Unit price = `88.451`
– Total = `88.451 x 2 = 176.902` → matches `prices_total['96'].purchase_price_incl_vat`
—
### ✅ When to Use Which?
| Use Case | Use `prices_total` | Use `prices_per_product` |
|—————————-|————————-|—————————|
| Add total line item price | ✅ Yes | ❌ No |
| Display unit price | ❌ Optional (for info) | ✅ Yes |
| Store for WooCommerce cart | ✅ Use total price | Optional, but you can show unit price too |
—
### 🔧 Final Suggestion for WooCommerce Integration
You should use:
const price = configData.products[0].prices_total["96"]?.purchase_price_incl_vat;
And optionally store/display:
const unitPrice = configData.products[0].prices_per_product["96"]?.purchase_price_incl_vat;
const quantity = configData.products[0].amount;
Then in WooCommerce:
– Save both as cart meta (optional)
– Use `total` for cart pricing
– Use `unit` only for display (optional)
—
Let me know if you want help calculating price dynamically based on user-chosen delivery time or showing both prices in the cart.
### 🔍 `linked_products` in the Probo API response represents:
> ✅ **Extra items** or **associated products** that are related to the main product configuration.
These are often **accessories** or **materials** that need to be ordered along with the main item.
—
### 💡 Example from your data:
"linked_products": [
{
"title": "Soundmesh",
"description": "Soundmesh",
"code": "soundmesh",
"article_group_code": "MA",
"unit_code": "m2",
"shipping_weight": 0.2
}
]
### 🧠 This means:
– A **material** called **Soundmesh** is required (likely selected or implied by the configuration).
– It’s a **related product** with:
– `code`: "soundmesh" — used in backend systems
– `unit_code`: "m2" — likely priced per square meter
– `shipping_weight`: 0.2 — per unit (e.g., per m²)
—
### ✅ Common Use Cases for `linked_products`:
| Use Case | Example |
|———-|———|
| Display info in cart/order summary | “Includes material: Soundmesh (0.2kg per m²)” |
| Pass to fulfillment system | So the API knows what material/code to ship |
| Charge separately (advanced case) | If each linked product has its own SKU and cost |
—
### ✅ How to Use in WooCommerce (Recommended):
You **can store it in the cart item meta** like this (PHP):
if (!empty($config['linked_products'])) {
wc_add_order_item_meta($item_id, '_probo_linked_products', $config['linked_products']);
}
And **display it** in the cart or order emails like:
$linked = wc_get_order_item_meta($item->get_id(), '_probo_linked_products', true);
if ($linked && is_array($linked)) {
echo '<ul><strong>Included Materials:</strong>';
foreach ($linked as $lp) {
echo '<li>' . esc_html($lp['title']) . ' (' . esc_html($lp['unit_code']) . ')</li>';
}
echo '</ul>';
}
—
### 🛑 Note:
– `linked_products` are not user-selected separately — they are **attached automatically** based on the main configuration (like a dependency).
– They’re **informational**, unless you have a use case to bill or display them prominently.
—
Let me know if you want:
– These linked products shown on the cart page or order summary
– A way to use their weights for shipping calculation
– To show them as a line item (separately) in WooCommerce
<script>
document.addEventListener("DOMContentLoaded", () => {
const configurator = document.querySelector("connect-product-configurator");
const addToCartBtn = document.getElementById("launchConfigurator");
addToCartBtn.disabled = true; // Disable by default
let proboCartData = null;
configurator?.addEventListener("submit", async (event) => {
const detail = event.detail;
// Check for calculation ID and product data
if (!detail?.calculation_id || !detail?.products?.[0]) {
console.error("Invalid configurator response");
alert("Configuration is incomplete or invalid.");
return;
}
const productId = <?php echo intval($product->get_id()); ?>;
const selectedOptions = detail.products[0].selected_options || [];
const price = detail.products[0].prices_total?.["96"]?.purchase_price_incl_vat || 0;
// Store cart data for later use
proboCartData = {
product_id: productId,
calculation_id: detail.calculation_id,
selected_options: selectedOptions,
price: price
};
addToCartBtn.disabled = false; // Enable the button
});
addToCartBtn?.addEventListener("click", async () => {
if (!proboCartData) {
alert("Please finish customizing the product before adding to cart.");
return;
}
const res = await fetch("/wp-admin/admin-ajax.php?action=add_probo_to_cart", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(proboCartData)
});
const result = await res.json();
if (result.success) {
window.location.href = result.redirect || "/cart";
} else {
alert("Error: " + result.message);
}
});
});
</script>
not working
—
## ✅ 1. Double-Check: Is the `submit` event from “ really firing?
The most likely reasons it's **not working** are:
### 🔴 Either:
– The `submit` event is not fired by the component.
– Or the event name is different (e.g., `complete`, `configuratorSubmit`, etc.).
– Or it fires but `event.detail` is missing or malformed.
– Or the JS block runs **before the component is fully defined**.
—
## ✅ 2. Confirm If Event Fires (Add Logging Early)
Update your code to **log ALL events** emitted from the component:
document.addEventListener("DOMContentLoaded", () => {
const configurator = document.querySelector("connect-product-configurator");
const addToCartBtn = document.getElementById("launchConfigurator");
if (!configurator) {
console.error("Configurator element not found!");
return;
}
// Log all custom events
configurator.addEventListener("submit", (event) => {
console.log("EVENT: submit", event.detail);
});
configurator.addEventListener("change", (event) => {
console.log("EVENT: change", event.detail);
});
configurator.addEventListener("calculated", (event) => {
console.log("EVENT: calculated", event.detail);
});
configurator.addEventListener("completed", (event) => {
console.log("EVENT: completed", event.detail);
});
// Disable button by default
addToCartBtn.disabled = true;
let proboCartData = null;
// Handle submit or completed
configurator.addEventListener("submit", (event) => {
const detail = event.detail;
console.log("Configurator Submitted:", detail);
if (!detail?.calculation_id || !detail?.products?.[0]) {
alert("Invalid configurator data.");
return;
}
const productId = <?php echo intval($product->get_id()); ?>;
const selectedOptions = detail.products[0].selected_options || [];
const price = detail.products[0].prices_total?.["96"]?.purchase_price_incl_vat || 0;
proboCartData = {
product_id: productId,
calculation_id: detail.calculation_id,
selected_options: selectedOptions,
price: price
};
addToCartBtn.disabled = false;
});
// Handle Add to Cart click
addToCartBtn.addEventListener("click", async () => {
if (!proboCartData) {
alert("Please complete configuration first.");
return;
}
const res = await fetch("/wp-admin/admin-ajax.php?action=add_probo_to_cart", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(proboCartData)
});
const result = await res.json();
if (result.success) {
window.location.href = result.redirect || "/cart";
} else {
alert("Error: " + result.message);
}
});
});
—
## ✅ 3. Check in Browser Console
1. Open your page in Chrome.
2. Go to **Developer Tools → Console tab**.
3. Customize a product → See if any of the events (`submit`, `completed`, etc.) appear.
4. If **none appear**, then the event name is different or the component isn't emitting anything.
—
## ✅ 4. Next Steps Based on Result
| What you see | What to do |
|————–|————|
| `submit` logged | Your script works. You may have misconfigured pricing or button |
| No event logged | Contact Probo or check their Web Component documentation — the event might be called `completed`, `configuratorFinished`, or require a callback |
| Error in JS console | The component might not be initialized (e.g., wrong script or JS file missing) |
—
## ✅ Also Double-Check
– The component script is loaded (no 404 errors for `connect-product-configurator.js`).
– You're not calling `window.connectConfigurator.init()` if you're using the tag directly.
– No JS syntax errors above the script tag.
—
If you'd like, I can debug live with the HTML/JS code in place — or you can paste any console log/error here.