{"version":3,"file":"ProductProvider.mjs","names":[],"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 * @publicDocs\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 * @publicDocs\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"],"mappings":";;;;AAuBA,IAAM,wBAAwB,cAAuC,KAAK;;;;;;;AA0B1E,SAAgB,gBAAgB,EAC9B,UACA,MAAM,SACN,kBAAkB,qBACkB;CAEpC,MAAM,WAAW,cACT,kBAAkB,QAAQ,YAAY,EAAE,CAAC,EAC/C,CAAC,QAAQ,SAAS,CACnB;AAED,KAAI,CAAC,sBAAsB,SAAS,CAClC,OAAM,IAAI,MACR,mFACD;CAIH,MAAM,UAAU,cAAc,WAAW,SAAS,EAAE,CAAC,SAAS,CAAC;;;;CAK/D,MAAM,CAAC,iBAAiB,sBAAsB,eAItC,wBAAwB,mBAAmB,SAAS,CAAC;;;;;CAM7D,MAAM,CAAC,iBAAiB,sBAAsB,eAC5C,mBAAmB,gBAAgB,CACpC;;;;;;;AAQD,iBAAgB;EACd,MAAM,qBAAqB,wBACzB,mBACA,SACD;AACD,qBAAmB,mBAAmB;AACtC,qBAAmB,mBAAmB,mBAAmB,CAAC;IACzD,CAAC,mBAAmB,SAAS,CAAC;;;;CAKjC,MAAM,oBAAoB,aACvB,MAAc,UAAkB;AAC/B,sBAAoB,oBAAoB;GACtC,MAAM,OAAO;IAAC,GAAG;KAAkB,OAAO;IAAM;AAChD,sBAAmB,mBAAmB,UAAU,KAAK,CAAC;AACtD,UAAO;IACP;IAEJ,CAAC,oBAAoB,SAAS,CAC/B;CAED,MAAM,kBAAkB,aACrB,QAAgB,UAAkB;AAMjC,SALwB,mBAAmB,UAAU;GACnD,GAAG;IACE,SAAS;GACf,CAAC,EAEsB,oBAAoB;IAE9C,CAAC,iBAAiB,SAAS,CAC5B;CAED,MAAM,oBAAoB,cAEtB,kBAAkB,QAAQ,qBAAqB,EAAE,CAAC,CAAC,KAChD,sBAAsB;EACrB,GAAG;EACH,cAAc,kBAAkB,kBAAkB,gBAAgB,EAAE,CAAC;EACtE,EACF,EACH,CAAC,QAAQ,kBAAkB,CAC5B;;;;;;CAOD,MAAM,CAAC,qBAAqB,0BAA0B,SAEpD,KAAA,EAAU;CAEZ,MAAM,gCAAgC,cAE9B;AACN,MAAI,CAAC,mBAAmB,CAAC,oBACvB;AAGF,MACE,CAAC,gBAAgB,wBAAwB,SACzC,CAAC,gBAAgB,wBAAwB,MAEzC,OAAM,IAAI,MACR,6KACD;AAGH,SAAO,kBAAkB,gBAAgB,uBAAuB,CAAC,MAC9D,eAAe,YAAY,aAAa,OAAO,oBAAoB,GACrE;IACA,CAAC,iBAAiB,oBAAoB,CAAC;CAE1C,MAAM,QAAQ,eACL;EACL;EACA;EACA,oBAAoB,QAAQ;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,6BAA6B,QAAQ;EACtC,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,oBAAC,sBAAsB,UAAvB;EAAuC;EACpC;EAC8B,CAAA;;;;;;AAQrC,SAAgB,aAA+B;CAC7C,MAAM,UAAU,WAAW,sBAAsB;AAEjD,KAAI,CAAC,QACH,OAAM,IAAI,MAAM,sDAAsD;AAGxE,QAAO;;AAGT,SAAS,mBACP,UACA,SACwE;;;;AAIxE,KACE,CAAC,SAAS,UACV,WAAW,IAAI,iBAAiB,WAAW,OAAO,KAAK,QAAQ,CAAC,OAEhE;AAGF,QAAO,UAAU,MAAM,YAAY;AACjC,SAAO,OAAO,QAAQ,QAAQ,CAAC,OAAO,CAAC,MAAM,WAAW;AACtD,UAAO,SAAS,iBAAiB,MAC9B,WAAW,QAAQ,SAAS,QAAQ,QAAQ,UAAU,MACxD;IACD;GACF;;AAGJ,SAAS,WACP,UACoB;CACpB,MAAM,MAAM,SAAS,QAClB,MAAM,YAAY;AACjB,MAAI,CAAC,QAAQ,gBACX,OAAM,IAAI,MAAM,kDAAkD;AAEpE,WAAS,iBAAiB,SAAS,QAAQ;AACzC,QAAK,KAAK,QAAQ,MAAM,KAAK,KAAK,QAAQ,uBAAO,IAAI,KAAK;AAC1D,QAAK,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS,GAAG;IAC3C;AAEF,SAAO;IAET,EAAE,CACH;AAED,QAAO,OAAO,KAAK,IAAI,CAAC,KAAK,WAAW;AACtC,SAAO;GACL,MAAM;GACN,QAAQ,MAAM,KAAK,IAAI,QAAQ;GAChC;GACD;;AAGJ,SAAS,wBACP,mBACA,UAMO;AAGP,KAAI,mBAAmB;EACrB,MAAM,eAAe,SAAS,MAC3B,YAAY,SAAS,OAAO,kBAC9B;AACD,MAAI,CAAC,aACH,SAAQ,KACN,4GACD;AAEH,SAAO;;AAGT,KAAI,sBAAsB,KACxB,QAAO;AAIT,KAAI,sBAAsB,KAAA,EACxB,QAAO,SAAS,MAAM,YAAY,SAAS,iBAAiB,IAAI,SAAS;;AAI7E,SAAS,mBACP,iBAIiB;AACjB,QAAO,iBAAiB,kBACpB,gBAAgB,gBAAgB,QAC7B,MAAM,cAAc;AACnB,OAAK,WAAW,QAAQ,MAAM,WAAW,SAAS;AAClD,SAAO;IAET,EAAE,CACH,GACD,EAAE;;AAGR,SAAS,sBACP,mBAME;AACF,KAAI,CAAC,qBAAqB,CAAC,MAAM,QAAQ,kBAAkB,CACzD,QAAO;AAGT,QAAO"}