Multiple upload buttons code
August 25, 2025 · AI Conversations
if (!empty($cart_item['probo_uploader_url'])) {
$upload_url = esc_url($cart_item['probo_uploader_url']);
$button = 'Upload Files';
echo $button;
}
}
make it to handle muliple loine item has multile upload files button
// Hook it (if not already)
add_action('woocommerce_after_cart_item_name', 'probo_upload_field_to_cart_items', 10, 2);
function probo_upload_field_to_cart_items($cart_item, $cart_item_key) {
// 1) If you pass an array of URLs (preferred): $cart_item['probo_uploader_urls'] = ['https://...','https://...']
if ( ! empty($cart_item['probo_uploader_urls']) && is_array($cart_item['probo_uploader_urls']) ) {
$i = 1;
echo '<div class="probo-upload-buttons" style="margin-top:10px;">';
foreach ( $cart_item['probo_uploader_urls'] as $url ) {
$upload_url = esc_url($url);
printf(
'<a href="%1$s" class="button probo-upload-button wp-element-button" style="margin:4px 6px 0 0;" target="_blank" rel="noopener noreferrer">Upload Files %2$d</a>',
$upload_url,
$i
);
$i++;
}
echo '</div>';
return;
}
// 2) Backward compatibility: single URL key like before
if ( ! empty($cart_item['probo_uploader_url']) ) {
$qty = isset($cart_item['quantity']) ? max(1, (int) $cart_item['quantity']) : 1;
$base_url = esc_url($cart_item['probo_uploader_url']);
echo '<div class="probo-upload-buttons" style="margin-top:10px;">';
// If quantity > 1, show one button per unit, appending an index param (?item=1,2,3...)
for ($i = 1; $i <= $qty; $i++) {
// Add an index query param so each button can map to a distinct upload
$url_with_index = add_query_arg('item', $i, $base_url);
printf(
'<a href="%1$s" class="button probo-upload-button wp-element-button" style="margin:4px 6px 0 0;" target="_blank" rel="noopener noreferrer">Upload Files %2$d</a>',
esc_url($url_with_index),
$i
);
}
echo '</div>';
}
}
### How it works
– If you set `probo_uploader_urls` (array), it prints one button per URL (numbered 1, 2, 3…).
– Otherwise it falls back to the old `probo_uploader_url` (string). If the line item quantity is >1, it renders that many buttons and adds `?item=N` so your upload page can distinguish each unit.
> On your upload handler page, read `$_GET['item']` (if present) to know which unit the upload belongs to.
1) **Render a button per line item** (uses `$cart_item_key` so every row is unique)
// Show an Upload button per CART ITEM row
add_action('woocommerce_after_cart_item_name', function($cart_item, $cart_item_key) {
// Accept single URL or array of URLs saved on THIS line item only
$urls = [];
if (!empty($cart_item['probo_uploader_urls']) && is_array($cart_item['probo_uploader_urls'])) {
$urls = $cart_item['probo_uploader_urls'];
} elseif (!empty($cart_item['probo_uploader_url'])) {
$urls = [ $cart_item['probo_uploader_url'] ];
}
if (empty($urls)) return;
echo '<div class="probo-upload-buttons" style="margin-top:10px;">';
$i = 1;
foreach ($urls as $url) {
$href = add_query_arg([
'cart_item_key' => $cart_item_key, // identifies this exact row
'product_id' => $cart_item['product_id'] ?? 0,
'index' => $i // if multiple URLs per row
], esc_url($url));
printf(
'<a href="%1$s" class="button probo-upload-button wp-element-button" target="_blank" rel="noopener noreferrer" style="margin:4px 6px 0 0;">Upload Files %2$d</a>',
esc_url($href),
$i
);
$i++;
}
echo '</div>';
}, 10, 2);
2) **Make sure each line item actually stores its own URL(s)** when added to cart
If you’re posting an upload URL from the product page (or generating it), attach it **to the specific cart item** and add a `unique_key` so two identical products don’t merge into one row:
// Store per-line-item data when adding to cart
add_filter('woocommerce_add_cart_item_data', function($cart_item_data, $product_id, $variation_id) {
// Example: get from request; adjust to your field name(s)
if (!empty($_POST['probo_uploader_url'])) {
$cart_item_data['probo_uploader_url'] = esc_url_raw($_POST['probo_uploader_url']);
}
if (!empty($_POST['probo_uploader_urls']) && is_array($_POST['probo_uploader_urls'])) {
$cart_item_data['probo_uploader_urls'] = array_map('esc_url_raw', $_POST['probo_uploader_urls']);
}
// Prevent merging of identical items so each stays its own line item
if (!empty($cart_item_data)) {
$cart_item_data['unique_key'] = md5(microtime(true) . wp_rand());
}
return $cart_item_data;
}, 10, 3);
That’s it: each **cart row** now has its **own** upload button(s), even for the same product added multiple times.
public function probo_upload_field_to_cart_items($cart_item, $cart_item_key) {
if (!empty($cart_item['probo_uploader_url'])) {
$upload_url = esc_url($cart_item['probo_uploader_url']);
// Add cart item key to the URL to identify which item is being uploaded for
$upload_url = add_query_arg('cart_item_key', $cart_item_key, $upload_url);
$button = sprintf(
'
%s
',
esc_attr($cart_item_key),
esc_url($upload_url),
__('Upload Files', 'wp-probo-connect')
);
echo $button;
}
}
Now make neccessary change baased on above to
jQuery(document).ready(function($) {
let pollInterval = 3000; // 3 seconds
let pollingTimer = null;
// $('.checkout-button').css({'opacity': 0.5,'cursor': 'not-allowed','pointer-events': 'none'});
function checkStatus() {
$('.probo-upload-button').html(proboObj.upload_in_progress).addClass('probo-uploader-processing');
$.ajax({
url: proboObj.ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'probo_poll_callback',
},
success: function(response) {
console.log(response);
if (response.success && response.status === 'processed') {
console.log('Upload processed, stopping polling…');
clearInterval(pollingTimer);
$('.checkout-button')
.css({'opacity': 1,'cursor': 'pointer','pointer-events': 'auto'})
.text('Proceed to Checkout');
$('.probo-upload-button').html(proboObj.file_processed).removeClass('probo-uploader-processing').addClass('probo-uploader-processed');
} else {
let status = response.status;
if(response.status === '' || response.status === null){
status='progress';
}
$('.probo-upload-button').html(`${proboObj.upload_status}: ${status}`).removeClass('probo-uploader-processing').addClass(`probo-uploader-${status}`);
$('.checkout-button')
.css({'opacity': 1,'cursor': 'pointer','pointer-events': 'auto'})
.text(proboObj.upload_in_progress);
}
},
error: function() {
console.log('Error polling callback');
}
});
}
// Only start polling when "Upload Files" button is clicked
$(document).on('click', '.probo-upload-button', function(e) {
if (pollingTimer) return; // prevent multiple intervals
checkStatus(); // optional immediate check
pollingTimer = setInterval(checkStatus, pollInterval);
});
});
– polls **per line item** (separate timer per `cart_item_key`)
– updates **only the clicked button’s** label/state
– sends the `cart_item_key` to your AJAX handler
– enables “Proceed to Checkout” only when **all upload buttons present are processed**
jQuery(document).ready(function($) {
const pollInterval = 3000; // 3 seconds
const pollingTimers = {}; // keyed by cart_item_key
// Helper: enable/disable checkout depending on buttons' states
function refreshCheckoutState() {
const $buttons = $('.probo-upload-button');
if ($buttons.length === 0) return; // nothing to gate
const allProcessed = $buttons.toArray().every(btn => $(btn).hasClass('probo-uploader-processed'));
const $checkout = $('.checkout-button');
if (allProcessed) {
$checkout.css({'opacity': 1, 'cursor': 'pointer', 'pointer-events': 'auto'})
.text('Proceed to Checkout');
} else {
$checkout.css({'opacity': 0.5, 'cursor': 'not-allowed', 'pointer-events': 'none'})
.text(proboObj.upload_in_progress || 'Upload in progress…');
}
}
// Core: check status for a specific cart item
function checkStatus(cartItemKey, $btn) {
// Defensive: make sure we always show a working state during call
$btn.html(proboObj.upload_in_progress || 'Checking…')
.addClass('probo-uploader-processing')
.removeClass('probo-uploader-processed probo-uploader-progress probo-uploader-queued probo-uploader-error');
$.ajax({
url: proboObj.ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'probo_poll_callback',
cart_item_key: cartItemKey // <-- IMPORTANT: tell server which item we’re asking about
// nonce: proboObj.nonce // (optional) include if you have one
},
success: function(response) {
// Expect: { success: true, status: "processed"|"progress"|"queued"|"error", cart_item_key: "..." }
if (response && response.success && response.status === 'processed') {
// Stop polling for this item
clearInterval(pollingTimers[cartItemKey]);
delete pollingTimers[cartItemKey];
$btn.html(proboObj.file_processed || 'Files Uploaded')
.removeClass('probo-uploader-processing probo-uploader-progress probo-uploader-queued')
.addClass('probo-uploader-processed');
} else {
let status = (response && response.status) ? String(response.status) : 'progress';
$btn.html((proboObj.upload_status || 'Status') + ': ' + status)
.removeClass('probo-uploader-processing probo-uploader-processed probo-uploader-queued probo-uploader-error probo-uploader-progress')
.addClass('probo-uploader-' + status);
}
refreshCheckoutState();
},
error: function() {
// On error, keep polling, but mark visually
$btn.html((proboObj.upload_status || 'Status') + ': error')
.removeClass('probo-uploader-processing probo-uploader-processed probo-uploader-progress probo-uploader-queued')
.addClass('probo-uploader-error');
refreshCheckoutState();
}
});
}
// Start polling ONLY for the clicked item
$(document).on('click', '.probo-upload-button', function() {
const $btn = $(this);
const $wrap = $btn.closest('.probo-upload-container');
const cartItemKey = $wrap.data('cart-item-key');
if (!cartItemKey) return;
// If already polling for this item, do nothing
if (pollingTimers[cartItemKey]) return;
// Immediate check, then set interval for THIS item
checkStatus(cartItemKey, $btn);
pollingTimers[cartItemKey] = setInterval(function() {
// Button may be re-rendered by fragments, try to reselect
const $currentBtn = $('.probo-upload-container[data-cart-item-key="'+ cartItemKey +'"] .probo-upload-button').first();
if ($currentBtn.length === 0) {
// If row disappears, stop its timer
clearInterval(pollingTimers[cartItemKey]);
delete pollingTimers[cartItemKey];
return;
}
checkStatus(cartItemKey, $currentBtn);
}, pollInterval);
// As soon as user starts an upload flow, gate checkout
refreshCheckoutState();
});
// If cart updates via fragments/AJAX, clear dead timers and recompute state
$(document.body).on('updated_wc_div updated_cart_totals', function() {
// Clean up timers whose rows no longer exist
Object.keys(pollingTimers).forEach(function(key) {
const exists = $('.probo-upload-container[data-cart-item-key="'+ key +'"]').length > 0;
if (!exists) {
clearInterval(pollingTimers[key]);
delete pollingTimers[key];
}
});
refreshCheckoutState();
});
// Initial gate when page is ready (if some rows already processed or not)
refreshCheckoutState();
});
### Server expectations (`probo_poll_callback`)
Make sure your AJAX callback reads `$_POST['cart_item_key']` and returns a per‑item status, e.g.:
wp_send_json_success([
'cart_item_key' => $cart_item_key,
'status' => $status, // "processed" | "progress" | "queued" | "error"
]);
This way each cart row’s button is independent, and checkout unlocks only when all present upload buttons report `processed`.
if(WC()->cart->is_empty()){
return;
}else{
foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
if(!empty($cart_item['probo_uploader_uploader_id'])){
$uploaderid=$cart_item['probo_uploader_uploader_id'];
if(!empty($uploaderid)){
$optionData=get_option('uploader_callback'.$uploaderid);
//if($optionData['status']=='processed'){
// ✅ Update cart item meta directly
WC()->cart->cart_contents[$cart_item_key]['probo_uploader_status'] = $optionData['status'];
WC()->cart->cart_contents[$cart_item_key]['probo_uploader_preview'] = $optionData['url'];
WC()->cart->cart_contents[$cart_item_key]['probo_uploader_external_id'] = $optionData['external_id'];
// ✅ Persist the cart changes
WC()->cart->set_session();
WC()->cart->maybe_set_cart_cookies();
//}
break;
}
}
}
echo wp_json_encode(['success'=>true,'status'=>$optionData['status']]);
}
exit;
}
Now adjust this
public function probo_poll_callback() {
// Optional: verify AJAX + nonce if you localize one
// if ( empty($_POST['nonce']) || ! wp_verify_nonce( $_POST['nonce'], 'probo_poll' ) ) {
// wp_send_json_error( ['message' => 'Invalid nonce'] );
// }
if ( ! WC()->cart || WC()->cart->is_empty() ) {
wp_send_json_error( ['message' => 'Cart is empty'] );
}
// Which cart row are we polling?
$cart_item_key = isset($_POST['cart_item_key'])
? wc_clean( wp_unslash( $_POST['cart_item_key'] ) )
: '';
if ( empty($cart_item_key) ) {
wp_send_json_error( ['message' => 'Missing cart_item_key'] );
}
$cart_contents = WC()->cart->get_cart();
if ( empty($cart_contents[ $cart_item_key ]) ) {
wp_send_json_error( ['message' => 'Cart item not found'] );
}
$cart_item = $cart_contents[ $cart_item_key ];
// Expect an uploader id saved on THIS line item
$uploader_id = isset($cart_item['probo_uploader_uploader_id'])
? sanitize_text_field( $cart_item['probo_uploader_uploader_id'] )
: '';
if ( empty($uploader_id) ) {
wp_send_json_error( [
'cart_item_key' => $cart_item_key,
'message' => 'No uploader id on this item'
] );
}
// Fetch latest status from your option (adjust source if needed)
$option_key = 'uploader_callback' . $uploader_id;
$optionData = get_option( $option_key );
// Normalize
$optionData = is_array($optionData) ? $optionData : [];
$status = isset($optionData['status']) ? sanitize_text_field($optionData['status']) : 'progress';
$preview_url = isset($optionData['url']) ? esc_url_raw($optionData['url']) : '';
$external_id = isset($optionData['external_id']) ? sanitize_text_field($optionData['external_id']) : '';
// Write back to the ONE cart row we’re polling
WC()->cart->cart_contents[ $cart_item_key ]['probo_uploader_status'] = $status;
WC()->cart->cart_contents[ $cart_item_key ]['probo_uploader_preview'] = $preview_url;
WC()->cart->cart_contents[ $cart_item_key ]['probo_uploader_external_id'] = $external_id;
// Persist
WC()->cart->set_session();
WC()->cart->maybe_set_cart_cookies();
wp_send_json_success( [
'cart_item_key' => $cart_item_key,
'status' => $status, // "processed" | "progress" | "queued" | "error"
'preview' => $preview_url, // optional
'external_id' => $external_id, // optional
] );
}
### Why this fixes it
– Targets the **exact** line item via `cart_item_key` (no looping/breaking on the first match).
– Updates only that row’s meta in the cart and **persists** it.
– Returns structured JSON your JS already expects (`status`, and optionally `preview`, `external_id`).
– Gives clear error messages for missing/invalid inputs so you can debug quickly.
If you added a nonce in your localized `proboObj`, uncomment the nonce check at the top and include it in your JS request.
if(WC()->cart->is_empty()){
return;
}else{
foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
if(!empty($cart_item['probo_uploader_uploader_id'])){
$uploaderid=$cart_item['probo_uploader_uploader_id'];
if(!empty($uploaderid)){
$optionData=get_option('uploader_callback'.$uploaderid);
//if($optionData['status']=='processed'){
// ✅ Update cart item meta directly
WC()->cart->cart_contents[$cart_item_key]['probo_uploader_status'] = $optionData['status'];
WC()->cart->cart_contents[$cart_item_key]['probo_uploader_preview'] = $optionData['url'];
WC()->cart->cart_contents[$cart_item_key]['probo_uploader_external_id'] = $optionData['external_id'];
// ✅ Persist the cart changes
WC()->cart->set_session();
WC()->cart->maybe_set_cart_cookies();
//}
break;
}
}
}
echo wp_json_encode(['success'=>true,'status'=>$optionData['status']]);
}
exit;
}
Dont add extra code just fix in function with no extra code
public function probo_poll_callback() {
if (WC()->cart->is_empty()) {
exit;
} else {
// Default to avoid "undefined variable" if nothing matches
$optionData = ['status' => 'progress'];
// If a specific cart_item_key is posted, handle ONLY that row
$posted_key = isset($_POST['cart_item_key']) ? wc_clean( wp_unslash($_POST['cart_item_key']) ) : '';
if ($posted_key && isset(WC()->cart->cart_contents[$posted_key])) {
$cart_item = WC()->cart->cart_contents[$posted_key];
if (!empty($cart_item['probo_uploader_uploader_id'])) {
$uploaderid = $cart_item['probo_uploader_uploader_id'];
if (!empty($uploaderid)) {
$optionData = (array) get_option('uploader_callback' . $uploaderid);
WC()->cart->cart_contents[$posted_key]['probo_uploader_status'] = $optionData['status'] ?? 'progress';
WC()->cart->cart_contents[$posted_key]['probo_uploader_preview'] = $optionData['url'] ?? '';
WC()->cart->cart_contents[$posted_key]['probo_uploader_external_id'] = $optionData['external_id'] ?? '';
WC()->cart->set_session();
WC()->cart->maybe_set_cart_cookies();
}
}
} else {
// Fallback: previous behavior (first matching item)
foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
if (!empty($cart_item['probo_uploader_uploader_id'])) {
$uploaderid = $cart_item['probo_uploader_uploader_id'];
if (!empty($uploaderid)) {
$optionData = (array) get_option('uploader_callback' . $uploaderid);
WC()->cart->cart_contents[$cart_item_key]['probo_uploader_status'] = $optionData['status'] ?? 'progress';
WC()->cart->cart_contents[$cart_item_key]['probo_uploader_preview'] = $optionData['url'] ?? '';
WC()->cart->cart_contents[$cart_item_key]['probo_uploader_external_id'] = $optionData['external_id'] ?? '';
WC()->cart->set_session();
WC()->cart->maybe_set_cart_cookies();
break;
}
}
}
}
echo wp_json_encode(['success' => true, 'status' => $optionData['status'] ?? 'progress']);
}
exit;
}
new
jQuery(document).ready(function($) {
const pollInterval = 6000; // 3 seconds
const pollingTimers = {}; // keyed by cart_item_key
// Helper: enable/disable checkout depending on buttons' states
function refreshCheckoutState() {
const $buttons = $('.probo-upload-button');
if ($buttons.length === 0) return; // nothing to gate
const allProcessed = $buttons.toArray().every(btn => $(btn).hasClass('probo-uploader-processed'));
const $checkout = $('.checkout-button');
// if (allProcessed) {
// $checkout.css({'opacity': 1, 'cursor': 'pointer', 'pointer-events': 'auto'})
// .text('Proceed to Checkout');
// } else {
// $checkout.css({'opacity': 0.5, 'cursor': 'not-allowed', 'pointer-events': 'none'})
// .text(proboObj.upload_in_progress || 'Upload in progress…');
// }
}
// Core: check status for a specific cart item
function checkStatus(cartItemKey, $btn) {
// Defensive: make sure we always show a working state during call
$btn.html(proboObj.upload_in_progress)
.addClass('probo-uploader-processing')
.removeClass('probo-uploader-processed probo-uploader-progress probo-uploader-queued probo-uploader-error');
$.ajax({
url: proboObj.ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'probo_poll_callback',
cart_item_key: cartItemKey // 0;
if (!exists) {
clearInterval(pollingTimers[key]);
delete pollingTimers[key];
}
});
refreshCheckoutState();
});
// Initial gate when page is ready (if some rows already processed or not)
refreshCheckoutState();
});
changing button text to Adding to cart some time
### Fix (pick 1–2 of these)
**A. Use a unique class; drop Woo‑targeted classes**
– In your PHP, change the anchor class from:
class="button probo-upload-button wp-element-button"
to:
class="probo-upload-button probo-btn"
(keep your own styles; don’t use `wp-element-button` or `add_to_cart_button`)
**B. Stop other handlers from running on click**
In your JS click handler, add this at the top:
$(document).on('click', '.probo-upload-button', function(e) {
e.stopImmediatePropagation(); // stop Woo handlers
// open in new tab yourself to avoid theme JS interfering
const href = this.href;
if (href) {
window.open(href, '_blank', 'noopener');
e.preventDefault();
}
// ...existing polling code...
});
**C. (Optional) Also remove the generic `.button` class**
Some themes bind to `.button` too. If A alone doesn’t fix it, drop `.button` as well and style via your own CSS.
—
Small nit: your `pollInterval` is `6000` ms but the comment says “3 seconds”. Either set it to `3000` or update the comment so it’s not confusing.