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 '' . $alt . ''; } 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 '' . esc_attr($cart_item['studiou_fpp_file_name'] ?? '') . ''; echo '
'; } } private function get_cart_item_thumb_url($cart_item) { // 1. Use stored URL from session (most reliable) if (!empty($cart_item['studiou_fpp_thumb_url'])) { return $cart_item['studiou_fpp_thumb_url']; } // 2. Try WP attachment lookup if (!empty($cart_item['studiou_fpp_attachment_id'])) { $att_id = (int) $cart_item['studiou_fpp_attachment_id']; $thumb = wp_get_attachment_image_src($att_id, 'thumbnail'); if ($thumb) { return $thumb[0]; } $url = wp_get_attachment_url($att_id); if ($url) { return $url; } } return ''; } public function save_order_item_meta($item, $cart_item_key, $values, $order) { if (isset($values['studiou_fpp_attachment_id'])) { $item->add_meta_data('_studiou_fpp_attachment_id', $values['studiou_fpp_attachment_id']); } if (isset($values['studiou_fpp_file_record_id'])) { $item->add_meta_data('_studiou_fpp_file_record_id', $values['studiou_fpp_file_record_id']); } if (isset($values['studiou_fpp_file_name'])) { $item->add_meta_data('_studiou_fpp_file_name', $values['studiou_fpp_file_name']); $item->add_meta_data(__('Uploaded Photo', 'studiou-wc-free-photo-product'), $values['studiou_fpp_file_name']); } if (isset($values['studiou_fpp_thumb_url'])) { $item->add_meta_data('_studiou_fpp_thumb_url', $values['studiou_fpp_thumb_url']); } } public function link_files_to_order($order_id, $posted_data, $order) { $this->do_link_files($order); } /** * Block checkout fires this hook with only the $order parameter. */ public function link_files_to_order_from_store_api($order) { $this->do_link_files($order); } /** * Fallback: runs on thank-you page to catch any missed linking. */ public function link_files_to_order_fallback($order_id) { $order = wc_get_order($order_id); if ($order) { $this->do_link_files($order); } } private function do_link_files($order) { if (!$order) { return; } $order_id = $order->get_id(); foreach ($order->get_items() as $item_id => $item) { $file_record_id = $item->get_meta('_studiou_fpp_file_record_id'); if ($file_record_id) { // Only link if not already linked $record = $this->db->get_file_record((int) $file_record_id); if ($record && (int) $record->order_id === 0) { $this->db->link_file_to_order((int) $file_record_id, $order_id, $item_id); } } } } public function admin_order_item_thumbnail($thumbnail, $item_id, $item) { // Try stored URL first $thumb_url = $item->get_meta('_studiou_fpp_thumb_url'); if ($thumb_url) { return ''; } // Fallback to attachment lookup $attachment_id = $item->get_meta('_studiou_fpp_attachment_id'); if ($attachment_id) { $image = wp_get_attachment_image($attachment_id, 'thumbnail'); if ($image) { return $image; } $url = wp_get_attachment_url($attachment_id); if ($url) { return ''; } } return $thumbnail; } public function admin_order_item_download_link($item_id, $item, $product) { $attachment_id = $item->get_meta('_studiou_fpp_attachment_id'); if (!$attachment_id) { return; } $file_url = wp_get_attachment_url($attachment_id); $file_name = $item->get_meta('_studiou_fpp_file_name'); if (!$file_url) { return; } $file_record_id = $item->get_meta('_studiou_fpp_file_record_id'); // Build download URL that also marks file as downloaded $download_url = add_query_arg(array( 'studiou_fpp_download_file' => 1, 'file_record_id' => $file_record_id ?: 0, 'attachment_id' => $attachment_id, '_wpnonce' => wp_create_nonce('studiou-fpp-download-file'), ), admin_url('admin.php')); echo '

'; echo ''; echo ' '; echo esc_html__('Download uploaded photo', 'studiou-wc-free-photo-product'); echo ''; if ($file_name) { echo ' (' . esc_html($file_name) . ')'; } echo '

'; } }