Multiple upload buttons code

User
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']);
$button = 'Upload Files';
echo $button;
}

}
make it to handle muliple loine item has multile upload files button

Assistant
Here’s a drop‑in version that renders **one Upload button per unit** (qty) and also supports **multiple URLs per line item** if you store them as an array in `probo_uploader_urls`. It keeps your original hook/signature.

// 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.

User
NO in cart mulitple cart item can have uplaod button
Assistant
Got it — you want **each cart line item** (even if it’s the same product added multiple times) to show **its own Upload button**. Use this pattern:

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.

User
we have added this
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);
});
});

Assistant
Perfect—since each Upload button now carries its own `cart_item_key` (via the URL and the `data-cart-item-key` wrapper), here’s a drop‑in JS update that:

– 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`.

User
public function probo_poll_callback() {
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

Assistant
Here’s a safe, per‑item version of your AJAX callback that matches the JS (it reads `cart_item_key`, updates only that line item, and returns rich data). Drop this in:

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.

User
public function probo_poll_callback() {
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

Assistant
Here’s your same function, just fixed to respect the clicked line item (`cart_item_key`) and avoid “undefined variable” when nothing is found. No extra helpers added.

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;
}
User
why this
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

Assistant
Because WooCommerce (or your theme) hooks global click handlers on generic button classes like `.wp-element-button` / `.button`. When you click your upload link, those handlers sometimes fire and swap the label to “Adding to cart…”.

### 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.

Leave a Comment

Your email address will not be published. Required fields are marked *