[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/__tests__/ProductAssemblyOptions.test.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { fireEvent, render } from '@vtex/test-tools/react'
import useProduct, { ProductContext } from 'vtex.product-context/useProduct'

import ProductAssemblyOptions from '../ProductAssemblyOptions'
import InputValue from '../ProductAssemblyOptionInputValues'
import productSubscription from '../__fixtures__/productSubscription.json'

const mockUseProduct = useProduct as jest.Mock<ProductContext>

mockUseProduct.mockImplementation(() => ({
  product: productSubscription.data.product,
  selectedItem: productSubscription.data.product.items[0],
  selectedQuantity: 1,
}))

test('localizes subscription labels', async () => {
  const { getByText } = render(
    <ProductAssemblyOptions>
      <InputValue optionsDisplay="box" />
    </ProductAssemblyOptions>
  )

  const btn = getByText(/add subscription/i)

  fireEvent.click(btn)

  expect(getByText('Every 7 days')).toBeInTheDocument()
  expect(getByText('Every day')).toBeInTheDocument()
  expect(getByText('Every 2 months')).toBeInTheDocument()
  expect(getByText('Every year')).toBeInTheDocument()
  expect(getByText('Every 2 years')).toBeInTheDocument()

  expect(getByText(/Monday/i)).toBeInTheDocument()
  expect(getByText(/Tuesday/i)).toBeInTheDocument()
  expect(getByText(/Saturday/i)).toBeInTheDocument()
})

test('replaces weekdays for numeric days', async () => {
  const { getByText, queryByText } = render(
    <ProductAssemblyOptions>
      <InputValue optionsDisplay="box" />
    </ProductAssemblyOptions>
  )

  const btn = getByText(/add subscription/i)

  fireEvent.click(btn)
  fireEvent.click(getByText('Every 2 months'))

  expect(queryByText(/Sunday/i)).not.toBeInTheDocument()
  expect(queryByText(/Tuesday/i)).not.toBeInTheDocument()
  expect(queryByText(/Saturday/i)).not.toBeInTheDocument()

  expect(queryByText(/1st/)).toBeInTheDocument()
  expect(queryByText(/2nd/)).toBeInTheDocument()
  expect(queryByText(/6th/)).toBeInTheDocument()
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/modules/useAssemblyOptions.test.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { renderHook } from '@vtex/test-tools/react'
import useProduct, { ProductContext } from 'vtex.product-context/useProduct'

import parseAssemblyOptions from './useAssemblyOptions'
import productAttachment from '../__fixtures__/productAttachment.json'

const mockUseProduct = useProduct as jest.Mock<ProductContext>

test('it should return inputValues', () => {
  mockUseProduct.mockImplementation(() => ({
    product: productAttachment.data.product,
    selectedItem: productAttachment.data.product.items[0],
    selectedQuantity: 1,
  }))

  const { result } = renderHook(() => parseAssemblyOptions())
  const value = result.current!

  expect(value.Customization).toBeDefined()
  expect(value.Customization.inputValues).toBe(
    productAttachment.data.product.itemMetadata.items[0].assemblyOptions[0]
      .inputValues
  )
  expect(value.Customization.required).toBe(false)
})

test('it should return inputValues of the selected SKU', () => {
  mockUseProduct.mockImplementation(() => ({
    product: productAttachment.data.product,
    selectedItem: productAttachment.data.product.items[6],
    selectedQuantity: 1,
  }))

  const { result } = renderHook(() => parseAssemblyOptions())
  const value = result.current!

  expect(value.Customization).toBeDefined()
  expect(value.Customization.inputValues).toBe(
    productAttachment.data.product.itemMetadata.items[2].assemblyOptions[0]
      .inputValues
  )
  expect(value.Customization.required).toBe(true)
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/modules/useAssemblyOptionsModifications.ts from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { useEffect } from 'react'
import { pick } from 'ramda'
import { useProductDispatch } from 'vtex.product-context/ProductDispatchContext'

export default function useAssemblyOptionsModifications(
  localState: AssemblyOptionGroupState
) {
  const dispatch = useProductDispatch()

  useEffect(() => {
    const {
      items: localItems,
      id,
      type,
      valuesOfInputValues,
      optin,
    } = localState

    const items = optin
      ? Object.values(localItems ?? {}).map(parseItem(type))
      : []

    const isValid = !optin ? true : isGroupValid(localState)
    const groupInputValues = optin ? valuesOfInputValues : {}

    dispatch({
      type: 'SET_ASSEMBLY_OPTIONS',
      args: {
        groupId: id,
        groupItems: items,
        groupInputValues,
        isValid,
      },
    })
  }, [localState, dispatch])
}

function isGroupValid(group: AssemblyOptionGroupState) {
  const items = Object.values(group.items ?? {})
  const itemsToBeAdded = items.reduce((acc, { quantity }) => acc + quantity, 0)
  const isValid =
    (group.maxQuantity !== undefined &&
      group.maxQuantity >= itemsToBeAdded &&
      group.minQuantity <= itemsToBeAdded) ||
    group.maxQuantity === undefined

  if (!isValid) {
    // No need to check children if group is already invalid
    return false
  }

  const areItemsValid = items.every((item) => {
    if (!item.children) {
      return true
    }

    const childrenGroups = Object.values(item.children)
    const areChildrenValid = childrenGroups.every((childGroup) =>
      isGroupValid(childGroup)
    )

    return areChildrenValid
  })

  return areItemsValid
}

function parseItem(groupType: GroupTypes) {
  return (item: AssemblyItem) => ({
    ...pick(['name', 'id', 'initialQuantity', 'quantity', 'seller'], item),
    price: item.price / 100,
    choiceType: groupType,
    children: item.children ? parseItemChildren(item.children) : null,
  })
}

interface BuyButtonItem {
  name: string
  id: string
  initialQuantity: number
  quantity: number
  seller: string
  price: number
  choiceType: GroupTypes
  children: Record<string, BuyButtonItem[]> | null
}

function parseItemChildren(children: Record<string, AssemblyOptionGroupState>) {
  const groupIds = Object.keys(children)
  const result: Record<string, BuyButtonItem[]> = {}

  for (const groupId of groupIds) {
    const childrenAssemblyOption = children[groupId]

    result[groupId] = Object.values(childrenAssemblyOption.items ?? {}).map(
      parseItem(childrenAssemblyOption.type)
    )
  }

  return result
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/modules/assemblyGroupType.test.ts from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { getGroupType, GROUP_TYPES } from './assemblyGroupType'

test('getGroupType should return type single', () => {
  const fakeAssemblyOptionGroup = {
    composition: {
      minQuantity: 1,
      maxQuantity: 1,
    },
  } as AssemblyOption

  expect(getGroupType(fakeAssemblyOptionGroup)).toBe(GROUP_TYPES.SINGLE)
})

test('getGroupType should return type toggle', () => {
  const fakeAssemblyOptionGroup = {
    composition: {
      minQuantity: 0,
      maxQuantity: 1,
      items: [
        {
          minQuantity: 0,
          maxQuantity: 1,
        },
        {
          minQuantity: 0,
          maxQuantity: 1,
        },
      ],
    },
  } as AssemblyOption

  expect(getGroupType(fakeAssemblyOptionGroup)).toBe(GROUP_TYPES.TOGGLE)
})

test('getGroupType should return type multiple', () => {
  const fakeAssemblyOptionGroup = {
    composition: {
      minQuantity: 0,
      maxQuantity: 1,
      items: [
        {
          minQuantity: 0,
          maxQuantity: 1,
        },
        {
          minQuantity: 0,
          maxQuantity: 2, // this value will make the getGroupType return type multiple
        },
      ],
    },
  } as AssemblyOption

  expect(getGroupType(fakeAssemblyOptionGroup)).toBe(GROUP_TYPES.MULTIPLE)
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/modules/useAssemblyOptions.ts from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```typescript
/* eslint-disable @typescript-eslint/no-use-before-define */
import { useMemo } from 'react'
import useProduct, {
  SelectedItem,
  Product,
} from 'vtex.product-context/useProduct'
import { find, propEq, compose, last, split, pathOr } from 'ramda'

import { getGroupType } from './assemblyGroupType'

const splitGroupName = compose(last, split('_'))

type PriceMap = Record<string, Record<string, Record<string, number>>>
type ParsedAssemblyOptions = Record<string, AssemblyOptionGroupState>

const findItemMetadata = (id: string) => find<MetadataItem>(propEq('id', id))

function useAssemblyOptions(): ParsedAssemblyOptions | null {
  const { product, selectedItem } = useProduct()

  return useMemo(() => {
    if (!selectedItem || !product) {
      return null
    }

    const assemblyOptions = getItemAssemblyOptions(
      selectedItem,
      product.itemMetadata
    )

    if (!assemblyOptions || assemblyOptions.length === 0) {
      return null
    }

    const priceMap = parsePriceMap(product.itemMetadata)

    return parseAssemblyOptions(assemblyOptions, priceMap, product)
  }, [product, selectedItem])
}

function getItemAssemblyOptions(
  selectedItem: SelectedItem,
  itemMetadata: ItemMetadata
) {
  if (!itemMetadata || !itemMetadata.items) {
    return null
  }

  const metadata = findItemMetadata(selectedItem.itemId)(itemMetadata.items)

  return metadata?.assemblyOptions
}

function parsePriceMap(itemMetadata: ItemMetadata) {
  const result = {} as PriceMap

  for (const priceTableItem of itemMetadata.priceTable) {
    const { type } = priceTableItem

    result[type] = {}
    for (const priceItem of priceTableItem.values) {
      const groupPrices = result[type][priceItem.assemblyId] || {}

      groupPrices[priceItem.id] = priceItem.price
      result[type][priceItem.assemblyId] = groupPrices
    }
  }

  return result
}

// eslint-disable-next-line max-params
function parseAssemblyOptions(
  assemblyOptions: AssemblyOption[],
  priceMap: PriceMap,
  product: Product,
  parentItemId?: string,
  treePath?: TreePath[]
): ParsedAssemblyOptions {
  const assemblyOptionsParsed = assemblyOptions.reduce<ParsedAssemblyOptions>(
    // eslint-disable-next-line no-shadow
    (assemblyOptionsParsed, assemblyOption) => {
      if (!assemblyOption.composition && !assemblyOption.inputValues) {
        return assemblyOptionsParsed
      }

      const currentTreePath =
        parentItemId && treePath
          ? [...treePath, { itemId: parentItemId, groupId: assemblyOption.id }]
          : []

      if (assemblyOption.composition) {
        const items = assemblyOption.composition.items
          ? assemblyItems(priceMap, product, currentTreePath, assemblyOption)
          : undefined

        assemblyOptionsParsed[assemblyOption.id] = {
          id: assemblyOption.id,
          minQuantity: assemblyOption.composition.minQuantity,
          maxQuantity: assemblyOption.composition.maxQuantity,
          groupName: splitGroupName(assemblyOption.id),
          treePath: currentTreePath,
          type: getGroupType(assemblyOption),
          inputValues: assemblyOption.inputValues,
          required: assemblyOption.required,
          items,
        } as AssemblyOptionGroup
      } else {
        assemblyOptionsParsed[assemblyOption.id] = {
          id: assemblyOption.id,
          minQuantity: undefined,
          maxQuantity: undefined,
          groupName: splitGroupName(assemblyOption.id),
          treePath: currentTreePath,
          type: getGroupType(assemblyOption),
          inputValues: assemblyOption.inputValues,
          required: assemblyOption.required,
          items: undefined,
        } as AssemblyOptionGroupInputValue
      }

      return assemblyOptionsParsed
    },
    {}
  )

  return assemblyOptionsParsed
}

// eslint-disable-next-line max-params
function assemblyItems(
  priceMap: PriceMap,
  product: Product,
  currentTreePath: TreePath[],
  assemblyOption: AssemblyOption
) {
  if (!assemblyOption.composition || !assemblyOption.composition.items) {
    return undefined
  }

  return assemblyOption.composition.items.reduce<Record<string, AssemblyItem>>(
    (items, assemblyItem) => {
      const optionMetadata = findItemMetadata(assemblyItem.id)(
        product.itemMetadata.items
      )

      if (!optionMetadata) {
        return items
      }

      // Recursively parse children of this assembly option
      const children =
        optionMetadata.assemblyOptions.length > 0
          ? parseAssemblyOptions(
              // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
              optionMetadata!.assemblyOptions,
              priceMap,
              product,
              assemblyItem.id,
              currentTreePath
            )
          : null

      items[assemblyItem.id] = {
        image: optionMetadata.imageUrl,
        name: optionMetadata.name,
        id: assemblyItem.id,
        price: pathOr(
          0,
          [assemblyItem.priceTable, assemblyOption.id, assemblyItem.id],
          priceMap
        ),
        minQuantity: assemblyItem.minQuantity,
        maxQuantity: assemblyItem.maxQuantity,
        seller: assemblyItem.seller,
        initialQuantity: assemblyItem.initialQuantity,
        quantity: assemblyItem.initialQuantity,
        children,
      }

      return items
    },
    {}
  )
}

export default useAssemblyOptions

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/modules/subscriptions.ts from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { defineMessages, IntlShape } from 'react-intl'

type SubscriptionFrequencyType = 'year' | 'month' | 'week' | 'day'
type SubscriptionFrequency = {
  interval: number
  type: SubscriptionFrequencyType
}

const FREQUENCY_PATTERN = /(\d{1,3})?\s*(\w*?)(?:$|s|ly)/i

const SUBSCRIPTION_PREFIX = `vtex.subscription`
const SUBSCRIPTION_KEY_PREFIX = `${SUBSCRIPTION_PREFIX}.key`

export const SUBSCRIPTION_KEY_FREQUENCY = `${SUBSCRIPTION_KEY_PREFIX}.frequency`
export const SUBSCRIPTION_KEY_PURCHASEDAY = `${SUBSCRIPTION_KEY_PREFIX}.purchaseday`

const messages = defineMessages({
  subscription: { id: 'store/product-customizer.subscription' },
  frequency: { id: 'store/product-customizer.subscription.frequency' },
  purchaseday: { id: 'store/product-customizer.subscription.purchaseday' },
  purchaseDayLabel: {
    id: 'store/product-customizer.subscription.purchaseday.day',
  },
})

const frequencyMessages = defineMessages({
  day: { id: 'store/product-customizer.subscription.frequency.day' },
  month: { id: 'store/product-customizer.subscription.frequency.month' },
  week: { id: 'store/product-customizer.subscription.frequency.week' },
  year: { id: 'store/product-customizer.subscription.frequency.year' },
})

export function isSubscriptionRelated(id: string) {
  return id.indexOf(SUBSCRIPTION_PREFIX) === 0
}

export function isSubscriptionKey(id: string) {
  return id.indexOf(SUBSCRIPTION_KEY_PREFIX) === 0
}

function getSubscriptionMessageId(id: string) {
  // uses the 'keyName' of vtex.subscription.key.{keyName}
  if (isSubscriptionKey(id)) {
    return messages[
      id.slice(SUBSCRIPTION_KEY_PREFIX.length + 1) as keyof typeof messages
    ]
  }

  // uses 'subscriptions' for vtex.subscription.*
  if (isSubscriptionRelated(id)) {
    return messages.subscription
  }

  return { id }
}

export function formatSubscriptionLabel(id: string, intl: IntlShape) {
  const messageId = getSubscriptionMessageId(id)

  if (!messageId) return id

  return intl.formatMessage(messageId)
}

export function parseFrequency(
  frequency?: string
): SubscriptionFrequency | undefined {
  if (frequency == null) return

  const match = frequency.trim().match(FREQUENCY_PATTERN)

  if (!match) {
    return
  }

  const [, count = '1', type] = match

  if (!type) return

  return {
    interval: +count,
    type: type as SubscriptionFrequencyType,
  }
}

export function formatSubscriptionOptions({
  inputId,
  inputDomain,
  frequency,
  intl,
}: {
  inputId: string
  inputDomain: string[]
  frequency: SubscriptionFrequency
  intl: IntlShape
}) {
  if (inputId === SUBSCRIPTION_KEY_FREQUENCY) {
    return inputDomain.map((value) => {
      const { interval, type } = parseFrequency(value) ?? {}
      let inputLabel = inputId

      if (type) {
        inputLabel = intl.formatMessage(frequencyMessages[type], {
          interval,
        })
      }

      return { label: inputLabel, value }
    })
  }

  if (inputId === SUBSCRIPTION_KEY_PURCHASEDAY && frequency.type) {
    const displayWeekday = frequency.type === 'day' || frequency.type === 'week'

    // use object to remove possible duplicates
    const options: Record<string, string> = {}

    inputDomain.forEach((value) => {
      let numericValue = +value.trim()

      // Purchase day is a single field in the back-end.
      // However, given the possibility of having both weekly and monthly,
      // it's possible to end up with values like 20, 25, 28
      // for a weekly interval, which accepts only values between 0 and 6
      // the if below fixes the value and any possible duplicate week days
      // are removed, preventing a broken UI.
      if (displayWeekday) {
        if (numericValue > 6) {
          // clamp to between 0..6 if number is greater than 6
          numericValue %= 7
        }
      }
      // for the same reason above, we can end up with a monthly config
      // with the initial day as "0", which is invalid
      else if (numericValue === 0) {
        numericValue = 1
      }

      const optionLabel = displayWeekday
        ? intl.formatDate(new Date(0, 0, numericValue), { weekday: 'long' })
        : intl.formatMessage(messages.purchaseDayLabel, { day: numericValue })

      options[optionLabel] = `${numericValue}`
    })

    return Object.entries(options).map(([label, value]) => ({ label, value }))
  }

  return inputDomain.map((value) => ({ label: value, value }))
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/modules/inputValueType.ts from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```typescript
// eslint-disable-next-line no-restricted-syntax
export enum InputValueType {
  'TEXT' = 'TEXT',
  'BOOLEAN' = 'BOOLEAN',
  'OPTIONS' = 'OPTIONS',
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/modules/slugify.ts from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```typescript
import slugify from 'slugify'

export default function slug(str: string) {
  const replaced = str?.replace(/[*+~.()'"!:@&[\]]/g, '') || ''
  const slugified = slugify(replaced, { lower: true }) || ''

  return slugified
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/modules/assemblyGroupType.ts from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { propEq } from 'ramda'

const maxIsOne = propEq('maxQuantity', 1)

export const GROUP_TYPES: Record<string, GroupTypes> = {
  SINGLE: 'SINGLE',
  TOGGLE: 'TOGGLE',
  MULTIPLE: 'MULTIPLE',
}

export const getGroupType = (assemblyOption: AssemblyOption): GroupTypes => {
  const compositionMinQuantity = assemblyOption.composition?.minQuantity ?? 0
  const compositionMaxQuantity = assemblyOption.composition?.maxQuantity ?? 0

  if (compositionMinQuantity === 1 && compositionMaxQuantity === 1) {
    return GROUP_TYPES.SINGLE
  }

  if (assemblyOption.composition?.items.every(maxIsOne)) {
    return GROUP_TYPES.TOGGLE
  }

  return GROUP_TYPES.MULTIPLE
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyContext/Group.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, {
  createContext,
  useContext,
  Dispatch,
  useReducer,
  FC,
  useCallback,
} from 'react'
import { path } from 'ramda'

import { GROUP_TYPES } from '../../modules/assemblyGroupType'

type DispatchAction =
  | SetQuantityAction
  | SetInputValueAction
  | OptinAction
  | UpdateChildrenAction

type SetQuantityAction = {
  type: 'SET_QUANTITY'
  args: {
    itemId: string
    newQuantity: number
    type: string
    groupPath: string[]
  }
}

type SetInputValueAction = {
  type: 'SET_INPUT_VALUE'
  args: {
    inputValueLabel: string
    inputValue: string | boolean
    groupPath: string[]
  }
}

type OptinAction = {
  type: 'OPTIN'
  args: {
    groupPath: string[]
  }
}

type UpdateChildrenAction = {
  type: 'UPDATE_CHILDREN'
  args:
    | SetQuantityAction['args']
    | SetInputValueAction['args']
    | OptinAction['args']
}

export const ProductAssemblyDispatchContext = createContext<
  Dispatch<DispatchAction>
>(() => {})

export const ProductAssemblyGroupContext = createContext<
  AssemblyOptionGroupState | undefined
>(undefined)

const initState = (assemblyOption: AssemblyOptionGroupState) => {
  const groupPath = getGroupPath(assemblyOption.treePath)
  const quantitySum = Object.values(assemblyOption.items ?? {}).reduce(
    (acc, { quantity }) => acc + quantity,
    0
  )

  const valuesOfInputValues = assemblyOption.inputValues.reduce<
    Record<string, string | boolean>
  >((acc, inputValue) => {
    acc[inputValue.label] = inputValue.defaultValue

    return acc
  }, {})

  assemblyOption.path = assemblyOption.path ?? groupPath
  assemblyOption.quantitySum = assemblyOption.quantitySum ?? quantitySum
  assemblyOption.optin = assemblyOption.optin ?? assemblyOption.required
  assemblyOption.valuesOfInputValues =
    assemblyOption.valuesOfInputValues ?? valuesOfInputValues

  return assemblyOption
}

const useRecursiveDispatch = ({
  dispatch,
}: {
  dispatch: Dispatch<DispatchAction>
}) => {
  const parentDispatch = useContext(ProductAssemblyDispatchContext)

  const recursiveDispatch = useCallback(
    (action: DispatchAction) => {
      dispatch(action)
      parentDispatch({
        type: 'UPDATE_CHILDREN',
        args: action.args,
      })
    },
    [dispatch, parentDispatch]
  )

  return recursiveDispatch
}

export const ProductAssemblyGroupContextProvider: FC<ProductAssemblyGroupContextProviderProps> = ({
  assemblyOption,
  children,
  initiallyOpened = 'required',
}) => {
  if (initiallyOpened === 'always') {
    assemblyOption.optin = true
  }

  const [state, dispatch] = useReducer(reducer, assemblyOption, initState)

  const recursiveDispatch = useRecursiveDispatch({
    dispatch,
  })

  return (
    <ProductAssemblyDispatchContext.Provider value={recursiveDispatch}>
      <ProductAssemblyGroupContext.Provider value={state}>
        {children}
      </ProductAssemblyGroupContext.Provider>
    </ProductAssemblyDispatchContext.Provider>
  )
}

function getGroupPath(assemblyTreePath?: TreePath[]) {
  const treePath = assemblyTreePath ?? []

  let groupPath: string[] = []

  for (const currentPath of treePath) {
    groupPath = groupPath.concat([
      'items',
      currentPath.itemId,
      'children',
      currentPath.groupId,
    ])
  }

  return groupPath
}

interface ProductAssemblyGroupContextProviderProps {
  assemblyOption: AssemblyOptionGroupState
  initiallyOpened?: string
}

export const useProductAssemblyGroupDispatch = () =>
  useContext(ProductAssemblyDispatchContext)

export const useProductAssemblyGroupState = () =>
  useContext(ProductAssemblyGroupContext)

const checkSamePath = (path1: string[], path2: string[]) => {
  return path1.join('/') === path2.join('/')
}

const getGroupState = (
  state: AssemblyOptionGroupState,
  groupPath: string[]
) => {
  return checkSamePath(state.path, groupPath)
    ? (state as AssemblyOptionGroupState)
    : (path(groupPath, state) as AssemblyOptionGroupState)
}

function reducer(
  state: AssemblyOptionGroupState,
  action: DispatchAction
): AssemblyOptionGroupState {
  switch (action.type) {
    case 'OPTIN': {
      const { groupPath } = action.args
      const groupState = getGroupState(state, groupPath)

      groupState.optin = !groupState.optin

      return { ...state }
    }

    case 'SET_INPUT_VALUE': {
      const { groupPath, inputValue, inputValueLabel } = action.args
      const groupState = getGroupState(state, groupPath)

      groupState.valuesOfInputValues[inputValueLabel] = inputValue

      return { ...state }
    }

    case 'SET_QUANTITY': {
      if (!state.items) {
        return state
      }

      const {
        itemId,
        newQuantity,
        type,
        groupPath,
      } = action.args as SetQuantityAction['args']

      const groupState = (path(groupPath, state) ??
        state) as AssemblyOptionGroup

      if (type === GROUP_TYPES.SINGLE) {
        groupState.items = removeAllItems(groupState.items)
      }

      const newItems = {
        ...groupState.items,
        ...(itemId && groupState.items && groupState.items[itemId]
          ? {
              [itemId]: { ...groupState.items[itemId], quantity: newQuantity },
            }
          : {}),
      }

      const newQuantitySum = Object.values(newItems).reduce(
        (acc, { quantity }) => acc + quantity,
        0
      )

      groupState.quantitySum = newQuantitySum
      groupState.items = newItems

      return { ...state }
    }

    case 'UPDATE_CHILDREN': {
      if (
        !('itemId' in action.args) ||
        !('newQuantity' in action.args) ||
        !('type' in action.args)
      ) {
        return state
      }

      const { itemId, newQuantity, type, groupPath } = action.args

      const groupState = (path(groupPath, state) ??
        state) as AssemblyOptionGroup

      if (type === GROUP_TYPES.SINGLE) {
        groupState.items = removeAllItems(groupState.items)
      }

      if (groupState?.items && groupState.items[itemId]) {
        groupState.items[itemId].quantity = newQuantity
      }

      const newQuantitySum = Object.values(groupState?.items ?? {}).reduce(
        (acc, { quantity }) => acc + quantity,
        0
      )

      groupState.quantitySum = newQuantitySum

      return { ...state }
    }

    default:
      return state
  }
}

function removeAllItems(items: Record<string, AssemblyItem>) {
  return Object.keys(items).reduce<Record<string, AssemblyItem>>(
    (acc, itemId) => {
      acc[itemId] = {
        ...items[itemId],
        quantity: 0,
      }

      return acc
    },
    {}
  )
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyContext/Item.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC, createContext, useContext } from 'react'

type AssemblyItemContext = AssemblyItem | null

export const ProductAssemblyItemContext = createContext<AssemblyItemContext>(
  null
)

export const ProductAssemblyItemProvider: FC<Props> = ({ item, children }) => {
  return (
    <ProductAssemblyItemContext.Provider value={item}>
      {children}
    </ProductAssemblyItemContext.Provider>
  )
}

interface Props {
  item: AssemblyItem | null
}

export const useProductAssemblyItem = () =>
  useContext(ProductAssemblyItemContext)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/Buttons/MovingBottomButton.jsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```jsx
import React, { Fragment, PureComponent } from 'react'

import AddToCart from './AddToCart'

class MovingBottomButton extends PureComponent {
  scrollingButton = React.createRef()
  fixedButton = React.createRef()

  showScrollingButton = false

  componentDidMount() {
    window.addEventListener('scroll', this.handleScroll)
  }

  componentWillUnmount() {
    window.removeEventListener('scroll', this.handleScroll)
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.ready && this.props.ready) {
      this.handleScroll()
    }
  }

  handleScroll = () => {
    const fixedButtonY = this.fixedButton.current.offsetTop
    const scrollingButtonY =
      this.scrollingButton.current.offsetTop - window.pageYOffset

    if (scrollingButtonY <= fixedButtonY) {
      if (!this.showScrollingButton) {
        this.showScrollingButton = true
        this.fixedButton.current.style.visibility = 'hidden'
        this.scrollingButton.current.style.visibility = 'visible'
      }
    }

    if (scrollingButtonY > fixedButtonY) {
      if (this.showScrollingButton) {
        this.showScrollingButton = false
        this.fixedButton.current.style.visibility = 'visible'
        this.scrollingButton.current.style.visibility = 'hidden'
      }
    }
  }

  render() {
    const { ready, total, onSubmitAddToCart, isLoading } = this.props

    return (
      <Fragment>
        <div
          className="vtex-product-customizer__actions w-100"
          ref={this.scrollingButton}
        >
          <AddToCart
            ready={ready}
            total={total}
            onClick={onSubmitAddToCart}
            isLoading={isLoading}
          />
        </div>
        <div
          className="vtex-product-customizer__actions z-2 fixed bg-white bottom-0 w-40-ns ph1-ns w-100 bt b--light-gray"
          ref={this.fixedButton}
        >
          <AddToCart
            ready={ready}
            total={total}
            onClick={onSubmitAddToCart}
            isLoading={isLoading}
          />
        </div>
      </Fragment>
    )
  }
}

export default MovingBottomButton

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/Buttons/AddToCart.jsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```jsx
import PropTypes from 'prop-types'
import React, { memo } from 'react'
import Button from 'vtex.styleguide/Button'
import { FormattedMessage } from 'react-intl'
import ProductPrice from 'vtex.store-components/ProductPrice'

const AddToCart = ({ total, ready, onClick, isLoading }) => {
  if (!ready) return null
  const calculatedTotal = (total / 100).toFixed(2)

  return (
    <div className="actions--add-to-cart tc pa5">
      <Button type="submit" onClick={onClick} isLoading={isLoading} block>
        <div className="flex w-100 justify-between items-center">
          <FormattedMessage id="store/product-customizer.add-to-cart" />
          <ProductPrice
            showLabels={false}
            showListPrice={false}
            sellingPrice={calculatedTotal}
            listPrice={calculatedTotal}
          />
        </div>
      </Button>
    </div>
  )
}

AddToCart.propTypes = {
  /* Total of current selected variations */
  total: PropTypes.number,
  /* Toggle the button state if there's not selected variations */
  ready: PropTypes.bool,
  /* Handles submit skus to orderForm */
  onClick: PropTypes.func,
  /* Show loading state when skus are being added to orderForm */
  isLoading: PropTypes.bool,
}

export default memo(AddToCart)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionItemQuantity.test.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { render, fireEvent } from '@vtex/test-tools/react'
import useProduct, { ProductContext } from 'vtex.product-context/useProduct'
import { useProductDispatch } from 'vtex.product-context/ProductDispatchContext'

import ProductAssemblyOptions from '../../ProductAssemblyOptions'
import productPizza from '../../__fixtures__/productPizza.json'
import productHamburguer from '../../__fixtures__/productHamburguer.json'
import ProductAssemblyOptionItemName from './ProductAssemblyOptionItemName'
import ProductAssemblyOptionItemQuantity from './ProductAssemblyOptionItemQuantity'
import ProductAssemblyOptionItemCustomize from '../../ProductAssemblyOptionItemCustomize'

const mockUseProduct = useProduct as jest.Mock<ProductContext>
const mockedUseProductDispatch = useProductDispatch as jest.Mock<
  () => jest.Mock
>

let mockedDispatch = jest.fn()

beforeEach(() => {
  mockedDispatch = jest.fn()
  mockedUseProductDispatch.mockImplementation(() => mockedDispatch)
  mockUseProduct.mockReset()
})

const ItemNameAndQuantity = () => (
  <>
    <ProductAssemblyOptionItemName />
    <ProductAssemblyOptionItemQuantity />
  </>
)

function renderComponent() {
  return render(
    <ProductAssemblyOptions>
      <ItemNameAndQuantity />

      <ProductAssemblyOptionItemCustomize>
        <ItemNameAndQuantity />

        <ProductAssemblyOptionItemCustomize>
          <ItemNameAndQuantity />
        </ProductAssemblyOptionItemCustomize>
      </ProductAssemblyOptionItemCustomize>
    </ProductAssemblyOptions>
  )
}

function queryQuantityInput(container: HTMLElement, itemId: string | number) {
  return container.querySelector(
    `[data-testid="multipleItemQuantitySelector-${itemId}"] input`
  )
}

function queryQuantityIncreaseButton(
  container: HTMLElement,
  itemId: string | number
) {
  return container.querySelector(
    `[data-testid="multipleItemQuantitySelector-${itemId}"] [aria-label="+"]`
  ) as HTMLButtonElement
}

function getLastMockedDispatchArgs(dispatch: jest.Mock) {
  return dispatch.mock.calls[dispatch.mock.calls.length - 1][0].args
}

test('should allow changing quantity respecting the min and max quantity', () => {
  mockUseProduct.mockImplementation(() => ({
    product: productPizza.data.product,
    selectedItem: productPizza.data.product.items[0],
    selectedQuantity: 1,
  }))

  const { container } = renderComponent()

  const colaId = 110
  const colaDietId = 120

  const colaQuantityInput = queryQuantityInput(container, colaId)!

  // Decrease quantity of Cola from 2 to 1
  fireEvent.change(colaQuantityInput, { target: { value: '1' } })

  // Check if increase button is enable to go back to 2
  const colaIncreaseButton = queryQuantityIncreaseButton(container, colaId)!

  expect(colaIncreaseButton.disabled).toBeFalsy()

  // Should also be able to increase another item
  const colaDietIncreaseButton = queryQuantityIncreaseButton(
    container,
    colaDietId
  )!

  expect(colaDietIncreaseButton.disabled).toBeFalsy()

  // Increase Cola Diet to quantity 1
  const colaDietQuantityInput = queryQuantityInput(container, colaDietId)!

  fireEvent.change(colaDietQuantityInput, { target: { value: '1' } })

  // Make sure you can't add more quantity of any item
  expect(colaIncreaseButton.disabled).toBeTruthy()
  expect(colaDietIncreaseButton.disabled).toBeTruthy()
})

test('Check deep assembly options quantity', () => {
  window.scroll = () => {}
  mockUseProduct.mockImplementation(() => ({
    product: productHamburguer.data.product,
    selectedItem: productHamburguer.data.product.items[0],
    selectedQuantity: 1,
  }))

  const { getAllByText, getByText, getAllByLabelText } = renderComponent()

  const [sndComboOneCustomize, _, fstComboOneCustomize] = getAllByText(
    'Customize'
  )

  // Open combo2combo1 customization to select lanche generico
  fireEvent.click(sndComboOneCustomize)
  // Select lanche generico
  fireEvent.click(getAllByText('Lanche Genérico')[0])
  fireEvent.click(getByText('Done'))

  const mockedDispatchCallsFirstLength = mockedDispatch.mock.calls.length
  const [combo2Combo1] = getLastMockedDispatchArgs(mockedDispatch).groupItems

  // Expect Combo2-Combo1 to be selected
  expect(combo2Combo1.quantity).toBe(1)
  // Check if Lanche Generico is selected and Big Macct isn't
  expect(combo2Combo1.children['Combo 1_Lanche'][1].quantity).toBe(1)
  expect(combo2Combo1.children['Combo 1_Lanche'][0].quantity).toBe(0)

  // ------ TEST COMBO 1 COMBO 1
  // Open combo1combo1 customization to select lanche generico
  fireEvent.click(fstComboOneCustomize)
  // Select lanche generico
  fireEvent.click(getAllByText('Lanche Genérico')[1])

  const [combo1Combo1] = getLastMockedDispatchArgs(mockedDispatch).groupItems

  // Expect Combo1-Combo1 to be selected
  expect(combo1Combo1.quantity).toBe(1)
  // Check if Lanche Generico is selected and Big Macct isn't
  expect(combo1Combo1.children['Combo 1_Lanche'][1].quantity).toBe(1)
  expect(combo1Combo1.children['Combo 1_Lanche'][0].quantity).toBe(0)

  // Assure that dispatch is called
  expect(mockedDispatch.mock.calls).not.toHaveLength(
    mockedDispatchCallsFirstLength
  )

  // Check deepest child test -> Combo 2 -> Combo 1 -> Lanche Genérico -> Bacon
  const lancheGenericoCustomize = getAllByText('Customize')[5]

  fireEvent.click(lancheGenericoCustomize)
  fireEvent.click(getByText('Add Extra'))
  fireEvent.click(getAllByLabelText('+')[0])
  const combo2Combo1Extra = getLastMockedDispatchArgs(mockedDispatch)
    .groupItems[0].children['Combo 1_Lanche'][1].children.Extra_Extra

  expect(combo2Combo1Extra[0].initialQuantity).toBe(0)
  expect(combo2Combo1Extra[0].quantity).toBe(1)
  expect(combo2Combo1Extra[1].quantity).toBe(0)
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionItemImage.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { head } from 'ramda'
import React, { FC } from 'react'
import { useCssHandles } from 'vtex.css-handles'

import { useProductAssemblyItem } from '../ProductAssemblyContext/Item'
import { withItem } from './withItem'

const CSS_HANDLES = ['productAssemblyOptionItemImage'] as const

const IMAGE_SIZE = 54

export const imageUrlForSize = (imageUrl: string, size: number) => {
  if (!imageUrl) {
    return ''
  }

  const urlSplitted = imageUrl.split('/')
  const idsStringIdx = urlSplitted.findIndex((content) => content === 'ids')

  if (idsStringIdx < 0 || idsStringIdx === urlSplitted.length - 1) {
    return imageUrl
  }

  const sizeStringIdx = idsStringIdx + 1
  const sizeString = urlSplitted[sizeStringIdx]
  const imageId = head(sizeString.split('-'))
  const multiplier = window?.devicePixelRatio || 1
  const newSizeString = `${imageId}-${size * multiplier}-auto`

  return [
    ...urlSplitted.slice(0, sizeStringIdx),
    newSizeString,
    ...urlSplitted.slice(sizeStringIdx + 1),
  ].join('/')
}

const ProductAssemblyOptionItemImage: FC = () => {
  const handles = useCssHandles(CSS_HANDLES)
  const { image, name } = useProductAssemblyItem() as AssemblyItem

  return (
    <img
      alt={name}
      className={`${handles.productAssemblyOptionItemImage}`}
      src={imageUrlForSize(image, IMAGE_SIZE)}
      width={IMAGE_SIZE}
      height={IMAGE_SIZE}
    />
  )
}

export default withItem(ProductAssemblyOptionItemImage)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/styles.css from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```css
.multipleItemQuantitySelector button {
  background: transparent;
}
.multipleItemQuantitySelector input {
  background: transparent;
}

.frameAround {
  bottom: -0.25rem;
  top: -0.25rem;
  left: -0.25rem;
  right: -0.25rem;
}
```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionItemPrice.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC } from 'react'
import { useCssHandles } from 'vtex.css-handles'
import ProductPrice from 'vtex.store-components/ProductPrice'

import { useProductAssemblyItem } from '../ProductAssemblyContext/Item'
import { withItem } from './withItem'

const CSS_HANDLES = [
  'productAssemblyOptionItemPrice',
  'productAssemblyOptionItemSign',
] as const

const sumAssembliesPrice = (
  assemblyOptions: Record<string, AssemblyOptionGroupState>
): number => {
  const assembliesGroupItems = Object.values(assemblyOptions)

  return assembliesGroupItems.reduce((sum, group) => {
    const groupPrice = Object.values(group.items ?? {}).reduce(
      (groupSum, item) => {
        const childrenPrice = item.children
          ? sumAssembliesPrice(item.children)
          : 0

        return groupSum + (item.price + childrenPrice) * item.quantity
      },
      0
    )

    return groupPrice + sum
  }, 0)
}

const Price: FC = () => {
  const handles = useCssHandles(CSS_HANDLES)
  const { price, children } = useProductAssemblyItem() as AssemblyItem

  if ((price == null || price === 0) && !children) {
    return null
  }

  const childrenCost = children ? sumAssembliesPrice(children) : 0
  const totalPrice = (price + childrenCost) / 100

  return (
    <div
      className={`${handles.productAssemblyOptionItemPrice} flex items-center t-body b c-on-base`}
    >
      <span className={handles.productAssemblyOptionItemSign}>+ </span>
      <ProductPrice
        showLabels={false}
        showListPrice={false}
        sellingPrice={totalPrice}
      />
    </div>
  )
}

export default withItem(Price)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionsItem.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC } from 'react'
import { useCssHandles } from 'vtex.css-handles'

import { useProductAssemblyItem } from '../ProductAssemblyContext/Item'
import {
  useProductAssemblyGroupState,
  useProductAssemblyGroupDispatch,
} from '../ProductAssemblyContext/Group'
import { GROUP_TYPES } from '../../modules/assemblyGroupType'

const CSS_HANDLES = ['itemContainer'] as const

const useSelectOption = () => {
  const { id, quantity } = useProductAssemblyItem() as AssemblyItem
  const { type, path } = useProductAssemblyGroupState() as AssemblyOptionGroup
  const dispatch = useProductAssemblyGroupDispatch()

  if (quantity === 0 && type === GROUP_TYPES.SINGLE) {
    return () =>
      dispatch({
        type: 'SET_QUANTITY',
        args: {
          itemId: id,
          newQuantity: 1,
          type: GROUP_TYPES.SINGLE,
          groupPath: path,
        },
      })
  }

  if (type === GROUP_TYPES.TOGGLE) {
    return () =>
      dispatch({
        type: 'SET_QUANTITY',
        args: {
          itemId: id,
          newQuantity: quantity === 1 ? 0 : 1,
          type: GROUP_TYPES.TOGGLE,
          groupPath: path,
        },
      })
  }

  return undefined
}

const ProductAssemblyOptionsItem: FC = ({ children }) => {
  const handles = useCssHandles(CSS_HANDLES)
  const select = useSelectOption()

  const handleClick = () => {
    select?.()
  }

  const handleKeyDown = ({ key }: React.KeyboardEvent<HTMLDivElement>) => {
    const SPACE = ' '
    const ENTER = 'Enter'

    if (key === SPACE || key === ENTER) {
      select?.()
    }
  }

  return (
    <div
      tabIndex={0}
      role="button"
      onClick={handleClick}
      onKeyDown={handleKeyDown}
      className={`${handles.itemContainer} hover-bg-muted-5 bb b--muted-5 pa3 pointer`}
    >
      {children}
    </div>
  )
}

export default ProductAssemblyOptionsItem

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionItemQuantity.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC } from 'react'
import { Checkbox, Radio, NumericStepper } from 'vtex.styleguide'
import { useCssHandles } from 'vtex.css-handles'

import { useProductAssemblyItem } from '../ProductAssemblyContext/Item'
import { GROUP_TYPES } from '../../modules/assemblyGroupType'
import {
  useProductAssemblyGroupState,
  useProductAssemblyGroupDispatch,
} from '../ProductAssemblyContext/Group'
import { withItem } from './withItem'

const CSS_HANDLES = ['multipleItemQuantitySelector'] as const

const Single: FC = () => {
  const { id, quantity } = useProductAssemblyItem() as AssemblyItem
  const { path } = useProductAssemblyGroupState() as AssemblyOptionGroup
  const dispatch = useProductAssemblyGroupDispatch()
  const selected = quantity === 1

  const handleClick = () => {
    if (quantity === 0) {
      dispatch({
        type: 'SET_QUANTITY',
        args: {
          itemId: id,
          newQuantity: 1,
          type: GROUP_TYPES.SINGLE,
          groupPath: path,
        },
      })
    }
  }

  const handleKey = ({ key }: React.KeyboardEvent<HTMLDivElement>) => {
    const SPACE = ' '
    const ENTER = 'Enter'

    if (key === SPACE || key === ENTER) {
      handleClick()
    }
  }

  return (
    <div
      role="button"
      tabIndex={-1}
      onKeyDown={handleKey}
      onClick={handleClick}
    >
      {/* TODO: Properly use Radio from Styleguide */}
      <Radio
        checked={selected}
        id=""
        label=""
        name=""
        value=""
        onChange={() => {}}
      />
    </div>
  )
}

const Toggle: FC = () => {
  const { id, quantity, minQuantity } = useProductAssemblyItem() as AssemblyItem
  const { path } = useProductAssemblyGroupState() as AssemblyOptionGroup
  const dispatch = useProductAssemblyGroupDispatch()

  const selected = quantity === 1
  const disabled = minQuantity === 1

  return (
    <Checkbox
      id={`${path}-selected-${id}`}
      disabled={disabled}
      name={`selected-${id}`}
      checked={selected}
      onChange={() => {
        dispatch({
          type: 'SET_QUANTITY',
          args: {
            itemId: id,
            newQuantity: quantity === 1 ? 0 : 1,
            type: GROUP_TYPES.TOGGLE,
            groupPath: path,
          },
        })
      }}
    />
  )
}

const Multiple: FC = () => {
  const {
    quantity,
    maxQuantity,
    minQuantity,
    id,
  } = useProductAssemblyItem() as AssemblyItem

  const {
    path,
    maxQuantity: groupMaxQuantity,
    quantitySum,
  } = useProductAssemblyGroupState() as AssemblyOptionGroup

  const dispatch = useProductAssemblyGroupDispatch()

  const canIncrease =
    quantity + 1 <= maxQuantity && quantitySum + 1 <= groupMaxQuantity

  const handles = useCssHandles(CSS_HANDLES)

  return (
    <div
      className={handles.multipleItemQuantitySelector}
      data-testid={`multipleItemQuantitySelector-${id}`}
    >
      <NumericStepper
        lean
        value={quantity}
        minValue={minQuantity}
        maxValue={canIncrease ? undefined : quantity}
        onChange={({ value }: { value: number }) => {
          dispatch({
            type: 'SET_QUANTITY',
            args: {
              itemId: id,
              newQuantity: value,
              type: GROUP_TYPES.MULTIPLE,
              groupPath: path,
            },
          })
        }}
      />
    </div>
  )
}

const Quantity: FC = () => {
  const { type } = useProductAssemblyGroupState() as AssemblyOptionGroup

  if (type === 'SINGLE') {
    return <Single />
  }

  if (type === 'TOGGLE') {
    return <Toggle />
  }

  return <Multiple />
}

export default withItem(Quantity)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/withItem.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC, ComponentType } from 'react'

import { useProductAssemblyItem } from '../ProductAssemblyContext/Item'

export function withItem(WrappedComponent: ComponentType) {
  const WithItem: FC = (props) => {
    const item = useProductAssemblyItem()

    if (!item) {
      return null
    }

    return <WrappedComponent {...props} />
  }

  return WithItem
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionItemCustomize.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC, useState } from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import { useCssHandles } from 'vtex.css-handles'
import { useDevice } from 'vtex.device-detector'
import ProductPrice from 'vtex.store-components/ProductPrice'
import { Button, Modal } from 'vtex.styleguide'

import { ProductAssemblyGroupContextProvider } from '../ProductAssemblyContext/Group'
import { useProductAssemblyItem } from '../ProductAssemblyContext/Item'
import StopPropagation from '../StopPropagation'
import { imageUrlForSize } from './ProductAssemblyOptionItemImage'
import ProductAssemblyOptionsGroup from './ProductAssemblyOptionsGroup'
import { withItem } from './withItem'

const CSS_HANDLES = [
  'modalViewProductContainer',
  'modalViewProductImage',
  'modalViewProductName',
  'modalViewProductInfos',
  'modalViewDoneButton',
  'modalViewTitle',
  'productAssemblyOptionItemCustomize',
  'productAssemblyOptionItemCustomizeLabel',
] as const

const IMG_SIZE = 140

const ModalView: FC<{ closeAction: () => void }> = ({
  children,
  closeAction,
}) => {
  const handles = useCssHandles(CSS_HANDLES)
  const intl = useIntl()
  const {
    image,
    name,
    children: itemChildren,
    price,
  } = useProductAssemblyItem() as AssemblyItem

  return (
    <div className="flex flex-column">
      <div className={`${handles.modalViewProductContainer} flex`}>
        {/* eslint-disable-next-line jsx-a11y/alt-text */}
        <img
          className={handles.modalViewProductImage}
          src={imageUrlForSize(image, IMG_SIZE)}
          width={IMG_SIZE}
          height={IMG_SIZE}
        />
        <div
          className={`${handles.modalViewProductInfos} flex flex-column ml5`}
        >
          <span
            className={`${handles.modalViewProductName} t-heading-5 c-on-base`}
          >
            {name}
          </span>
          <ProductPrice
            showLabels={false}
            showListPrice={false}
            sellingPriceContainerClass="t-heading-3 c-on-base"
            sellingPrice={price / 100}
          />
        </div>
      </div>
      {itemChildren &&
        Object.keys(itemChildren).map((childId) => {
          return (
            <ProductAssemblyGroupContextProvider
              key={childId}
              assemblyOption={itemChildren[childId]}
            >
              <ProductAssemblyOptionsGroup>
                {children}
              </ProductAssemblyOptionsGroup>
            </ProductAssemblyGroupContextProvider>
          )
        })}
      <div className={`${handles.modalViewDoneButton} mt3`}>
        <Button block onClick={closeAction}>
          {intl.formatMessage({
            id: 'store/product-customizer.modal-view.done-button',
          })}
        </Button>
      </div>
    </div>
  )
}

interface Props {
  buttonProps?: ButtonProps
  modalProps?: unknown
}

interface ButtonProps {
  collapse?: 'left' | 'right' | 'none'
}

const ProductAssemblyOptionItemCustomize: FC<Props> = ({
  children,
  buttonProps = {},
  modalProps = {},
}) => {
  const intl = useIntl()
  const handles = useCssHandles(CSS_HANDLES)
  const {
    name,
    children: itemChildren,
  } = useProductAssemblyItem() as AssemblyItem

  const { isMobile } = useDevice()
  const buttonCollapse = buttonProps.collapse
  const [modalOpen, setModalOpen] = useState(false)

  if (!itemChildren) {
    return null
  }

  const handleClick = () => {
    setModalOpen(true)
  }

  const closeAction = () => setModalOpen(false)

  return (
    <div className={handles.productAssemblyOptionItemCustomize}>
      <Button
        variation="tertiary"
        onClick={handleClick}
        collapseLeft={buttonCollapse === 'left'}
        collapseRight={buttonCollapse === 'right'}
        {...buttonProps}
      >
        <div
          className={`${handles.productAssemblyOptionItemCustomizeLabel} c-action-primary t-action`}
        >
          <FormattedMessage id="store/product-customizer.customize" />
        </div>
      </Button>
      <StopPropagation>
        <Modal
          isOpen={modalOpen}
          onClose={closeAction}
          centered={!isMobile}
          title={
            <div className={handles.modalViewTitle}>
              {intl.formatMessage(
                {
                  id: 'store/product-customizer.modal-view.title',
                },
                { name }
              )}
            </div>
          }
          {...modalProps}
        >
          <ModalView closeAction={closeAction}>{children}</ModalView>
        </Modal>
      </StopPropagation>
    </div>
  )
}

export default withItem(ProductAssemblyOptionItemCustomize)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionItemName.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC } from 'react'
import { useCssHandles } from 'vtex.css-handles'

import { useProductAssemblyItem } from '../ProductAssemblyContext/Item'
import { withItem } from './withItem'

const CSS_HANDLES = ['productAssemblyOptionItemName'] as const

const ProductAssemblyOptionItemName: FC = () => {
  const { name } = useProductAssemblyItem() as AssemblyItem
  const handles = useCssHandles(CSS_HANDLES)

  return (
    <span
      className={`${handles.productAssemblyOptionItemName} t-heading-5 c-on-base`}
    >
      {name}
    </span>
  )
}

export default withItem(ProductAssemblyOptionItemName)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionsGroup.test.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { render, fireEvent } from '@vtex/test-tools/react'
import useProduct, { ProductContext } from 'vtex.product-context/useProduct'
import { useProductDispatch } from 'vtex.product-context/ProductDispatchContext'

import ProductAssemblyOptions from '../../ProductAssemblyOptions'
import InputValue from './ProductAssemblyOptionInputValues'
import ProductAssemblyOptionItemName from './ProductAssemblyOptionItemName'
import productAttachment from '../../__fixtures__/productAttachment.json'

const mockedUseProductDispatch = useProductDispatch as jest.Mock<
  () => jest.Mock
>

const mockUseProduct = useProduct as jest.Mock<ProductContext>

function renderComponent(
  props: { initiallyOpened?: 'required' | 'always' } = {}
) {
  return render(
    <ProductAssemblyOptions {...props}>
      <ProductAssemblyOptionItemName />
      <InputValue />
    </ProductAssemblyOptions>
  )
}

let mockedDispatch = jest.fn()

beforeEach(() => {
  mockedDispatch = jest.fn()
  mockedUseProductDispatch.mockImplementation(() => mockedDispatch)
  mockUseProduct.mockReset()
})

describe('Product with required assembly', () => {
  beforeEach(() => {
    mockUseProduct.mockImplementation(() => ({
      product: productAttachment.data.product,
      selectedItem: productAttachment.data.product.items[6],
      selectedQuantity: 1,
    }))
  })

  it('should NOT show Add Customization and Remove button', () => {
    const { queryByText } = renderComponent()

    const addButton = queryByText(/Add Customization/)

    expect(addButton).toBeFalsy()

    const removeButton = queryByText(/Remove/)

    expect(removeButton).toBeFalsy()
  })

  it('should show child elements', () => {
    const { getByText, getByLabelText } = renderComponent()

    expect(getByText(/Customization/)).toBeTruthy()
    expect(getByLabelText(/Font/)).toBeTruthy()
  })

  it('should NOT show Add Customization and Remove button if initiallyOpened is set to always', () => {
    const { queryByText } = renderComponent({ initiallyOpened: 'always' })

    const addButton = queryByText(/Add Customization/)

    expect(addButton).toBeFalsy()

    const removeButton = queryByText(/Remove/)

    expect(removeButton).toBeFalsy()
  })
})

describe('Product with not required assembly', () => {
  beforeEach(() => {
    mockUseProduct.mockImplementation(() => ({
      product: productAttachment.data.product,
      selectedItem: productAttachment.data.product.items[0],
      selectedQuantity: 1,
    }))
  })

  it('Add Customization and Remove button', () => {
    const { getByLabelText, getByText, queryByLabelText } = renderComponent()

    const addButton = getByText(/Add Customization/)

    fireEvent.click(addButton)

    // Show child elements
    getByLabelText(/Font/)

    const removeButton = getByText(/Remove/)

    fireEvent.click(removeButton)

    // No child elements
    const fontInput = queryByLabelText(/Font/)

    expect(fontInput).toBeFalsy()
  })

  it('object set in product context should be empty if Add customization is not pressed or after remove is pressed', () => {
    const { getByText } = renderComponent()

    expect(mockedDispatch.mock.calls[0][0].args.groupInputValues).toMatchObject(
      {}
    )

    const addButton = getByText(/Add Customization/)

    fireEvent.click(addButton)

    expect(mockedDispatch.mock.calls[1][0].args.groupInputValues).toMatchObject(
      {
        Font: 'Sans serif',
        'Front text': '',
        'Back text': '',
        'Glossy print': true,
      }
    )

    const removeButton = getByText(/Remove/)

    fireEvent.click(removeButton)

    expect(mockedDispatch.mock.calls[2][0].args.groupInputValues).toMatchObject(
      {}
    )
  })
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionsGroup.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC, Fragment } from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import { useCssHandles } from 'vtex.css-handles'
import { IOMessage } from 'vtex.native-types'
import { Button } from 'vtex.styleguide'

import {
  formatSubscriptionLabel,
  isSubscriptionRelated,
} from '../../modules/subscriptions'
import useAssemblyOptionsModifications from '../../modules/useAssemblyOptionsModifications'
import {
  useProductAssemblyGroupDispatch,
  useProductAssemblyGroupState,
} from '../ProductAssemblyContext/Group'
import { ProductAssemblyItemProvider } from '../ProductAssemblyContext/Item'
import ProductAssemblyOptionsItem from './ProductAssemblyOptionsItem'

const CSS_HANDLES = [
  'productAssemblyGroupName',
  'productAssemblyGroupNameRow',
  'productAssemblyGroupRequiredTag',
] as const

interface Props {
  initiallyOpened?: 'always' | 'required'
}

const ProductAssemblyOptionsGroup: FC<Props> = ({ children }) => {
  const intl = useIntl()
  const handles = useCssHandles(CSS_HANDLES)
  const assemblyOptionGroup = useProductAssemblyGroupState() as AssemblyOptionGroupState
  const dispatch = useProductAssemblyGroupDispatch()

  useAssemblyOptionsModifications(assemblyOptionGroup)

  const changeOptinInput = () => {
    dispatch({
      type: 'OPTIN',
      args: {
        groupPath: assemblyOptionGroup.path,
      },
    })
  }

  const groupName = isSubscriptionRelated(assemblyOptionGroup.groupName)
    ? formatSubscriptionLabel(assemblyOptionGroup.groupName, intl)
    : assemblyOptionGroup.groupName

  return (
    <Fragment>
      {assemblyOptionGroup.optin === false ? (
        <Button id={groupName} variation="secondary" onClick={changeOptinInput}>
          <IOMessage
            id="store/product-customizer.add-assembly"
            values={{ name: groupName }}
          />
        </Button>
      ) : (
        <Fragment>
          <div
            className={`${handles.productAssemblyGroupNameRow} flex justify-between items-center`}
          >
            <div
              className={`${handles.productAssemblyGroupName} ttc-s pv4 c-muted-2 t-small`}
            >
              {groupName}
            </div>
            {assemblyOptionGroup.required === false ? (
              <div>
                <Button
                  size="small"
                  collapseRight
                  variation="tertiary"
                  onClick={changeOptinInput}
                >
                  <IOMessage
                    id="store/product-customizer.remove-assembly"
                    values={{ name: groupName }}
                  />
                </Button>
              </div>
            ) : (
              <div className={`${handles.productAssemblyGroupRequiredTag} f7`}>
                <FormattedMessage id="store/product-customizer.required" />
              </div>
            )}
          </div>
          {assemblyOptionGroup.items ? (
            Object.values(assemblyOptionGroup.items).map((item) => {
              return (
                <ProductAssemblyItemProvider item={item} key={item.id}>
                  <ProductAssemblyOptionsItem>
                    {children}
                  </ProductAssemblyOptionsItem>
                </ProductAssemblyItemProvider>
              )
            })
          ) : (
            <ProductAssemblyItemProvider item={null}>
              {children}
            </ProductAssemblyItemProvider>
          )}
        </Fragment>
      )}
    </Fragment>
  )
}

export default ProductAssemblyOptionsGroup

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionInputValues.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC, Fragment } from 'react'

import { useProductAssemblyGroupState } from '../ProductAssemblyContext/Group'
import TextInputValue from './InputValue/TextInputValue'
import OptionsInputValue, {
  OptionDisplay,
} from './InputValue/OptionsInputValue'
import BooleanInputValue from './InputValue/BooleanInputValue'
import { InputValueType } from '../../modules/inputValueType'

interface Props {
  optionsDisplay?: OptionDisplay
}

const ProductAssemblyOptionItemInputValues: FC<Props> = ({
  optionsDisplay,
}) => {
  const state = useProductAssemblyGroupState()

  if (!state) {
    return null
  }

  const { inputValues } = state

  return (
    <Fragment>
      {inputValues.map((inputValue, index) => {
        if (inputValue.type === InputValueType.TEXT) {
          return <TextInputValue key={index} inputValueInfo={inputValue} />
        }

        if (inputValue.type === InputValueType.OPTIONS) {
          return (
            <OptionsInputValue
              key={index}
              inputValueInfo={inputValue}
              optionsDisplay={optionsDisplay}
            />
          )
        }

        if (inputValue.type === InputValueType.BOOLEAN) {
          return <BooleanInputValue key={index} inputValueInfo={inputValue} />
        }

        return null
      })}
    </Fragment>
  )
}

export default ProductAssemblyOptionItemInputValues

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionItemChildrenDescription.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC } from 'react'

import { useProductAssemblyItem } from '../ProductAssemblyContext/Item'
import { GROUP_TYPES } from '../../modules/assemblyGroupType'
import { withItem } from './withItem'

const getItemText = (item: AssemblyItem, groupType: GroupTypes) => {
  const deltaQuantity = item.quantity - item.initialQuantity

  if (deltaQuantity === 0) {
    return null
  }

  if (groupType === GROUP_TYPES.SINGLE) {
    return deltaQuantity > 0 ? item.name : null
  }

  if (groupType === GROUP_TYPES.TOGGLE) {
    return deltaQuantity > 0 ? item.name : `No ${item.name}`
  }

  const sign = deltaQuantity < 0 ? '-' : '+'

  return `${sign}${deltaQuantity}x ${item.name}`
}

const ProductAssemblyOptionItemChildrenDescription: FC = () => {
  const { children } = useProductAssemblyItem() as AssemblyItem

  if (!children) {
    return null
  }

  const groups = Object.values(children)

  return (
    <div>
      {groups.map((group) => {
        const assemblyGroup = group as AssemblyOptionGroup

        const items = Object.values(assemblyGroup.items || []).filter(
          ({ quantity, initialQuantity }) => quantity !== initialQuantity
        )

        if (items.length === 0) {
          return null
        }

        return (
          <div className="flex" key={assemblyGroup.id}>
            <div className="mh2">
              {items.map((item) => {
                const itemText = getItemText(item, assemblyGroup.type)

                return (
                  itemText && (
                    <div key={itemText} className="c-on-base t-body">
                      {itemText}
                    </div>
                  )
                )
              })}
            </div>
          </div>
        )
      })}
    </div>
  )
}

export default withItem(ProductAssemblyOptionItemChildrenDescription)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/ProductAssemblyOptions/ProductAssemblyOptionInputValues.test.tsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import {
  render,
  fireEvent,
  within,
  waitForElementToBeRemoved,
} from '@vtex/test-tools/react'
import useProduct, { ProductContext } from 'vtex.product-context/useProduct'
import { useProductDispatch } from 'vtex.product-context/ProductDispatchContext'

import ProductAssemblyOptions from '../../ProductAssemblyOptions'
import InputValue from './ProductAssemblyOptionInputValues'
import productAttachment from '../../__fixtures__/productAttachment.json'
import productRecursive from '../../__fixtures__/productRecursive.json'
import ProductAssemblyOptionItemName from './ProductAssemblyOptionItemName'
import ProductAssemblyOptionItemCustomize from './ProductAssemblyOptionItemCustomize'
import ProductAssemblyOptionItemQuantity from './ProductAssemblyOptionItemQuantity'

const mockedUseProductDispatch = useProductDispatch as jest.Mock<
  () => jest.Mock
>

const mockUseProduct = useProduct as jest.Mock<ProductContext>

function renderComponent(customProps = {}) {
  return render(
    <ProductAssemblyOptions>
      <ProductAssemblyOptionItemName />
      <InputValue {...customProps} />
    </ProductAssemblyOptions>
  )
}

mockUseProduct.mockImplementation(() => ({
  product: productAttachment.data.product,
  selectedItem: productAttachment.data.product.items[6],
  selectedQuantity: 1,
}))

let mockedDispatch = jest.fn()

beforeEach(() => {
  mockedDispatch = jest.fn()
  mockedUseProductDispatch.mockImplementation(() => mockedDispatch)
})

test('should handle Options input value', () => {
  const { getByLabelText } = renderComponent()

  const font = getByLabelText(/Font/) as HTMLSelectElement

  fireEvent.change(font, { target: { value: 'Roman' } })

  expect(font.value).toBe('Roman')
})

test('should handle Text input value', () => {
  const { getByLabelText } = renderComponent()

  const frontText = getByLabelText(/Front text/) as HTMLInputElement

  fireEvent.change(frontText, { target: { value: 'Foobar' } })

  expect(frontText.value).toBe('Foobar')
})

test('should handle Boolean input value', () => {
  const { getByLabelText } = renderComponent()

  const glossyPrint = getByLabelText(/Glossy print/) as HTMLInputElement

  fireEvent.click(glossyPrint)

  expect(glossyPrint.checked).toBe(false)
})

test('should show other type of options', () => {
  const { getByText } = renderComponent({ optionsDisplay: 'box' })

  getByText(/Sans serif/)

  const romanOption = getByText(/Roman/)

  fireEvent.click(romanOption)

  const lastCall =
    mockedDispatch.mock.calls[mockedDispatch.mock.calls.length - 1]

  expect(lastCall[0].args.groupInputValues.Font).toBe('Roman')
})

test('should trigger changes to Product Context', () => {
  const { getByLabelText } = renderComponent()

  const font = getByLabelText(/Font/) as HTMLSelectElement

  fireEvent.change(font, { target: { value: 'Roman' } })

  expect(mockedDispatch.mock.calls[0][0].args.groupInputValues.Font).toBe(
    'Roman'
  )
  expect(
    mockedDispatch.mock.calls[0][0].args.groupInputValues['Front text']
  ).toBe('')
  expect(
    mockedDispatch.mock.calls[0][0].args.groupInputValues['Back text']
  ).toBe('')
  expect(
    mockedDispatch.mock.calls[0][0].args.groupInputValues['Glossy print']
  ).toBe(true)

  const frontText = getByLabelText(/Front text/) as HTMLInputElement

  fireEvent.change(frontText, { target: { value: 'Foobar' } })
  const glossyPrint = getByLabelText(/Glossy print/) as HTMLInputElement

  fireEvent.click(glossyPrint)

  expect(mockedDispatch.mock.calls).toHaveLength(4)
  expect(mockedDispatch.mock.calls[0][0].type).toBe('SET_ASSEMBLY_OPTIONS')
  expect(mockedDispatch.mock.calls[1][0].args.groupInputValues.Font).toBe(
    'Roman'
  )
  expect(
    mockedDispatch.mock.calls[2][0].args.groupInputValues['Front text']
  ).toBe('Foobar')
  expect(
    mockedDispatch.mock.calls[3][0].args.groupInputValues['Glossy print']
  ).toBe(false)
})

function renderComponentRecursive() {
  return render(
    <ProductAssemblyOptions>
      <ProductAssemblyOptionItemName />
      <InputValue />
      <ProductAssemblyOptionItemCustomize>
        <ProductAssemblyOptionItemName />
        <InputValue />
      </ProductAssemblyOptionItemCustomize>
    </ProductAssemblyOptions>
  )
}

window.scroll = () => null
window.scrollTo = () => null

test('should keep input values for recursive assemblies', async () => {
  mockUseProduct.mockImplementation(() => ({
    product: productRecursive.data.product,
    selectedItem: productRecursive.data.product.items[0],
    selectedQuantity: 1,
  }))

  const { getByText, getByLabelText } = renderComponentRecursive()

  // Click on button Customize
  const assembly = getByText('Bells add-ons 1-3 lines')
  const customizeButton = assembly.parentElement?.querySelector(
    '.vtex-button'
  ) as HTMLElement

  fireEvent.click(customizeButton)

  // Click on Add 1-3-lines inside the modal
  const modal = within(
    document.querySelector('.vtex-modal__modal') as HTMLElement
  )

  const modalCustomizeButton = modal.getByText('Add 1-3-lines')

  fireEvent.click(modalCustomizeButton)

  // Type "Foobar" in the Input Value "Line 1"
  const input = modal.getByLabelText('Line 1')

  fireEvent.change(input, { target: { value: 'Foobar' } })

  expect(mockedDispatch.mock.calls).toHaveLength(7)
  // eslint-disable-next-line prefer-destructuring
  const [lastCall] = mockedDispatch.mock.calls[5]

  expect(lastCall.type).toBe('SET_ASSEMBLY_OPTIONS')
  expect(lastCall.args.groupInputValues['Line 1']).toBe('Foobar')

  // Close the modal
  fireEvent.click(
    document.querySelector('.vtex-modal__modal .vtex__icon-close') as Element
  )

  // Wait for the close transition to end
  await waitForElementToBeRemoved(() =>
    document.querySelector('.vtex-modal__modal .vtex__icon-close')
  )

  // Click on Customize button to open the modal again
  fireEvent.click(customizeButton)

  // Check if input still set with "Foobar" previously typed
  const inputLine1 = getByLabelText('Line 1') as HTMLInputElement

  expect(inputLine1.value).toBe('Foobar')
})

test('should select option when click in customize', () => {
  mockUseProduct.mockImplementation(() => ({
    product: productRecursive.data.product,
    selectedItem: productRecursive.data.product.items[0],
    selectedQuantity: 1,
  }))

  const { getByText } = render(
    <ProductAssemblyOptions>
      <ProductAssemblyOptionItemName />
      <InputValue />
      <ProductAssemblyOptionItemQuantity />
      <ProductAssemblyOptionItemCustomize>
        <ProductAssemblyOptionItemName />
        <InputValue />
      </ProductAssemblyOptionItemCustomize>
    </ProductAssemblyOptions>
  )

  const checkStyleguideCheckboxChecked = (container: HTMLElement) => {
    const radioButton = container.querySelector(
      '.vtex-checkbox__inner-container'
    ) as HTMLElement

    const classList = Array.from(radioButton.classList)

    return classList.includes('b--action-primary')
  }

  // Click on button Customize
  const assembly = getByText('Bells add-ons 1-3 lines')

  const isCheckedBefore = checkStyleguideCheckboxChecked(
    assembly.parentElement as HTMLElement
  )

  expect(isCheckedBefore).toBe(false)

  const customizeButton = assembly.parentElement?.querySelector(
    '.vtex-button'
  ) as HTMLElement

  fireEvent.click(customizeButton)

  const isCheckedAfter = checkStyleguideCheckboxChecked(
    assembly.parentElement as HTMLElement
  )

  expect(isCheckedAfter).toBe(true)
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/Product/ToggleChoiceAttachment.jsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```jsx
import React, { Component, Fragment } from 'react'

import ToggledChoice from '../Variation/Items/ToggledChoice'

const parseItem = ({ id, quantity, seller, name }) => ({
  [name]: { id, quantity: Number(!quantity), seller },
})

class ToggleChoiceAttachment extends Component {
  handleChange = (itemName) => {
    const { items, name, onAttachmentChange } = this.props

    onAttachmentChange(name, parseItem(items[itemName]), false)
  }

  render() {
    const { items } = this.props

    return (
      <Fragment>
        {Object.entries(items).map(([itemName, item]) => (
          <ToggledChoice
            imageUrl={item.imageUrl}
            name={item.name}
            selected={item.quantity === 1}
            disabled={item.minQuantity === 1}
            onChange={this.handleChange}
            key={itemName}
          />
        ))}
      </Fragment>
    )
  }
}

export default ToggleChoiceAttachment

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/Product/SingleChoiceAttachment.jsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```jsx
import React, { Fragment, Component } from 'react'

import SingleChoice from '../Variation/Items/SingleChoice'

const parseItem = ({ id, seller, name }) => ({
  [name]: { id, seller, quantity: 1 },
})

class SingleChoiceAttachment extends Component {
  handleOnChange = (itemName) => {
    const { name, onAttachmentChange, items } = this.props

    onAttachmentChange(name, parseItem(items[itemName]), true)
  }

  render() {
    const { items } = this.props

    return (
      <Fragment>
        {Object.entries(items).map(([itemName, item]) => (
          <SingleChoice
            price={item.price}
            imageUrl={item.imageUrl}
            name={item.name}
            selected={item.quantity === 1}
            onChange={this.handleOnChange}
            key={itemName}
            showPlus
          />
        ))}
      </Fragment>
    )
  }
}

export default SingleChoiceAttachment

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/Product/MultipleChoiceAttachment.jsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```jsx
import React, { Fragment, Component } from 'react'

import MultipleChoice from '../Variation/Items/MultipleChoice'

class MultipleChoiceAttachment extends Component {
  getUpdatedQuantities(updatedItem, quantity) {
    const quantities = Object.values(this.props.items).reduce(
      (acc, item) => ({
        ...acc,
        [item.name]: {
          id: item.id,
          quantity: item.quantity,
          seller: item.seller,
        },
      }),
      {}
    )

    quantities[updatedItem].quantity = quantity

    return quantities
  }

  canChangeTotalItems(delta) {
    const { minTotalItems, maxTotalItems, items } = this.props
    const totalItems = Object.values(items).reduce(
      (count, item) => count + item.quantity,
      0
    )

    const newTotalItems = totalItems + delta

    return minTotalItems <= newTotalItems && newTotalItems <= maxTotalItems
  }

  canChangeItemQuantity(itemName, newQuantity) {
    const { minQuantity, quantity, maxQuantity } = this.props.items[itemName]

    return (
      minQuantity <= newQuantity &&
      newQuantity <= maxQuantity &&
      this.canChangeTotalItems(newQuantity - quantity)
    )
  }

  handleChangeItemQuantity(itemName, quantity) {
    const { name, onAttachmentChange } = this.props

    // eslint-disable-next-line vtex/prefer-early-return
    if (this.canChangeItemQuantity(itemName, quantity)) {
      const quantities = this.getUpdatedQuantities(itemName, quantity)

      onAttachmentChange(name, quantities, false)
    }
  }

  handleOnChange = (value, itemName) =>
    this.handleChangeItemQuantity(itemName, value)

  getItemProps(itemName) {
    const { items } = this.props
    const { imageUrl, price, name, quantity } = items[itemName]

    return {
      imageUrl,
      price,
      name,
      chosenAmount: quantity,
      canIncrease: this.canChangeItemQuantity(itemName, quantity + 1),
      canDecrease: this.canChangeItemQuantity(itemName, quantity - 1),
      onChange: this.handleOnChange,
    }
  }

  render() {
    const { items } = this.props
    const itemsNames = Object.keys(items)

    return (
      <Fragment>
        {itemsNames.map((itemName) => (
          <MultipleChoice {...this.getItemProps(itemName)} key={itemName} />
        ))}
      </Fragment>
    )
  }
}

export default MultipleChoiceAttachment

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/Product/AttachmentPicker.jsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```jsx
import React, { Fragment } from 'react'
import { FormattedMessage } from 'react-intl'

import SingleChoiceAttachment from './SingleChoiceAttachment'
import MultipleChoiceAttachment from './MultipleChoiceAttachment'
import ToggleChoiceAttachment from './ToggleChoiceAttachment'

function getTitleId({ name }) {
  const key = name.toLowerCase().split(' ').join('-')

  return `store/product-customizer.${key}-title`
}

function AttachmentPicker({
  name,
  items,
  properties: { minTotalItems, maxTotalItems },
  onAttachmentChange,
  isToggleChoice,
  isSingleChoice,
}) {
  if (isSingleChoice) {
    return (
      <SingleChoiceAttachment
        name={name}
        items={items}
        onAttachmentChange={onAttachmentChange}
      />
    )
  }

  if (isToggleChoice) {
    return (
      <ToggleChoiceAttachment
        name={name}
        items={items}
        onAttachmentChange={onAttachmentChange}
      />
    )
  }

  return (
    <MultipleChoiceAttachment
      {...{ name, items, onAttachmentChange, minTotalItems, maxTotalItems }}
    />
  )
}

function AttachmentPickerWithTitle(props) {
  return (
    <Fragment>
      <div className="ph5 pv4 c-muted-2 t-small">
        <FormattedMessage id={getTitleId(props)} />
      </div>
      <AttachmentPicker {...props} />
    </Fragment>
  )
}

export default AttachmentPickerWithTitle

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/Product/ProductCustomizerWrapper.jsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```jsx
/* eslint-disable max-params */
import React, { Fragment, Component } from 'react'
import smoothscroll from 'smoothscroll-polyfill'
import { withToast } from 'vtex.styleguide'
import { injectIntl } from 'react-intl'
import { compose, pathOr } from 'ramda'
import { graphql } from 'react-apollo'

import SkuSelector from './SkuSelector'
import AttachmentsPicker from './AttachmentsPicker'
import MovingBottomButton from '../Buttons/MovingBottomButton'
import { scrollToElementTop } from '../../utils/scroll'
import ADD_TO_CART_MUTATION from '../../mutations/addToCart.gql'

class ProductCustomizerWrapper extends Component {
  state = {
    selectedSku: null,
    chosenAttachments: {},
    isAddingToCart: false,
  }

  attachmentView = React.createRef()

  componentDidMount() {
    smoothscroll.polyfill()
  }

  handleSkuChange = (item, shouldScroll) => {
    this.setState({
      selectedSku: item,
      chosenAttachments: this.getQuantitiesFromSkuAttachments(item),
    })
    item && shouldScroll && scrollToElementTop(this.attachmentView.current)
  }

  getQuantitiesFromSkuAttachments = (sku) =>
    sku
      ? Object.entries(this.props.product.items[sku].attachments).reduce(
          (quantities, [attName, attachment]) => ({
            ...quantities,
            [attName]: this.getQuantitiesFromItems(attachment.items),
          }),
          {}
        )
      : {}

  getQuantitiesFromItems = (items) =>
    Object.entries(items).reduce(
      (quantities, [itemName, item]) => ({
        ...quantities,
        [itemName]: {
          id: item.id,
          quantity: +item.initialQuantity,
          seller: item.seller,
        },
      }),
      {}
    )

  handleAttachmentChange = (
    attachmentName,
    quantities,
    isSingleChoice,
    callback
  ) => {
    this.setState(
      (state) => ({
        chosenAttachments: {
          ...state.chosenAttachments,
          [attachmentName]: {
            ...(state.chosenAttachments[attachmentName] && !isSingleChoice),
            ...quantities,
          },
        },
      }),
      () => callback && callback(this.state.chosenAttachments)
    )
  }

  getTotalPrice(attachments) {
    const { selectedSku } = this.state
    const baseValue = this.props.product.items[selectedSku].price

    return Object.values(attachments).reduce(
      (total, attachment) => total + this.getAttachmentPrice(attachment),
      baseValue * 100
    )
  }

  getAttachmentPrice(attachment) {
    return Object.values(attachment.items).reduce(
      (total, { price, quantity }) => total + price * quantity,
      0
    )
  }

  getAttachmentsWithQuantities(attachments, quantities) {
    return Object.entries(attachments).reduce(
      (obj, [name, attachment]) => ({
        ...obj,
        [name]: this.getAttachmentWithQuantities(attachment, quantities[name]),
      }),
      {}
    )
  }

  getAttachmentWithQuantities(attachment, quantities) {
    const [quantity, items] = Object.entries(attachment.items).reduce(
      ([total, obj], [name, item]) => {
        // eslint-disable-next-line no-shadow
        const quantity =
          name in quantities ? quantities[name].quantity : +item.initialQuantity

        return [total + quantity, { ...obj, [name]: { ...item, quantity } }]
      },
      [0, {}]
    )

    return { ...attachment, items, quantity }
  }

  isSkuReady(attachments) {
    return Object.values(attachments).every(this.isAttachmentReady)
  }

  isAttachmentReady(attachment) {
    const {
      quantity,
      properties: { minTotalItems, maxTotalItems },
    } = attachment

    return quantity >= minTotalItems && quantity <= maxTotalItems
  }

  getChoiceType = ({ isSingleChoice, isToggleChoice }) => {
    if (isSingleChoice) {
      return 'SINGLE'
    }

    if (isToggleChoice) {
      return 'TOGGLE'
    }

    return 'MULTIPLE'
  }

  getAssemblyOptions = () => {
    const { chosenAttachments, selectedSku } = this.state
    const { product } = this.props

    const options = []
    const added = []
    const removed = []

    Object.entries(chosenAttachments).forEach(([suffix, attachObj]) => {
      const attachmentTypeInfo = product.items[selectedSku].attachments[suffix]
      const { assemblyId } = attachmentTypeInfo

      Object.entries(attachObj).forEach(([name, { id, quantity, seller }]) => {
        const initialQuantity = pathOr(
          0,
          [
            'items',
            selectedSku,
            'attachments',
            suffix,
            'items',
            name,
            'initialQuantity',
          ],
          product
        )

        if (quantity > initialQuantity) {
          added.push({
            normalizedQuantity: quantity,
            extraQuantity: quantity - initialQuantity,
            choiceType: this.getChoiceType(attachmentTypeInfo),
            item: {
              name,
              sellingPrice:
                pathOr(
                  0,
                  [
                    'items',
                    selectedSku,
                    'attachments',
                    suffix,
                    'items',
                    name,
                    'price',
                  ],
                  product
                ) / 100,
              quantity,
              id,
            },
          })
        }

        if (quantity < initialQuantity) {
          removed.push({
            name,
            initialQuantity,
            removedQuantity: initialQuantity - quantity,
          })
        }

        if (quantity !== initialQuantity) {
          options.push({ assemblyId, id, quantity, seller })
        }
      })
    })

    return { options, removed, added }
  }

  showToast = (success) => {
    const suffix = success ? 'buy-success' : 'add-failure'
    const message = this.props.intl.formatMessage({
      id: `store/product-customizer.${suffix}`,
    })

    this.props.showToast({ message })
  }

  handleSubmitAddToCart = async () => {
    const { product, addToCart } = this.props
    const { selectedSku } = this.state

    this.setState({ isAddingToCart: true })

    const skuData = product.items[selectedSku]
    const {
      skuId,
      seller,
      commertialOffer,
      skuImageUrl,
      name,
      detailUrl,
    } = skuData

    const { options, added, removed } = this.getAssemblyOptions()
    const payload = {
      quantity: 1,
      options,
      id: skuId,
      sellingPrice: commertialOffer.Price,
      listPrice: commertialOffer.ListPrice,
      skuName: name,
      imageUrl: skuImageUrl,
      name,
      detailUrl,
      seller,
      assemblyOptions: {
        added,
        removed,
        parentPrice: commertialOffer.Price,
      },
    }

    try {
      const {
        data: { addToCart: linkStateItems },
      } = await addToCart([payload])

      const success =
        linkStateItems && !!linkStateItems.find(({ id }) => id === skuId)

      this.showToast(success)
    } catch (err) {
      this.showToast(false)
      // TODO send to splunk
    }

    this.setState({ isAddingToCart: false })
  }

  render() {
    const { imageText, imageUrl, items, productName } = this.props.product
    const { selectedSku, chosenAttachments, isAddingToCart } = this.state

    const attachments =
      selectedSku &&
      this.getAttachmentsWithQuantities(
        items[selectedSku].attachments,
        chosenAttachments
      )

    const ready = attachments && this.isSkuReady(attachments)
    const total = attachments && this.getTotalPrice(attachments)

    return (
      <Fragment>
        <div className="flex-ns pv6-ns justify-center-ns">
          <div className="w-100 w-third-ns flex tc items-center-ns h-100-ns ph2-ns">
            <img
              className="vtex-product-customizer__image"
              src={imageUrl}
              alt={imageText}
            />
          </div>
          <div className="w-40-ns ph2-ns">
            <div className="t-heading-4 c-on-base ph5 pt5-s">{productName}</div>
            <SkuSelector
              items={items}
              selectedSku={selectedSku}
              onSkuChange={this.handleSkuChange}
              productQuery={this.props.productQuery}
            />
            <div ref={this.attachmentView}>
              {selectedSku && (
                <AttachmentsPicker
                  attachments={attachments}
                  selectedSku={selectedSku}
                  onAttachmentChange={this.handleAttachmentChange}
                  chosenAttachments={chosenAttachments}
                />
              )}
            </div>
            <MovingBottomButton
              ready={ready}
              total={total}
              onSubmitAddToCart={this.handleSubmitAddToCart}
              isLoading={isAddingToCart}
            />
          </div>
        </div>
      </Fragment>
    )
  }
}

const withMutation = graphql(ADD_TO_CART_MUTATION, {
  props: ({ mutate }) => ({
    addToCart: (items) => mutate({ variables: { items } }),
  }),
})

export default compose(
  injectIntl,
  withToast,
  withMutation
)(ProductCustomizerWrapper)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-customizer/react/components/Product/AttachmentsPicker.jsx from the product-customizer VTEX app.

[Assistant]
Here's the implementation:

```jsx
import React, { Component, Fragment } from 'react'
import smoothscroll from 'smoothscroll-polyfill'

import AttachmentPicker from './AttachmentPicker'
import { scrollToElementTop } from '../../utils/scroll'

function precedence({ isSingleChoice, isToggleChoice }) {
  if (isSingleChoice) {
    return 0
  }

  if (isToggleChoice) {
    return 1
  }

  return 2
}

function compareAttachments([_, att1], [__, att2]) {
  return precedence(att1) - precedence(att2)
}

function isSingleChoicePicked(chosenAttachment) {
  return Object.values(chosenAttachment).some(({ quantity }) => quantity > 0)
}

function areAllSinglesPicked(attachments, chosenAttachments) {
  return Object.values(attachments)
    .filter(({ isSingleChoice }) => isSingleChoice)
    .every(({ name }) => isSingleChoicePicked(chosenAttachments[name]))
}

function shouldShowAttachment(attachment, canShowOtherAttachments) {
  if (attachment.isSingleChoice) return true

  return canShowOtherAttachments
}

function findFirstNonSingleChoice(attachments) {
  return Object.entries(attachments)
    .sort(compareAttachments)
    .find(([_, { isSingleChoice }]) => !isSingleChoice)
}

class AttachmentsPicker extends Component {
  pickers = Object.keys(this.props.attachments).reduce(
    (prev, name) => ({ ...prev, [name]: React.createRef() }),
    {}
  )

  state = {
    allSinglesPicked: areAllSinglesPicked(
      this.props.attachments,
      this.props.chosenAttachments
    ),
  }

  componentDidMount() {
    smoothscroll.polyfill()
  }

  componentDidUpdate(prevProps) {
    const { attachments, chosenAttachments } = this.props

    if (this.props.selectedSku !== prevProps.selectedSku) {
      this.setState({
        allSinglesPicked: areAllSinglesPicked(attachments, chosenAttachments),
      })
    }
  }

  attachmentChangeCallback = (newChosenAttachments) => {
    const { attachments } = this.props
    const currentAllSinglesPicked = this.state.allSinglesPicked
    const newSinglesPicked = areAllSinglesPicked(
      attachments,
      newChosenAttachments
    )

    // eslint-disable-next-line vtex/prefer-early-return
    if (currentAllSinglesPicked !== newSinglesPicked) {
      this.setState({ allSinglesPicked: newSinglesPicked })
      const [firstNotSingleName] = findFirstNonSingleChoice(attachments) || []
      const picker = firstNotSingleName && this.pickers[firstNotSingleName]

      picker && scrollToElementTop(picker.current)
    }
  }

  handleOnAttachmentChange = (attachmentName, quantities, isSingleChoice) => {
    this.props.onAttachmentChange(
      attachmentName,
      quantities,
      isSingleChoice,
      this.attachmentChangeCallback
    )
  }

  render() {
    const { attachments } = this.props
    const { allSinglesPicked } = this.state

    return (
      <Fragment>
        {Object.entries(attachments)
          .sort(compareAttachments)
          .map(([name, attachment]) => (
            <div
              ref={this.pickers[name]}
              key={name}
              className={`${
                shouldShowAttachment(attachment, allSinglesPicked) ? '' : 'dn'
              }`}
            >
              <AttachmentPicker
                key={name}
                onAttachmentChange={this.handleOnAttachmentChange}
                {...attachment}
              />
            </div>
          ))}
      </Fragment>
    )
  }
}

export default AttachmentsPicker

```