import $ from 'jquery' import { notification } from 'ant-design-vue' import { __ } from '@wordpress/i18n' import { getI18n, l10nObj } from '@/helper' const loadedObject = l10nObj() const STOCK_OUTOFSTOCK = 'outofstock' const PRODUCT_TYPE_VARIABLE = 'variable' export interface CartItem { id?: number collection_id: number quantity: number product: { id: number stock: string type: string is_purchasable?: boolean needs_configuration?: boolean } } type GetItemKeyFn = (item: CartItem) => number export interface AddToCartResult { successCount: number failureCount: number } /** * Determine whether a wishlist item can be added to cart. * Non-purchasable items (e.g. catalog-mode plugins), out-of-stock items, * variable products without a selected variation, and products that must be * configured on the product page (e.g. PPOM add-ons) are ineligible. * * @param item Wishlist item to evaluate. * @param selectedVariation Map of item key to chosen variation id. * @param getItemKey Resolves the lookup key used against `selectedVariation`. * @return `true` when the item is eligible to be added to cart. * @since 1.0.4 * @since 1.0.9 Added `is_purchasable` check; excludes non-purchasable products (e.g. catalog-mode). */ function isItemEligible( item: CartItem, selectedVariation: Record, getItemKey: GetItemKeyFn ): boolean { if (item.product.is_purchasable === false) return false if (item.product.stock === STOCK_OUTOFSTOCK) return false if (item.product.needs_configuration) return false if (item.product.type === PRODUCT_TYPE_VARIABLE && !selectedVariation[getItemKey(item)]) return false return true } /** * Filter wishlist items eligible to be added to cart. * Excludes items ineligible per `isItemEligible` (non-purchasable, out-of-stock, * unconfigured, or missing a variation selection). * * @param items Wishlist items to evaluate. * @param selectedVariation Map of item key to chosen variation id. * @param getItemKey Resolves the lookup key used against `selectedVariation`. * @return Items that pass `isItemEligible`. * @since 1.0.4 */ export function filterEligibleCartItems( items: CartItem[], selectedVariation: Record, getItemKey: GetItemKeyFn ): CartItem[] { return items.filter((item: CartItem) => isItemEligible(item, selectedVariation, getItemKey)) } /** * Filter wishlist items that are both selected and eligible to be added to cart. */ export function filterSelectedCartItems( items: CartItem[], selectedItems: number[], selectedVariation: Record, getItemKey: GetItemKeyFn ): CartItem[] { return items.filter((item: CartItem) => { if (!selectedItems.includes(getItemKey(item))) return false return isItemEligible(item, selectedVariation, getItemKey) }) } /** * Send a single add-to-cart AJAX request. * Resolves with `true` on success and `false` on failure, so callers can * aggregate per-item outcomes without relying on rejected promises. * * The endpoint (`stwlite_add_to_cart`) replies via `wp_send_json()`, which * sends HTTP 200 even when the add fails (`{ status: 'error' }`). Inspecting * the JSON `status` is therefore required — a raw HTTP-200 check would count * failed adds as successes and silently drop items. * * @param item Wishlist item to add. * @param selectedVariation Map of item key to selected variation id. * @param getItemKey Resolves the lookup key used against `selectedVariation`. * @return Resolves `true` when the item was accepted by the server, `false` on any failure. * @since 1.0.4 * @since 1.0.8 Changed to inspect `res.status` for server-side failures under HTTP 200. */ export function addSingleCartItem( item: CartItem, selectedVariation: Record, getItemKey: GetItemKeyFn ): Promise { return new Promise((resolve) => { $.ajax({ type: 'POST', url: loadedObject.ajax.url, dataType: 'json', data: { action: 'stwlite_add_to_cart', nonce: loadedObject.ajax.nonce, collection_id: item.collection_id, product_id: item.product.id, quantity: item.quantity, product_type: item.product.type, variation_id: selectedVariation[getItemKey(item)] || 0, }, success: (res) => resolve(res?.status !== 'error'), error: () => resolve(false), }) }) } /** * Add items to the WooCommerce cart one-by-one. * * Requests must run sequentially because WooCommerce persists the cart in * the PHP session; parallel requests each load the same starting cart, append * their own item, and save the session back, causing earlier additions to be * silently dropped (classic lost-update race). */ export async function addCartItemsSequentially( items: CartItem[], selectedVariation: Record, getItemKey: GetItemKeyFn ): Promise { let successCount = 0 let failureCount = 0 for (const item of items) { const ok = await addSingleCartItem(item, selectedVariation, getItemKey) if (ok) { successCount++ } else { failureCount++ } } return { successCount, failureCount } } /** * Surface the outcome of a bulk add-to-cart as a user-facing notification. * Distinguishes all-failed, partial-failure, and all-succeeded so users are * not misled when the underlying requests silently drop an item. */ export function notifyAddToCartResult(successCount: number, failureCount: number): void { if (successCount === 0 && failureCount > 0) { notification.error({ message: getI18n('add_to_cart_failed', __('Failed to add items to cart. Please try again.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-error-notification', }) return } if (failureCount > 0) { notification.warning({ message: getI18n('some_items_failed', __('Some items could not be added to cart.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-warning-notification', }) return } notification.success({ message: getI18n('all_added_to_cart', __('Items added to cart.', 'saveto-wishlist-lite-for-woocommerce')), class: 'wishlist-lists-success-notification', }) } /** * Resolve the post-bulk-add redirect URL configured in admin, or `undefined` * when the customer should stay on the wishlist page. URLs come from * `wc_get_cart_url()` / `wc_get_checkout_url()`, so the value is always a * trusted WooCommerce route — no user input flows into it. * * The `'checkout'` / `'cart'` values mirror the `stwlite_settings_add_all_redirect` * option defined in `Classes/Admin/Settings/Controls.php` (its canonical source), * and consumed server-side in `Classes/Frontend/Wishlist.php`. * * @return The WooCommerce cart or checkout URL when a redirect is configured, * or `undefined` to stay on the wishlist page. * @since 1.0.8 */ export function getAddAllRedirectUrl(): string | undefined { const setting = loadedObject?.wishlistOptions?.addAllRedirect if (setting === 'checkout') return loadedObject?.pageUrls?.checkoutUrl if (setting === 'cart') return loadedObject?.pageUrls?.cartUrl return undefined } /** * Finalize a bulk add-to-cart flow: show the outcome notification, and when * at least one item succeeded, refresh WooCommerce cart fragments and the * wishlist view. When `redirectUrl` is provided and every item was added * successfully, redirect there instead — the destination already reflects * the new cart contents, so the in-page refresh is redundant. * * @param result Aggregated success/failure counts from `addCartItemsSequentially`. * @param refreshWishlist Callback to reload wishlist data after a successful add. * @param redirectUrl When provided and all items succeeded, navigate here instead of refreshing in-place. * @since 1.0.4 * @since 1.0.8 Added the `redirectUrl` parameter for the post-bulk-add redirect. */ export function finalizeAddToCart( result: AddToCartResult, refreshWishlist: () => void, redirectUrl?: string ): void { if (redirectUrl && result.successCount > 0 && result.failureCount === 0) { window.location.href = redirectUrl return } notifyAddToCartResult(result.successCount, result.failureCount) if (result.successCount > 0) { document.body.dispatchEvent(new CustomEvent('wc_fragment_refresh')) refreshWishlist() } }