{"version":3,"file":"ProductProvider.mjs","sources":["../../src/ProductProvider.tsx"],"sourcesContent":["import {\n  useMemo,\n  useState,\n  useEffect,\n  useCallback,\n  createContext,\n  useContext,\n} from 'react';\nimport type {\n  SelectedOption as SelectedOptionType,\n  SellingPlan,\n  SellingPlanAllocation,\n  Product,\n  ProductVariant as ProductVariantType,\n  ProductVariantConnection,\n  SellingPlan as SellingPlanType,\n  SellingPlanAllocation as SellingPlanAllocationType,\n  SellingPlanGroup as SellingPlanGroupType,\n  SellingPlanGroupConnection,\n} from './storefront-api-types.js';\nimport type {PartialDeep} from 'type-fest';\nimport {flattenConnection} from './flatten-connection.js';\n\nconst ProductOptionsContext = createContext<ProductHookValue | null>(null);\n\ntype InitialVariantId = ProductVariantType['id'] | null;\n\ninterface ProductProviderProps {\n  /** A Storefront API [Product object](https://shopify.dev/api/storefront/reference/products/product). */\n  data: PartialDeep<Product, {recurseIntoArrays: true}>;\n  /** A `ReactNode` element. */\n  children: React.ReactNode;\n  /**\n   * The initially selected variant.\n   * The following logic applies to `initialVariantId`:\n   * 1. If `initialVariantId` is provided, then it's used even if it's out of stock.\n   * 2. If `initialVariantId` is provided but is `null`, then no variant is used.\n   * 3. If nothing is passed to `initialVariantId` then the first available / in-stock variant is used.\n   * 4. If nothing is passed to `initialVariantId` and no variants are in stock, then the first variant is used.\n   */\n  initialVariantId?: InitialVariantId;\n}\n\n/**\n * `<ProductProvider />` is a context provider that enables use of the `useProduct()` hook.\n *\n * It helps manage selected options and variants for a product.\n */\nexport function ProductProvider({\n  children,\n  data: product,\n  initialVariantId: explicitVariantId,\n}: ProductProviderProps): JSX.Element {\n  // The flattened variants\n  const variants = useMemo(\n    () => flattenConnection(product.variants ?? {}),\n    [product.variants],\n  );\n\n  if (!isProductVariantArray(variants)) {\n    throw new Error(\n      `<ProductProvider/> requires 'product.variants.nodes' or 'product.variants.edges'`,\n    );\n  }\n\n  // All the options available for a product, based on all the variants\n  const options = useMemo(() => getOptions(variants), [variants]);\n\n  /**\n   * Track the selectedVariant within the provider.\n   */\n  const [selectedVariant, setSelectedVariant] = useState<\n    | PartialDeep<ProductVariantType, {recurseIntoArrays: true}>\n    | undefined\n    | null\n  >(() => getVariantBasedOnIdProp(explicitVariantId, variants));\n\n  /**\n   * Track the selectedOptions within the provider. If a `initialVariantId`\n   * is passed, use that to select initial options.\n   */\n  const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>(() =>\n    getSelectedOptions(selectedVariant),\n  );\n\n  /**\n   * When the initialVariantId changes, we need to make sure we\n   * update the selected variant and selected options. If not,\n   * then the selected variant and options will reference incorrect\n   * values.\n   */\n  useEffect(() => {\n    const newSelectedVariant = getVariantBasedOnIdProp(\n      explicitVariantId,\n      variants,\n    );\n    setSelectedVariant(newSelectedVariant);\n    setSelectedOptions(getSelectedOptions(newSelectedVariant));\n  }, [explicitVariantId, variants]);\n\n  /**\n   * Allow the developer to select an option.\n   */\n  const setSelectedOption = useCallback(\n    (name: string, value: string) => {\n      setSelectedOptions((selectedOptions) => {\n        const opts = {...selectedOptions, [name]: value};\n        setSelectedVariant(getSelectedVariant(variants, opts));\n        return opts;\n      });\n    },\n    [setSelectedOptions, variants],\n  );\n\n  const isOptionInStock = useCallback(\n    (option: string, value: string) => {\n      const proposedVariant = getSelectedVariant(variants, {\n        ...selectedOptions,\n        ...{[option]: value},\n      });\n\n      return proposedVariant?.availableForSale ?? true;\n    },\n    [selectedOptions, variants],\n  );\n\n  const sellingPlanGroups = useMemo(\n    () =>\n      flattenConnection(product.sellingPlanGroups ?? {}).map(\n        (sellingPlanGroup) => ({\n          ...sellingPlanGroup,\n          sellingPlans: flattenConnection(sellingPlanGroup?.sellingPlans ?? {}),\n        }),\n      ),\n    [product.sellingPlanGroups],\n  );\n\n  /**\n   * Track the selectedSellingPlan within the hook. If `initialSellingPlanId`\n   * is passed, use that as an initial value. Look it up from the `selectedVariant`, since\n   * that is also a requirement.\n   */\n  const [selectedSellingPlan, setSelectedSellingPlan] = useState<\n    PartialDeep<SellingPlan, {recurseIntoArrays: true}> | undefined\n  >(undefined);\n\n  const selectedSellingPlanAllocation = useMemo<\n    PartialDeep<SellingPlanAllocation, {recurseIntoArrays: true}> | undefined\n  >(() => {\n    if (!selectedVariant || !selectedSellingPlan) {\n      return;\n    }\n\n    if (\n      !selectedVariant.sellingPlanAllocations?.nodes &&\n      !selectedVariant.sellingPlanAllocations?.edges\n    ) {\n      throw new Error(\n        `<ProductProvider/>: You must include 'sellingPlanAllocations.nodes' or 'sellingPlanAllocations.edges' in your variants in order to calculate selectedSellingPlanAllocation`,\n      );\n    }\n\n    return flattenConnection(selectedVariant.sellingPlanAllocations).find(\n      (allocation) => allocation?.sellingPlan?.id === selectedSellingPlan.id,\n    );\n  }, [selectedVariant, selectedSellingPlan]);\n\n  const value = useMemo<ProductHookValue>(\n    () => ({\n      product,\n      variants,\n      variantsConnection: product.variants,\n      options,\n      selectedVariant,\n      setSelectedVariant,\n      selectedOptions,\n      setSelectedOption,\n      setSelectedOptions,\n      isOptionInStock,\n      selectedSellingPlan,\n      setSelectedSellingPlan,\n      selectedSellingPlanAllocation,\n      sellingPlanGroups,\n      sellingPlanGroupsConnection: product.sellingPlanGroups,\n    }),\n    [\n      product,\n      isOptionInStock,\n      options,\n      selectedOptions,\n      selectedSellingPlan,\n      selectedSellingPlanAllocation,\n      selectedVariant,\n      sellingPlanGroups,\n      setSelectedOption,\n      variants,\n    ],\n  );\n\n  return (\n    <ProductOptionsContext.Provider value={value}>\n      {children}\n    </ProductOptionsContext.Provider>\n  );\n}\n\n/**\n * Provides access to the context value provided by `<ProductProvider />`. Must be a descendent of `<ProductProvider />`.\n */\nexport function useProduct(): ProductHookValue {\n  const context = useContext(ProductOptionsContext);\n\n  if (!context) {\n    throw new Error(`'useProduct' must be a child of <ProductProvider />`);\n  }\n\n  return context;\n}\n\nfunction getSelectedVariant(\n  variants: PartialDeep<ProductVariantType, {recurseIntoArrays: true}>[],\n  choices: SelectedOptions,\n): PartialDeep<ProductVariantType, {recurseIntoArrays: true}> | undefined {\n  /**\n   * Ensure the user has selected all the required options, not just some.\n   */\n  if (\n    !variants.length ||\n    variants?.[0]?.selectedOptions?.length !== Object.keys(choices).length\n  ) {\n    return;\n  }\n\n  return variants?.find((variant) => {\n    return Object.entries(choices).every(([name, value]) => {\n      return variant?.selectedOptions?.some(\n        (option) => option?.name === name && option?.value === value,\n      );\n    });\n  });\n}\n\nfunction getOptions(\n  variants: PartialDeep<ProductVariantType, {recurseIntoArrays: true}>[],\n): OptionWithValues[] {\n  const map = variants.reduce(\n    (memo, variant) => {\n      if (!variant.selectedOptions) {\n        throw new Error(`'getOptions' requires 'variant.selectedOptions'`);\n      }\n      variant?.selectedOptions?.forEach((opt) => {\n        memo[opt?.name ?? ''] = memo[opt?.name ?? ''] || new Set();\n        memo[opt?.name ?? ''].add(opt?.value ?? '');\n      });\n\n      return memo;\n    },\n    {} as Record<string, Set<string>>,\n  );\n\n  return Object.keys(map).map((option) => {\n    return {\n      name: option,\n      values: Array.from(map[option]),\n    };\n  });\n}\n\nfunction getVariantBasedOnIdProp(\n  explicitVariantId: InitialVariantId | undefined,\n  variants: Array<\n    PartialDeep<ProductVariantType, {recurseIntoArrays: true}> | undefined\n  >,\n):\n  | PartialDeep<ProductVariantType, {recurseIntoArrays: true}>\n  | undefined\n  | null {\n  // get the initial variant based on the logic outlined in the comments for 'initialVariantId' above\n  // * 1. If `initialVariantId` is provided, then it's used even if it's out of stock.\n  if (explicitVariantId) {\n    const foundVariant = variants.find(\n      (variant) => variant?.id === explicitVariantId,\n    );\n    if (!foundVariant) {\n      console.warn(\n        `<ProductProvider/> received a 'initialVariantId' prop, but could not actually find a variant with that ID`,\n      );\n    }\n    return foundVariant;\n  }\n  // * 2. If `initialVariantId` is provided but is `null`, then no variant is used.\n  if (explicitVariantId === null) {\n    return null;\n  }\n  // * 3. If nothing is passed to `initialVariantId` then the first available / in-stock variant is used.\n  // * 4. If nothing is passed to `initialVariantId` and no variants are in stock, then the first variant is used.\n  if (explicitVariantId === undefined) {\n    return variants.find((variant) => variant?.availableForSale) || variants[0];\n  }\n}\n\nfunction getSelectedOptions(\n  selectedVariant:\n    | PartialDeep<ProductVariantType, {recurseIntoArrays: true}>\n    | undefined\n    | null,\n): SelectedOptions {\n  return selectedVariant?.selectedOptions\n    ? selectedVariant.selectedOptions.reduce<SelectedOptions>(\n        (memo, optionSet) => {\n          memo[optionSet?.name ?? ''] = optionSet?.value ?? '';\n          return memo;\n        },\n        {},\n      )\n    : {};\n}\n\nfunction isProductVariantArray(\n  maybeVariantArray:\n    | (PartialDeep<ProductVariantType, {recurseIntoArrays: true}> | undefined)[]\n    | undefined,\n): maybeVariantArray is PartialDeep<\n  ProductVariantType,\n  {recurseIntoArrays: true}\n>[] {\n  if (!maybeVariantArray || !Array.isArray(maybeVariantArray)) {\n    return false;\n  }\n\n  return true;\n}\n\nexport interface OptionWithValues {\n  name: SelectedOptionType['name'];\n  values: SelectedOptionType['value'][];\n}\n\ntype UseProductObjects = {\n  /** The raw product from the Storefront API */\n  product: Product;\n  /** An array of the variant `nodes` from the `VariantConnection`. */\n  variants: ProductVariantType[];\n  variantsConnection?: ProductVariantConnection;\n  /** An array of the product's options and values. */\n  options: OptionWithValues[];\n  /** The selected variant. */\n  selectedVariant?: ProductVariantType | null;\n  selectedOptions: SelectedOptions;\n  /** The selected selling plan. */\n  selectedSellingPlan?: SellingPlanType;\n  /** The selected selling plan allocation. */\n  selectedSellingPlanAllocation?: SellingPlanAllocationType;\n  /** The selling plan groups. */\n  sellingPlanGroups?: (Omit<SellingPlanGroupType, 'sellingPlans'> & {\n    sellingPlans: SellingPlanType[];\n  })[];\n  sellingPlanGroupsConnection?: SellingPlanGroupConnection;\n};\n\ntype UseProductFunctions = {\n  /** A callback to set the selected variant to the variant passed as an argument. */\n  setSelectedVariant: (\n    variant: PartialDeep<ProductVariantType, {recurseIntoArrays: true}> | null,\n  ) => void;\n  /** A callback to set the selected option. */\n  setSelectedOption: (\n    name: SelectedOptionType['name'],\n    value: SelectedOptionType['value'],\n  ) => void;\n  /** A callback to set multiple selected options at once. */\n  setSelectedOptions: (options: SelectedOptions) => void;\n  /** A callback to set the selected selling plan to the one passed as an argument. */\n  setSelectedSellingPlan: (\n    sellingPlan: PartialDeep<SellingPlanType, {recurseIntoArrays: true}>,\n  ) => void;\n  /** A callback that returns a boolean indicating if the option is in stock. */\n  isOptionInStock: (\n    name: SelectedOptionType['name'],\n    value: SelectedOptionType['value'],\n  ) => boolean;\n};\n\ntype ProductHookValue = PartialDeep<\n  UseProductObjects,\n  {recurseIntoArrays: true}\n> &\n  UseProductFunctions;\n\nexport type SelectedOptions = {\n  [key: string]: string;\n};\n"],"names":["value","selectedOptions","_a"],"mappings":"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"}