{"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 [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) {\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      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      isOptionInStock,\n      options,\n      product.sellingPlanGroups,\n      product.variants,\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() {\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((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  }, {} as Record<string, Set<string>>);\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  // 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 ProductHookValue = PartialDeep<\n  {\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  {recurseIntoArrays: true}\n> & {\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\nexport type SelectedOptions = {\n  [key: string]: string;\n};\n"],"names":["ProductOptionsContext","createContext","ProductProvider","children","data","product","initialVariantId","explicitVariantId","variants","useMemo","flattenConnection","isProductVariantArray","Error","options","getOptions","selectedVariant","setSelectedVariant","useState","getVariantBasedOnIdProp","selectedOptions","setSelectedOptions","getSelectedOptions","useEffect","newSelectedVariant","setSelectedOption","useCallback","name","value","opts","getSelectedVariant","isOptionInStock","option","proposedVariant","availableForSale","sellingPlanGroups","map","sellingPlanGroup","sellingPlans","selectedSellingPlan","setSelectedSellingPlan","undefined","selectedSellingPlanAllocation","sellingPlanAllocations","nodes","edges","find","allocation","sellingPlan","id","variantsConnection","sellingPlanGroupsConnection","_jsx","useProduct","context","useContext","choices","length","Object","keys","variant","entries","every","some","reduce","memo","forEach","opt","Set","add","values","Array","from","foundVariant","console","warn","optionSet","maybeVariantArray","isArray"],"mappings":"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"}