db = $db;
// New per-card add-to-cart / remove-line endpoints (1.5.0)
add_action('wp_ajax_studiou_wcfpp_add_file_to_cart', array($this, 'ajax_add_file_to_cart'));
add_action('wp_ajax_nopriv_studiou_wcfpp_add_file_to_cart', array($this, 'ajax_add_file_to_cart'));
add_action('wp_ajax_studiou_wcfpp_remove_cart_line', array($this, 'ajax_remove_cart_line'));
add_action('wp_ajax_nopriv_studiou_wcfpp_remove_cart_line', array($this, 'ajax_remove_cart_line'));
// Capture file data from WC session on add-to-cart
add_filter('woocommerce_add_cart_item_data', array($this, 'add_cart_item_data'), 10, 3);
// Display uploaded file info in cart
add_filter('woocommerce_get_item_data', array($this, 'display_cart_item_data'), 10, 2);
// Replace cart item thumbnail with uploaded image (classic cart)
add_filter('woocommerce_cart_item_thumbnail', array($this, 'cart_item_thumbnail'), 10, 3);
// Show uploaded photo preview directly after item name in cart (classic cart)
add_action('woocommerce_after_cart_item_name', array($this, 'show_cart_item_photo_preview'), 10, 2);
// Block cart (Store API) reads the thumbnail from $cart_item['data']->get_image_id(). Stamp
// the uploaded attachment onto a per-line clone of the product so each cart line shows its
// OWN photo — even when several lines share the same variation_id.
add_filter('woocommerce_get_cart_item_from_session', array($this, 'hydrate_cart_item_image'), 10, 3);
// Save file reference to order item
add_action('woocommerce_checkout_create_order_line_item', array($this, 'save_order_item_meta'), 10, 4);
// Link file records to order after checkout (classic + block checkout)
add_action('woocommerce_checkout_order_processed', array($this, 'link_files_to_order'), 10, 3);
add_action('woocommerce_store_api_checkout_order_processed', array($this, 'link_files_to_order_from_store_api'), 10, 1);
add_action('woocommerce_thankyou', array($this, 'link_files_to_order_fallback'), 10, 1);
// Replace order item thumbnail with uploaded image (admin)
add_filter('woocommerce_admin_order_item_thumbnail', array($this, 'admin_order_item_thumbnail'), 10, 3);
// Add download link to order item in admin
add_action('woocommerce_after_order_itemmeta', array($this, 'admin_order_item_download_link'), 10, 3);
}
public function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
// Check if this product (or its parent) is FPP enabled
$fpp_product_id = $product_id;
if (!Studiou_WC_FPP_Product::is_fpp_product($product_id)) {
$product = wc_get_product($product_id);
if ($product && $product->get_parent_id() && Studiou_WC_FPP_Product::is_fpp_product($product->get_parent_id())) {
$fpp_product_id = $product->get_parent_id();
} else {
return $cart_item_data;
}
}
// 1) New path (1.5.0+): per-card Add-to-Cart pre-stamped the file_record_id.
if (!empty($cart_item_data['studiou_fpp_file_record_id'])) {
$record = $this->db->get_file_record((int) $cart_item_data['studiou_fpp_file_record_id']);
if ($record && (int) $record->order_id === 0) {
$thumb = wp_get_attachment_image_src((int) $record->attachment_id, 'thumbnail');
$cart_item_data['studiou_fpp_attachment_id'] = (int) $record->attachment_id;
$cart_item_data['studiou_fpp_file_name'] = (string) $record->file_name;
$cart_item_data['studiou_fpp_thumb_url'] = $thumb ? $thumb[0] : '';
$this->db->update_file_record((int) $record->id, array(
'variation_id' => (int) ($variation_id ?: $product_id),
));
return $cart_item_data;
}
// Stamped id is bogus — fall through to legacy resolver
unset($cart_item_data['studiou_fpp_file_record_id']);
}
// 2) Legacy single-file path (shortcode / non-card callers) — fall back to the
// oldest unbound upload in the current batch.
$resolved = Studiou_WC_FPP_Single_Product::resolve_uploaded_file($this->db);
if (!$resolved) {
return $cart_item_data;
}
$cart_item_data['studiou_fpp_attachment_id'] = (int) $resolved['attachment_id'];
$cart_item_data['studiou_fpp_file_record_id'] = (int) $resolved['file_record_id'];
$cart_item_data['studiou_fpp_file_name'] = $resolved['file_name'];
$cart_item_data['studiou_fpp_thumb_url'] = $resolved['thumb_url'];
$this->db->update_file_record((int) $resolved['file_record_id'], array(
'variation_id' => (int) ($variation_id ?: $product_id),
));
return $cart_item_data;
}
/**
* AJAX: add one (upload × variant × qty) triple as a new cart line.
* Called from the upload-card "Add to Cart" button.
*/
public function ajax_add_file_to_cart() {
check_ajax_referer('studiou-wcfpp-front-nonce', 'nonce');
$file_record_id = isset($_POST['file_record_id']) ? absint($_POST['file_record_id']) : 0;
$variation_id = isset($_POST['variation_id']) ? absint($_POST['variation_id']) : 0;
$quantity = isset($_POST['quantity']) ? max(1, absint($_POST['quantity'])) : 1;
if (!$file_record_id || !$variation_id) {
wp_send_json_error(array('message' => __('Invalid request.', 'studiou-wc-free-photo-product')));
return;
}
$record = $this->db->get_file_record($file_record_id);
if (!$record || (int) $record->order_id !== 0) {
wp_send_json_error(array('message' => __('File not found.', 'studiou-wc-free-photo-product')));
return;
}
// Ownership check: record must belong to the caller's batch
$token = Studiou_WC_FPP_Upload::get_batch_token();
if (!empty($token) && !empty($record->batch_token) && $token !== $record->batch_token) {
wp_send_json_error(array('message' => __('Permission denied.', 'studiou-wc-free-photo-product')));
return;
}
$variation = wc_get_product($variation_id);
if (!$variation || $variation->get_type() !== 'variation') {
wp_send_json_error(array('message' => __('Invalid variant.', 'studiou-wc-free-photo-product')));
return;
}
$parent_id = $variation->get_parent_id();
if (!Studiou_WC_FPP_Product::is_fpp_product($parent_id)) {
wp_send_json_error(array('message' => __('Invalid product.', 'studiou-wc-free-photo-product')));
return;
}
if (function_exists('wc_load_cart')) {
wc_load_cart();
}
if (!WC()->cart) {
wp_send_json_error(array('message' => __('Cart unavailable.', 'studiou-wc-free-photo-product')));
return;
}
$variation_attrs = $variation->get_variation_attributes();
$cart_item_data = array('studiou_fpp_file_record_id' => (int) $file_record_id);
$cart_key = WC()->cart->add_to_cart($parent_id, $quantity, $variation_id, $variation_attrs, $cart_item_data);
if (!$cart_key) {
// WC emits notices on failure — surface the first error if available
$notices = function_exists('wc_get_notices') ? wc_get_notices('error') : array();
$msg = __('Could not add to cart.', 'studiou-wc-free-photo-product');
if (!empty($notices[0]['notice'])) {
$msg = wp_strip_all_tags($notices[0]['notice']);
}
if (function_exists('wc_clear_notices')) {
wc_clear_notices();
}
wp_send_json_error(array('message' => $msg));
return;
}
wp_send_json_success(array(
'cart_key' => $cart_key,
'overview_html' => $this->render_overview_html($parent_id),
));
}
/**
* AJAX: remove one cart line (from the order-overview panel).
* Does NOT delete the upload — the × on the upload card handles that.
*/
public function ajax_remove_cart_line() {
check_ajax_referer('studiou-wcfpp-front-nonce', 'nonce');
$cart_item_key = isset($_POST['cart_item_key']) ? sanitize_text_field($_POST['cart_item_key']) : '';
if (!$cart_item_key) {
wp_send_json_error(array('message' => __('Invalid request.', 'studiou-wc-free-photo-product')));
return;
}
if (function_exists('wc_load_cart')) {
wc_load_cart();
}
if (!WC()->cart) {
wp_send_json_error(array('message' => __('Cart unavailable.', 'studiou-wc-free-photo-product')));
return;
}
$item = WC()->cart->get_cart_item($cart_item_key);
if (!$item) {
wp_send_json_error(array('message' => __('Cart line not found.', 'studiou-wc-free-photo-product')));
return;
}
$product_id = (int) ($item['product_id'] ?? 0);
WC()->cart->remove_cart_item($cart_item_key);
wp_send_json_success(array(
'overview_html' => $this->render_overview_html($product_id),
));
}
/**
* Render the order-overview panel for the given product. Returns the HTML
* so the JS can replace the in-page panel.
*/
private function render_overview_html($product_id) {
if (!$product_id) {
return '';
}
$cart = WC()->cart;
if (!$cart) {
return '';
}
// Force a totals recalculation so the lines reflect the tier discount
$cart->calculate_totals();
ob_start();
include STUDIOU_WCFPP_PLUGIN_DIR . 'views/single-product-order-overview.php';
return ob_get_clean();
}
public function cart_item_thumbnail($thumbnail, $cart_item, $cart_item_key) {
$url = $this->get_cart_item_thumb_url($cart_item);
if ($url) {
$alt = isset($cart_item['studiou_fpp_file_name']) ? esc_attr($cart_item['studiou_fpp_file_name']) : '';
return '';
}
return $thumbnail;
}
/**
* Hydrate each cart line with its own product instance carrying the uploaded image_id.
*
* Fires on session restore (both classic cart render and Store API block cart). Because the
* product instance is cloned per line, setting `set_image_id()` only affects THIS line's
* display — multiple cart lines sharing the same variation_id still render their own photos.
*
* Previous implementation hooked `woocommerce_product_get_image_id` which fires per-product
* and couldn't tell which line was currently being rendered; when two lines shared a
* variation it returned the first match for both, causing mismatched thumbs in block cart.
*/
public function hydrate_cart_item_image($cart_item, $values, $cart_item_key) {
if (empty($cart_item['studiou_fpp_attachment_id'])) {
return $cart_item;
}
if (empty($cart_item['data']) || !is_object($cart_item['data'])) {
return $cart_item;
}
if (!method_exists($cart_item['data'], 'set_image_id')) {
return $cart_item;
}
$product = clone $cart_item['data'];
$product->set_image_id((int) $cart_item['studiou_fpp_attachment_id']);
$cart_item['data'] = $product;
return $cart_item;
}
public function display_cart_item_data($item_data, $cart_item) {
if (isset($cart_item['studiou_fpp_file_name'])) {
$item_data[] = array(
'name' => __('Uploaded Photo', 'studiou-wc-free-photo-product'),
'value' => esc_html($cart_item['studiou_fpp_file_name']),
'display' => '',
);
}
return $item_data;
}
public function show_cart_item_photo_preview($cart_item, $cart_item_key) {
$url = $this->get_cart_item_thumb_url($cart_item);
if ($url) {
echo '
'; echo ''; echo ' '; echo esc_html__('Download uploaded photo', 'studiou-wc-free-photo-product'); echo ''; if ($file_name) { echo ' (' . esc_html($file_name) . ')'; } echo '
'; } }