diff --git a/packages/hydrogen-react/src/getProductOptions.test.ts b/packages/hydrogen-react/src/getProductOptions.test.ts index f58b19910..8a5350988 100644 --- a/packages/hydrogen-react/src/getProductOptions.test.ts +++ b/packages/hydrogen-react/src/getProductOptions.test.ts @@ -1,13 +1,68 @@ import {afterEach, describe, expect, it, vi} from 'vitest'; -import {checkProductParam} from './getProductOptions.js'; +import {checkProductParam, getAdjacentAndFirstAvailableVariants} from './getProductOptions.js'; import {Product} from './storefront-api-types.js'; -const ERROR_MSG_START = '[h2:warn:getProductOptions] product.'; +const ERROR_MSG_START = '[h2:error:getProductOptions] product.'; const ERROR_MSG_END = ' is missing. Make sure you query for this field from the Storefront API.'; +describe('getAdjacentAndFirstAvailableVariants', () => { + it('returns the correct number of variants found', () => { + const variants = getAdjacentAndFirstAvailableVariants({ + options: [ + { + optionValues: [ + { + firstSelectableVariant: { + id: "snowboard", + selectedOptions: [ + { + name: "Color", + value: "Turquoise" + }, + ], + }, + } + ], + } + ], + selectedOrFirstAvailableVariant: { + id: "snowboard", + selectedOptions: [ + { + name: "Color", + value: "Turquoise" + }, + ], + }, + adjacentVariants: [{ + id: "snowboard-2", + selectedOptions: [ + { + name: "Color", + value: "Ember" + }, + ], + }], + } as unknown as Product); + + expect(variants.length).toBe(2); + expect(variants[0]).toMatchInlineSnapshot(` + { + "id": "snowboard", + "selectedOptions": [ + { + "name": "Color", + "value": "Turquoise", + }, + ], + } + `); + }); +}); + describe('checkProductParam', () => { beforeEach(() => { - vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.spyOn(console, 'error').mockImplementation(() => {}); }) afterEach(() => { @@ -46,23 +101,49 @@ describe('checkProductParam', () => { encodedVariantAvailability: '', selectedOrFirstAvailableVariant: null, adjacentVariants: [], + } as unknown as Product, true); + + expect(console.error).toHaveBeenCalledTimes(0); + }); + + it('logs nothing when provided a valid product input without checkAll flag', () => { + checkProductParam({ + options: [ + { + optionValues: [ + { + firstSelectableVariant: { + id: "snowboard", + selectedOptions: [ + { + name: "Color", + value: "Turquoise" + }, + ], + }, + } + ], + } + ], + selectedOrFirstAvailableVariant: null, + adjacentVariants: [], } as unknown as Product); - expect(console.warn).toHaveBeenCalledTimes(0); + expect(console.error).toHaveBeenCalledTimes(0); }); it('logs warnings for each missing field when provided an invalid product input', () => { checkProductParam({ id: "snowboard", - } as unknown as Product) - - expect(console.warn).toHaveBeenCalledTimes(6); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}handle${ERROR_MSG_END}`); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}options${ERROR_MSG_END}`); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}encodedVariantExistence${ERROR_MSG_END}`); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}encodedVariantAvailability${ERROR_MSG_END}`); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}selectedOrFirstAvailableVariant${ERROR_MSG_END}`); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}adjacentVariants${ERROR_MSG_END}`); + } as unknown as Product, true) + + expect(console.error).toHaveBeenCalledTimes(6); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}handle${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}options${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}encodedVariantExistence${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}encodedVariantAvailability${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}selectedOrFirstAvailableVariant${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}adjacentVariants${ERROR_MSG_END}`); }); it('logs warnings when provided an invalid options input - missing optionValues', () => { @@ -78,10 +159,10 @@ describe('checkProductParam', () => { encodedVariantAvailability: '', selectedOrFirstAvailableVariant: null, adjacentVariants: [], - } as unknown as Product) + } as unknown as Product, true) - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues${ERROR_MSG_END}`); }); it('logs warnings when provided an invalid options input - missing optionValues.name', () => { @@ -111,10 +192,10 @@ describe('checkProductParam', () => { encodedVariantAvailability: '', selectedOrFirstAvailableVariant: null, adjacentVariants: [], - } as unknown as Product) + } as unknown as Product, true) - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.name${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.name${ERROR_MSG_END}`); }); it('logs warnings when provided an invalid options input - missing optionValues.firstSelectableVariant', () => { @@ -139,10 +220,10 @@ describe('checkProductParam', () => { encodedVariantAvailability: '', selectedOrFirstAvailableVariant: null, adjacentVariants: [], - } as unknown as Product) + } as unknown as Product, true) - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.firstSelectableVariant${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.firstSelectableVariant${ERROR_MSG_END}`); }); it('logs warnings when provided an invalid options input - missing optionValues.firstSelectableVariant.product.handle', () => { @@ -171,10 +252,10 @@ describe('checkProductParam', () => { encodedVariantAvailability: '', selectedOrFirstAvailableVariant: null, adjacentVariants: [], - } as unknown as Product) + } as unknown as Product, true) - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.firstSelectableVariant.product.handle${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.firstSelectableVariant.product.handle${ERROR_MSG_END}`); }); it('logs warnings when provided an invalid options input - missing optionValues.firstSelectableVariant.product.selectedOptions', () => { @@ -200,10 +281,10 @@ describe('checkProductParam', () => { encodedVariantAvailability: '', selectedOrFirstAvailableVariant: null, adjacentVariants: [], - } as unknown as Product) + } as unknown as Product, true) - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.firstSelectableVariant.selectedOptions${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.firstSelectableVariant.selectedOptions${ERROR_MSG_END}`); }); it('logs warnings when provided an invalid options input - missing optionValues.firstSelectableVariant.product.selectedOptions.name', () => { @@ -234,10 +315,10 @@ describe('checkProductParam', () => { encodedVariantAvailability: '', selectedOrFirstAvailableVariant: null, adjacentVariants: [], - } as unknown as Product) + } as unknown as Product, true) - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.firstSelectableVariant.selectedOptions.name${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.firstSelectableVariant.selectedOptions.name${ERROR_MSG_END}`); }); it('logs warnings when provided an invalid options input - missing optionValues.firstSelectableVariant.product.selectedOptions.value', () => { @@ -268,10 +349,10 @@ describe('checkProductParam', () => { encodedVariantAvailability: '', selectedOrFirstAvailableVariant: null, adjacentVariants: [], - } as unknown as Product) + } as unknown as Product, true) - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.firstSelectableVariant.selectedOptions.value${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}options.optionValues.firstSelectableVariant.selectedOptions.value${ERROR_MSG_END}`); }); it('logs warnings when product.selectedOrFirstAvailableVariant is available but is invalid - missing selectedOrFirstAvailableVariant.product.handle', () => { @@ -313,10 +394,10 @@ describe('checkProductParam', () => { ] }, adjacentVariants: [], - } as unknown as Product); + } as unknown as Product, true); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}selectedOrFirstAvailableVariant.product.handle${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}selectedOrFirstAvailableVariant.product.handle${ERROR_MSG_END}`); }); it('logs warnings when product.selectedOrFirstAvailableVariant is available but is invalid - missing selectedOrFirstAvailableVariant.selectedOptions', () => { @@ -355,10 +436,10 @@ describe('checkProductParam', () => { }, }, adjacentVariants: [], - } as unknown as Product); + } as unknown as Product, true); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}selectedOrFirstAvailableVariant.selectedOptions${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}selectedOrFirstAvailableVariant.selectedOptions${ERROR_MSG_END}`); }); it('logs warnings when product.selectedOrFirstAvailableVariant is available but is invalid - missing selectedOrFirstAvailableVariant.selectedOptions.name', () => { @@ -402,10 +483,10 @@ describe('checkProductParam', () => { ] }, adjacentVariants: [], - } as unknown as Product); + } as unknown as Product, true); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}selectedOrFirstAvailableVariant.selectedOptions.name${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}selectedOrFirstAvailableVariant.selectedOptions.name${ERROR_MSG_END}`); }); it('logs warnings when product.selectedOrFirstAvailableVariant is available but is invalid - missing selectedOrFirstAvailableVariant.selectedOptions.value', () => { @@ -449,10 +530,10 @@ describe('checkProductParam', () => { ] }, adjacentVariants: [], - } as unknown as Product); + } as unknown as Product, true); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}selectedOrFirstAvailableVariant.selectedOptions.value${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}selectedOrFirstAvailableVariant.selectedOptions.value${ERROR_MSG_END}`); }); it('logs warnings when product.adjacentVariants is available but is invalid - missing adjacentVariants.product.handle', () => { @@ -494,10 +575,10 @@ describe('checkProductParam', () => { }, ] }], - } as unknown as Product); + } as unknown as Product, true); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}adjacentVariants.product.handle${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}adjacentVariants.product.handle${ERROR_MSG_END}`); }); it('logs warnings when product.adjacentVariants is available but is invalid - missing adjacentVariants.selectedOptions', () => { @@ -536,10 +617,10 @@ describe('checkProductParam', () => { handle: "snowboard" }, }], - } as unknown as Product); + } as unknown as Product, true); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}adjacentVariants.selectedOptions${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}adjacentVariants.selectedOptions${ERROR_MSG_END}`); }); it('logs warnings when product.adjacentVariants is available but is invalid - missing adjacentVariants.selectedOptions.name', () => { @@ -583,10 +664,10 @@ describe('checkProductParam', () => { }, ] }], - } as unknown as Product); + } as unknown as Product, true); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}adjacentVariants.selectedOptions.name${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}adjacentVariants.selectedOptions.name${ERROR_MSG_END}`); }); it('logs warnings when product.adjacentVariants is available but is invalid - missing adjacentVariants.selectedOptions.value', () => { @@ -630,9 +711,9 @@ describe('checkProductParam', () => { }, ] }], - } as unknown as Product); + } as unknown as Product, true); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith(`${ERROR_MSG_START}adjacentVariants.selectedOptions.value${ERROR_MSG_END}`); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(`${ERROR_MSG_START}adjacentVariants.selectedOptions.value${ERROR_MSG_END}`); }); }); diff --git a/packages/hydrogen-react/src/getProductOptions.ts b/packages/hydrogen-react/src/getProductOptions.ts index ce1a11714..103e561e7 100644 --- a/packages/hydrogen-react/src/getProductOptions.ts +++ b/packages/hydrogen-react/src/getProductOptions.ts @@ -1,5 +1,7 @@ +import { validate } from "graphql"; import {decodeEncodedVariant, isOptionValueCombinationInEncodedVariant } from "./optionValueDecoder"; -import type {Product, ProductOption, ProductOptionValue, ProductVariant, SelectedOption } from "./storefront-api-types"; +import type {Maybe, Product, ProductOption, ProductOptionValue, ProductVariant, SelectedOption } from "./storefront-api-types"; +import { PartialDeep } from "type-fest"; type RecursivePartial = { [P in keyof T]?: RecursivePartial; @@ -70,6 +72,15 @@ function mapSelectedProductOptionToObject(options: Pick[]) { + return JSON.stringify(mapSelectedProductOptionToObject(options)); +} + /** * Encode the selected product option as a key for mapping to the encoded variants * For example, a selected product option of @@ -138,7 +149,7 @@ function encodeSelectedProductOptionAsKey(selectedOption: Pick { const variantKey = encodeSelectedProductOptionAsKey(variant.selectedOptions || [], productOptionMappings); return {[variantKey]: variant};; @@ -150,25 +161,28 @@ export type MappedProductOptions = Omit & { } const PRODUCT_INPUTS = [ - 'handle', - 'encodedVariantExistence', - 'encodedVariantAvailability', 'options', 'selectedOrFirstAvailableVariant', 'adjacentVariants', ]; +const PRODUCT_INPUTS_EXTRA = [ + 'handle', + 'encodedVariantExistence', + 'encodedVariantAvailability', +]; + function logError(key: string) { console.error(`[h2:error:getProductOptions] product.${key} is missing. Make sure you query for this field from the Storefront API.`) return false; } -export function checkProductParam(product: RecursivePartial, ): Product { +export function checkProductParam(product: RecursivePartial, checkAll = false): Product { let validParam = true; const productKeys = Object.keys(product); // Check product input - PRODUCT_INPUTS.forEach((key) => { + (checkAll ? [...PRODUCT_INPUTS, ...PRODUCT_INPUTS_EXTRA] : PRODUCT_INPUTS).forEach((key) => { if (!productKeys.includes(key)) { validParam = logError(key); } @@ -181,14 +195,14 @@ export function checkProductParam(product: RecursivePartial, ): Product const firstOptionValues = product.options[0].optionValues[0]; // Check for options.optionValues.name - if (!firstOptionValues?.name) { + if (checkAll && !firstOptionValues?.name) { validParam = logError('options.optionValues.name'); } // Check for options.optionValues.firstSelectableVariant if (firstOptionValues?.firstSelectableVariant) { // check product variant - validParam = checkProductVariantParam(firstOptionValues.firstSelectableVariant, 'options.optionValues.firstSelectableVariant', validParam) + validParam = checkProductVariantParam(firstOptionValues.firstSelectableVariant, 'options.optionValues.firstSelectableVariant', validParam, checkAll) } else { validParam = logError('options.optionValues.firstSelectableVariant'); } @@ -199,21 +213,21 @@ export function checkProductParam(product: RecursivePartial, ): Product // Check for nested selectedOrFirstAvailableVariant requirements if(product.selectedOrFirstAvailableVariant) { - validParam = checkProductVariantParam(product.selectedOrFirstAvailableVariant, 'selectedOrFirstAvailableVariant', validParam) + validParam = checkProductVariantParam(product.selectedOrFirstAvailableVariant, 'selectedOrFirstAvailableVariant', validParam, checkAll) } // Check for nested adjacentVariants requirements if(!!product.adjacentVariants && product.adjacentVariants[0]) { - validParam = checkProductVariantParam(product.adjacentVariants[0], 'adjacentVariants', validParam) + validParam = checkProductVariantParam(product.adjacentVariants[0], 'adjacentVariants', validParam, checkAll) } return (validParam ? product : {}) as Product; } -function checkProductVariantParam(variant: RecursivePartial, key: string, currentValidParamState: boolean): boolean { +function checkProductVariantParam(variant: RecursivePartial, key: string, currentValidParamState: boolean, checkAll: boolean): boolean { let validParam = currentValidParamState; - if (!variant.product?.handle) { + if (checkAll && !variant.product?.handle) { validParam = logError(`${key}.product.handle`); } if (variant.selectedOptions) { @@ -231,12 +245,48 @@ function checkProductVariantParam(variant: RecursivePartial, key return validParam; } -export function getProductOptions(product: RecursivePartial): MappedProductOptions[] { +/** + * Finds all the variants provided by adjacentVariants, options.optionValues.firstAvailableVariant, + * and selectedOrFirstAvailableVariant and return them in a single array + * @param product + * @returns + */ +export function getAdjacentAndFirstAvailableVariants(product: RecursivePartial) { // Checks for valid product input const checkedProduct = checkProductParam(product); if (!checkedProduct.options) return []; + const availableVariants: Record = {}; + checkedProduct.options.map((option) => { + option.optionValues?.map((value) => { + if(!!value.firstSelectableVariant) { + const variantKey = mapSelectedProductOptionToObjectAsString(value.firstSelectableVariant.selectedOptions); + availableVariants[variantKey] = value.firstSelectableVariant; + } + }) + }); + + checkedProduct.adjacentVariants.map((variant) => { + const variantKey = mapSelectedProductOptionToObjectAsString(variant.selectedOptions); + availableVariants[variantKey] = variant; + }); + + const selectedVariant = checkedProduct.selectedOrFirstAvailableVariant; + if (!!selectedVariant) { + const variantKey = mapSelectedProductOptionToObjectAsString(selectedVariant.selectedOptions); + availableVariants[variantKey] = selectedVariant; + } + + return Object.values(availableVariants); +} + +export function getProductOptions(product: RecursivePartial): MappedProductOptions[] { + // Checks for valid product input + const checkedProduct = checkProductParam(product, true); + + if (!checkedProduct.options) return []; + const { options, selectedOrFirstAvailableVariant: selectedVariant, @@ -249,7 +299,7 @@ export function getProductOptions(product: RecursivePartial): MappedPro const productOptionMappings = mapProductOptions(options); // Get the adjacent variants mapped to the encoded selected option values - const variants = mapAdjacentVariants(selectedVariant ? [ + const variants = mapVariants(selectedVariant ? [ selectedVariant, ...adjacentVariants, ] : adjacentVariants, productOptionMappings); @@ -257,7 +307,7 @@ export function getProductOptions(product: RecursivePartial): MappedPro // Get the key:value version of selected options for building url query params const selectedOptions = mapSelectedProductOptionToObject(selectedVariant ? selectedVariant.selectedOptions : []); - return options.map((option, optionIndex) => { + const productOptions = options.map((option, optionIndex) => { return { ...option, optionValues: option.optionValues.map((value) => { @@ -272,7 +322,7 @@ export function getProductOptions(product: RecursivePartial): MappedPro // Top-down option check for existence and availability const topDownKey = JSON.parse(targetKey).slice(0, optionIndex + 1); const exists = isOptionValueCombinationInEncodedVariant(topDownKey, encodedVariantExistence || ''); - const available = isOptionValueCombinationInEncodedVariant(topDownKey, encodedVariantAvailability || ''); + let available = isOptionValueCombinationInEncodedVariant(topDownKey, encodedVariantAvailability || ''); // Get the variant for the current option value if exists, else use the first selectable variant const variant: ProductVariant = variants[targetKey] || value.firstSelectableVariant; @@ -295,4 +345,34 @@ export function getProductOptions(product: RecursivePartial): MappedPro }), }; }); + + // Workaround for bug in encodedVariantAvailability + // Remove once https://github.com/Shopify/combined-listings-app/issues/2377 is resolved + const optionsAvailable: boolean[] = []; + productOptions.reverse().map((option, optionIndex) => { + let optionAvailable = false; + if (optionIndex === 0) { + // Make sure leaf option values always reflect the variant's availableForSale attribute + option.optionValues.map((value, valueIndex) => { + if (!value.isDifferentProduct) { + const available = value.variant.availableForSale + productOptions[optionIndex].optionValues[valueIndex].available = available; + if (available) optionAvailable = true; + } + }) + } else { + // If not the last option, for selected optionValue, check for previous option for availability + const selectedValue = option.optionValues.filter((value) => value.selected); + const previousOptionAvailable = optionsAvailable[optionIndex - 1]; + if (selectedValue && selectedValue.length === 1 && !selectedValue[0].isDifferentProduct) { + selectedValue[0].available = previousOptionAvailable; + if (previousOptionAvailable) optionAvailable = true; + } + } + + // Keep track of availability of the current option + optionsAvailable.push(optionAvailable); + }); + + return productOptions.reverse(); } diff --git a/packages/hydrogen-react/src/index.ts b/packages/hydrogen-react/src/index.ts index d7255ca0d..c3ac75d15 100644 --- a/packages/hydrogen-react/src/index.ts +++ b/packages/hydrogen-react/src/index.ts @@ -42,7 +42,7 @@ export { customerAccountApiCustomScalars, storefrontApiCustomScalars, } from './codegen.helpers.js'; -export {getProductOptions, type MappedProductOptions} from'./getProductOptions.js'; +export {getProductOptions, getAdjacentAndFirstAvailableVariants, type MappedProductOptions} from'./getProductOptions.js'; export {getShopifyCookies} from './cookies-utils.js'; export {ExternalVideo} from './ExternalVideo.js'; export {flattenConnection} from './flatten-connection.js'; diff --git a/packages/hydrogen/src/index.ts b/packages/hydrogen/src/index.ts index 7ca202413..cf9cd2434 100644 --- a/packages/hydrogen/src/index.ts +++ b/packages/hydrogen/src/index.ts @@ -143,6 +143,7 @@ export { isOptionValueCombinationInEncodedVariant, decodeEncodedVariant, getProductOptions, + getAdjacentAndFirstAvailableVariants, } from '@shopify/hydrogen-react'; export {RichText} from './RichText'; diff --git a/packages/hydrogen/src/product/useOptimisticVariant.ts b/packages/hydrogen/src/product/useOptimisticVariant.ts index b5e70f9d0..438671eb6 100644 --- a/packages/hydrogen/src/product/useOptimisticVariant.ts +++ b/packages/hydrogen/src/product/useOptimisticVariant.ts @@ -33,8 +33,7 @@ export function useOptimisticVariant< >( variants instanceof Array ? variants - : (variants as PartialDeep).product - ?.variants?.nodes || [] + : (variants as PartialDeep).product?.variants?.nodes || [] ); // useEffect(() => { @@ -84,6 +83,8 @@ export function useOptimisticVariant< }); }); + console.log({matchingVariant, queryParams: queryParams.toString()}) + if (matchingVariant) { return { ...matchingVariant, diff --git a/templates/skeleton/app/components/ProductForm.tsx b/templates/skeleton/app/components/ProductForm.tsx index c5531c5a9..bf3d4646e 100644 --- a/templates/skeleton/app/components/ProductForm.tsx +++ b/templates/skeleton/app/components/ProductForm.tsx @@ -13,7 +13,7 @@ export function ProductForm({ variants, }: { product: ProductFragment; - selectedVariant: ProductFragment['selectedVariant']; + selectedVariant: ProductFragment['selectedOrFirstAvailableVariant']; variants: Array; }) { const {open} = useAside(); diff --git a/templates/skeleton/app/components/ProductFormV2.tsx b/templates/skeleton/app/components/ProductFormV2.tsx index 691ab2d26..e46114fbb 100644 --- a/templates/skeleton/app/components/ProductFormV2.tsx +++ b/templates/skeleton/app/components/ProductFormV2.tsx @@ -18,7 +18,6 @@ export function ProductFormV2({productOptions}: { handle, variantUriQuery, selected, - exists, available, isDifferentProduct, swatch, @@ -45,9 +44,8 @@ export function ProductFormV2({productOptions}: { return (