[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 speedy-gtm/react/__tests__/index.test.tsx from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```tsx
import productImpressionData from '../__mocks__/productImpression'
import productDetails from '../__mocks__/productDetail'
import productClick from '../__mocks__/productClick'
import { buildViewPromotionData } from '../__mocks__/viewPromotion'
import { handleEvents } from '../index'
import updateEcommerce from '../modules/updateEcommerce'
import {
  Promotion,
  PromotionClickData,
  AddToCartData,
  RemoveFromCartData,
  CartItem,
} from '../typings/events'
import { creditCardPaymentInfoMock } from '../__mocks__/addPaymentInfo'
import shouldSendGA4Events from '../modules/utils/shouldSendGA4Events'
import { beginCheckoutMock } from '../__mocks__/beginCheckout'
import { shippingInfoMock } from '../__mocks__/addShippingInfo'
import {
  viewCartWithItemsMock,
  viewCartWithNoItemsMock,
} from '../__mocks__/viewCart'
import { transaction, refundTransaction } from '../__mocks__/transaction'
import productWishlist from '../__mocks__/addToWishlist'

jest.mock('../modules/utils/shouldSendGA4Events')

jest.mock('../modules/updateEcommerce', () => jest.fn())

const mockedUpdate = updateEcommerce as jest.Mock

beforeEach(() => {
  mockedUpdate.mockReset()
})

test('productImpression', () => {
  const message = new MessageEvent('message', {
    data: productImpressionData,
  })

  handleEvents(message)

  expect(mockedUpdate).toHaveBeenCalledWith('productImpression', {
    event: 'productImpression',
    ecommerce: {
      currencyCode: 'USD',
      impressions: [
        {
          brand: 'Mizuno',
          category: 'Apparel & Accessories/Shoes',
          id: '16',
          variant: '35',
          list: 'Shelf',
          name: 'Classic Shoes Top',
          position: 1,
          price: '38.9',
          dimension1: '12531',
          dimension2: '232344',
          dimension3: 'Classic Pink',
        },
        {
          brand: 'Nintendo',
          category: 'Apparel & Accessories/Watches',
          id: '15',
          variant: '32',
          list: 'Shelf',
          name: 'Gorgeous Top Watch',
          position: 2,
          price: '2200',
          dimension1: '',
          dimension2: '',
          dimension3: 'Grey',
        },
      ],
    },
  })
})

test('productDetail', () => {
  const message = new MessageEvent('message', {
    data: productDetails,
  })

  handleEvents(message)

  expect(updateEcommerce).toHaveBeenCalledWith('productDetail', {
    event: 'productDetail',
    ecommerce: {
      detail: {
        actionField: {
          list: 'List of products',
        },
        products: [
          {
            brand: 'Mizuno',
            category: 'Apparel & Accessories/Shoes',
            id: '16',
            variant: '35',
            name: 'Classic Shoes Top',
            price: 1540.99,
            dimension1: '123',
            dimension2: '12531',
            dimension3: 'Classic Pink',
            dimension4: 'available',
          },
        ],
      },
    },
  })
})

test('productClick', () => {
  const message = new MessageEvent('message', {
    data: productClick,
  })

  handleEvents(message)

  expect(mockedUpdate).toHaveBeenCalledWith('productClick', {
    event: 'productClick',
    ecommerce: {
      click: {
        actionField: {
          list: 'List of products',
        },
        products: [
          {
            brand: 'Mizuno',
            category: 'Apparel & Accessories/Shoes',
            id: '16',
            variant: '35',
            name: 'Classic Shoes Top',
            position: 3,
            price: 38.9,
            dimension1: '12531',
            dimension2: '',
            dimension3: 'Classic Pink',
          },
        ],
      },
    },
  })
})

describe('GA4 events', () => {
  const sendGA4Events = true
  const mockedShouldSendGA4Events = shouldSendGA4Events as jest.Mock

  beforeEach(() => {
    mockedShouldSendGA4Events.mockReset()
    mockedShouldSendGA4Events.mockReturnValue(sendGA4Events)
  })

  describe('view_item_list', () => {
    it('sends an event that signifies that some content was shown to the user', () => {
      const message = new MessageEvent('message', {
        data: productImpressionData,
      })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('view_item_list', {
        ecommerce: {
          item_list_name: 'Shelf',
          items: [
            {
              discount: 0,
              index: 1,
              item_brand: 'Mizuno',
              item_category: 'Apparel & Accessories',
              item_category2: 'Shoes',
              item_id: '16',
              item_name: 'Classic Shoes Top',
              item_variant: '35',
              price: 38.9,
              quantity: 1,
              dimension1: '12531',
              dimension2: '232344',
              dimension3: 'Classic Pink',
              dimension4: 'available',
            },
            {
              discount: 0,
              index: 2,
              item_brand: 'Nintendo',
              item_category: 'Apparel & Accessories',
              item_category2: 'Watches',
              item_id: '15',
              item_name: 'Gorgeous Top Watch',
              item_variant: '32',
              price: 2200,
              quantity: 0,
              dimension1: '',
              dimension2: '',
              dimension3: 'Grey',
              dimension4: 'unavailable',
            },
          ],
        },
      })
    })
  })

  describe('view_item', () => {
    it('sends an event that signifies that some content was shown to the user', () => {
      const message = new MessageEvent('message', { data: productDetails })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('view_item', {
        ecommerce: {
          currency: 'USD',
          value: 1540.99,
          items: [
            {
              item_id: '16',
              item_name: 'Classic Shoes Top',
              item_list_name: 'List of products',
              item_brand: 'Mizuno',
              item_variant: '35',
              price: 1540.99,
              quantity: 1,
              discount: 0,
              item_category: 'Apparel & Accessories',
              item_category2: 'Shoes',
              dimension1: '123',
              dimension2: '12531',
              dimension3: 'Classic Pink',
              dimension4: 'available',
            },
          ],
        },
      })
    })
  })

  describe('select_item', () => {
    it('sends an event that signifies an item was selected from a list', () => {
      const message = new MessageEvent('message', { data: productClick })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('select_item', {
        ecommerce: {
          item_list_name: 'List of products',
          items: [
            {
              item_id: '16',
              item_name: 'Classic Shoes Top',
              item_list_name: 'List of products',
              item_brand: 'Mizuno',
              item_variant: '35',
              index: 3,
              price: 38.9,
              quantity: 1,
              discount: 0,
              item_category: 'Apparel & Accessories',
              item_category2: 'Shoes',
              dimension1: '12531',
              dimension2: '',
              dimension3: 'Classic Pink',
              dimension4: 'available',
            },
          ],
        },
      })
    })
  })

  describe('select_promotion', () => {
    it('sends an event that signifies a promotion was selected from a list', () => {
      const promotion: Promotion = {
        id: 'P_12345',
        name: 'Summer Sale',
        creative: 'Summer Banner',
        position: 'featured_app_1',
      }

      const data: PromotionClickData = {
        currency: 'USD',
        event: 'promotionClick',
        eventName: 'vtex:promotionClick',
        eventType: 'vtex:promotionClick',
        promotions: [promotion],
      }

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('select_promotion', {
        ecommerce: {
          creative_name: 'Summer Banner',
          creative_slot: 'featured_app_1',
          promotion_id: 'P_12345',
          promotion_name: 'Summer Sale',
        },
      })
    })
  })

  describe('add_to_cart', () => {
    it('sends an event that signifies an item being added to the cart', () => {
      type CartItemMockType = Pick<
        CartItem,
        | 'name'
        | 'brand'
        | 'price'
        | 'skuId'
        | 'skuName'
        | 'quantity'
        | 'category'
        | 'productId'
        | 'productRefId'
        | 'referenceId'
        | 'variant'
      >

      const cartItem1: CartItemMockType = {
        productId: '200000202',
        skuId: '2000304',
        brand: 'Sony',
        name: 'Top Wood',
        skuName: 'top_wood_200',
        price: 197.99,
        category: 'Home & Decor',
        quantity: 1,
        productRefId: '123',
        referenceId: '456',
        variant: 'Red',
      }

      const cartItem2: CartItemMockType = {
        productId: '200000203',
        skuId: '2000305',
        brand: 'Sony',
        name: 'Top Wood 2',
        skuName: 'top_wood_300',
        price: 150.9,
        category: 'Home & Decor/Tables',
        quantity: 2,
        productRefId: '789',
        referenceId: '101',
        variant: 'Blue',
      }

      const data: AddToCartData = {
        currency: 'USD',
        event: 'addToCart',
        eventName: 'vtex:addToCart',
        items: [cartItem1 as CartItem, cartItem2 as CartItem],
      }

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('add_to_cart', {
        ecommerce: {
          currency: 'USD',
          value: 499.79,
          items: [
            {
              item_id: '200000202',
              item_brand: 'Sony',
              item_name: 'Top Wood',
              item_variant: '2000304',
              item_category: 'Home & Decor',
              quantity: 1,
              price: 197.99,
              dimension1: '123',
              dimension2: '456',
              dimension3: 'Red',
              dimension4: 'available',
            },
            {
              item_id: '200000203',
              item_brand: 'Sony',
              item_name: 'Top Wood 2',
              item_variant: '2000305',
              item_category: 'Home & Decor',
              item_category2: 'Tables',
              quantity: 2,
              price: 150.9,
              dimension1: '789',
              dimension2: '101',
              dimension3: 'Blue',
              dimension4: 'available',
            },
          ],
        },
      })
    })
  })

  describe('remove_from_cart', () => {
    it('sends an event that signifies an item being removed from cart', () => {
      type CartItemMockType = Pick<
        CartItem,
        | 'name'
        | 'brand'
        | 'price'
        | 'skuId'
        | 'skuName'
        | 'quantity'
        | 'category'
        | 'productId'
        | 'productRefId'
        | 'referenceId'
        | 'variant'
      >

      const cartItem: CartItemMockType = {
        productId: '200000202',
        skuId: '2000304',
        brand: 'Sony',
        name: 'Top Wood',
        skuName: 'top_wood_200',
        price: 197.99,
        category: 'Home & Decor',
        quantity: 3,
        productRefId: '123',
        referenceId: '456',
        variant: 'Red',
      }

      const data: RemoveFromCartData = {
        currency: 'USD',
        event: 'removeFromCart',
        eventName: 'vtex:removeFromCart',
        items: [cartItem as CartItem],
      }

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('remove_from_cart', {
        ecommerce: {
          currency: 'USD',
          value: 593.97,
          items: [
            {
              item_id: '200000202',
              item_brand: 'Sony',
              item_name: 'Top Wood',
              item_variant: '2000304',
              item_category: 'Home & Decor',
              quantity: 3,
              price: 197.99,
              dimension1: '123',
              dimension2: '456',
              dimension3: 'Red',
              dimension4: 'available',
            },
          ],
        },
      })
    })
  })

  describe('purchase', () => {
    it('sends an event that signifies a successful checkout on orderPlaced page', () => {
      const data = transaction

      data.event = 'orderPlaced'
      data.eventName = 'vtex:orderPlaced'

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('purchase', {
        ecommerce: {
          coupon: null,
          currency: 'USD',
          items: [
            {
              item_brand: 'New Offers!!',
              item_category: 'Apparel & Accessories',
              item_id: '9',
              item_name: 'Top Everyday Necessaire',
              item_variant: '20',
              price: 1600.99,
              quantity: 2,
              dimension1: '',
              dimension2: '9812983',
              dimension3: '100 RMS',
              dimension4: 'available',
            },
          ],
          shipping: 1942.61,
          tax: 0,
          transaction_id: '1310750551387',
          value: 5144.59,
        },
      })
    })
  })

  describe('view_promotion', () => {
    it('sends an event that signifies a promotion was viewed from a list', () => {
      const data = buildViewPromotionData({
        id: 'P_12345',
        name: 'Summer Sale',
        creative: 'Summer Banner',
        position: 'featured_app_1',
        products: [
          {
            productId: 'SKU_20',
            productName: 'Tea Leaves',
          },
          {
            productId: 'SKU_30',
            productName: 'Coffee Beans',
          },
        ],
      })

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('view_promotion', {
        ecommerce: {
          creative_name: 'Summer Banner',
          creative_slot: 'featured_app_1',
          promotion_id: 'P_12345',
          promotion_name: 'Summer Sale',
          items: [
            {
              item_id: 'SKU_20',
              item_name: 'Tea Leaves',
            },
            {
              item_id: 'SKU_30',
              item_name: 'Coffee Beans',
            },
          ],
        },
      })
    })
  })

  describe('add_payment_info', () => {
    it('sends an event when a user add a payment information (credit card)', () => {
      const data = creditCardPaymentInfoMock

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('add_payment_info', {
        ecommerce: {
          currency: 'USD',
          value: 211.19,
          payment_type: 'creditCard',
          items: [
            {
              item_id: '200000202',
              item_brand: 'Sony',
              item_name: 'Top Wood',
              item_variant: '2000304',
              item_category: 'Home & Decor',
              quantity: 1,
              price: 197.99,
              dimension1: '123',
              dimension2: '456',
              dimension3: 'Red',
              dimension4: 'available',
            },
          ],
        },
      })
    })
  })

  describe('begin_checkout', () => {
    it('sends an event when a user access the checkout page', () => {
      const data = beginCheckoutMock

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('begin_checkout', {
        ecommerce: {
          currency: 'USD',
          value: 650.69,
          items: [
            {
              item_id: '200000202',
              item_brand: 'Sony',
              item_name: 'Top Wood',
              item_variant: '2000304',
              item_category: 'Home & Decor',
              quantity: 1,
              price: 197.99,
              dimension1: '123',
              dimension2: '456',
              dimension3: 'Red',
              dimension4: 'available',
            },
            {
              item_id: '200000203',
              item_brand: 'Sony',
              item_name: 'Top Wood 2',
              item_variant: '2000305',
              item_category: 'Home & Decor',
              item_category2: 'Tables',
              quantity: 3,
              price: 150.9,
              dimension1: '789',
              dimension2: '101',
              dimension3: 'Blue',
              dimension4: 'available',
            },
          ],
        },
      })
    })
  })

  describe('view_cart', () => {
    it('sends an event when a user opens the cart with items', () => {
      const data = viewCartWithItemsMock

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('view_cart', {
        ecommerce: {
          currency: 'USD',
          value: 546.88,
          items: [
            {
              item_id: '200000202',
              item_brand: 'Sony',
              item_name: 'Top Wood',
              item_variant: '2000304',
              item_category: 'Home & Decor',
              quantity: 2,
              price: 197.99,
              dimension1: '123',
              dimension2: '456',
              dimension3: 'Red',
              dimension4: 'available',
            },
            {
              item_id: '200000203',
              item_brand: 'Sony',
              item_name: 'Top Wood 2',
              item_variant: '2000305',
              item_category: 'Home & Decor',
              item_category2: 'Tables',
              quantity: 1,
              price: 150.9,
              dimension1: '789',
              dimension2: '101',
              dimension3: 'Blue',
              dimension4: 'available',
            },
          ],
        },
      })
    })

    it('sends an event when a user opens the cart with no items', () => {
      const data = viewCartWithNoItemsMock

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('view_cart', {
        ecommerce: {
          currency: 'USD',
          value: 0.0,
          items: [],
        },
      })
    })
  })

  describe('refund', () => {
    it('sends an event when the user refunds an order', () => {
      const data = refundTransaction

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('refund', {
        ecommerce: {
          currency: 'USD',
          items: [
            {
              item_brand: 'New Offers!!',
              item_category: 'Apparel & Accessories',
              item_id: '9',
              item_name: 'Top Everyday Necessaire',
              item_variant: '20',
              price: 1600.99,
              quantity: 2,
              dimension1: '',
              dimension2: '9812983',
              dimension3: '100 RMS',
              dimension4: 'available',
            },
          ],
          shipping: 1942.61,
          tax: 0,
          transaction_id: '1310750551387',
          value: 3543.6,
        },
      })
    })
  })

  describe('add_shipping_info', () => {
    it('sends an event when a user add a shipping info', () => {
      const data = shippingInfoMock

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('add_shipping_info', {
        ecommerce: {
          currency: 'USD',
          value: 1958.35,
          shipping_tier: 'Ground',
          items: [
            {
              item_id: '200000202',
              item_brand: 'Sony',
              item_name: 'Top Wood',
              item_variant: '2000304',
              item_category: 'Home & Decor',
              quantity: 1,
              price: 197.99,
              dimension1: '123',
              dimension2: '456',
              dimension3: 'Red',
              dimension4: 'available',
            },
          ],
        },
      })
    })
  })

  describe('add_to_wishlist', () => {
    it('sends an event when the user add a product to wishlist', () => {
      const message = new MessageEvent('message', { data: productWishlist })

      message.data.eventName = 'vtex:addToWishlist'

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('add_to_wishlist', {
        ecommerce: {
          currency: 'USD',
          value: 1540.99,
          items: [
            {
              item_id: '16',
              item_name: 'Classic Shoes Top',
              item_list_name: 'List of products',
              item_brand: 'Mizuno',
              item_variant: '35',
              price: 1540.99,
              quantity: 1,
              discount: 0,
              item_category: 'Apparel & Accessories',
              item_category2: 'Shoes',
              dimension1: '123',
              dimension2: '12531',
              dimension3: 'Classic Pink',
              dimension4: 'available',
            },
          ],
        },
      })
    })
  })
  describe('login', () => {
    it('sends an event when a user login on store with any method', () => {
      const data = {
        event: 'login',
        eventName: 'vtex:login',
        method: 'Google',
      }

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('login', {
        ecommerce: {
          method: 'Google',
        },
      })
    })
  })
  describe('search', () => {
    it('sends an event when a user search for a term on store', () => {
      const data = {
        event: 'search',
        eventName: 'vtex:search',
        term: 'Top Wood',
      }

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('search', {
        ecommerce: {
          search_term: 'Top Wood',
        },
      })
    })
  })

  describe('sign_up', () => {
    it('sends an event when a user signup on store with any method', () => {
      const data = {
        event: 'signUp',
        eventName: 'vtex:signUp',
        method: 'Google',
      }

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('sign_up', {
        ecommerce: {
          method: 'Google',
        },
      })
    })
  })

  describe('share', () => {
    it('sends an event when a user share a product via any type', () => {
      const data = {
        event: 'share',
        eventName: 'vtex:share',
        method: 'Facebook',
        contentType: 'image',
        itemId: '10',
      }

      const message = new MessageEvent('message', { data })

      handleEvents(message)

      expect(mockedUpdate).toHaveBeenCalledWith('share', {
        ecommerce: {
          method: 'Facebook',
          content_type: 'image',
          item_id: '10',
        },
      })
    })
  })
})

```


[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 speedy-gtm/react/modules/push.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { createOrGetAnalyticsData } from './analytics'

window.dataLayer = window.dataLayer || []

let isFirstPush = true

export default function push(rawEvent: Record<string, unknown>) {
  const {
    location: originalLocation,
    referrer: originalReferrer,
    origin,
  } = createOrGetAnalyticsData()

  let event = rawEvent

  if (isFirstPush || origin === 'fresh') {
    isFirstPush = false

    event = {
      ...rawEvent,
      originalLocation,
      originalReferrer,
    }
  }

  window.dataLayer.push(event)
}

```


[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 speedy-gtm/react/modules/updateEcommerce.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
import push from './push'

export default function updateEcommerce(
  eventName: string,
  data: Record<string, unknown>
) {
  push({ ecommerce: null, ecommerceV2: null })

  data.event ? push(data) : push({ event: eventName, ...data })
}

```


[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 speedy-gtm/react/modules/enhancedEcommerceEvents.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
import updateEcommerce from './updateEcommerce'
import {
  PixelMessage,
  ProductOrder,
  Impression,
  CartItem,
} from '../typings/events'
import { AnalyticsEcommerceProduct } from '../typings/gtm'
import {
  purchase,
  selectItem,
  selectPromotion,
  viewItem,
  viewItemList,
  viewPromotion,
  addToCart,
  removeFromCart,
  addPaymentInfo,
  beginCheckout,
  addShippingInfo,
  viewCart,
  search,
  login,
  refund,
  addToWishlist,
  signUp,
  share,
} from './gaEvents'
import {
  getCategory,
  getSeller,
  getProductNameWithoutVariant,
  getPurchaseObjectData,
} from './utils'
import {
  customDimensionSkuAvailability,
  productViewSkuReference,
} from './customDimensions'

export async function sendEnhancedEcommerceEvents(e: PixelMessage) {
  switch (e.data.eventName) {
    case 'vtex:productView': {
      const {
        productId,
        productReference,
        selectedSku,
        productName,
        brand,
        categories,
      } = e.data.product

      const productAvailableQuantity = getSeller(selectedSku.sellers)
        .commertialOffer.AvailableQuantity

      const isAvailable = customDimensionSkuAvailability(
        productAvailableQuantity
      )

      // Product summary list title. Ex: 'List of products'
      const list = e.data.list ? { actionField: { list: e.data.list } } : {}

      let price

      try {
        price = getSeller(selectedSku.sellers).commertialOffer.Price
      } catch {
        price = undefined
      }

      const data = {
        ecommerce: {
          detail: {
            ...list,
            products: [
              {
                brand,
                category: getCategory(categories),
                id: productId,
                variant: selectedSku.itemId,
                name: productName,
                dimension1: productReference ?? '',
                dimension2: productViewSkuReference(e.data.product) ?? '',
                dimension3: selectedSku.name,
                dimension4: isAvailable,
                price,
              },
            ],
          },
        },
        event: 'productDetail',
      }

      viewItem(e.data)
      updateEcommerce('productDetail', data)

      return
    }

    case 'vtex:productClick': {
      const { product, position } = e.data
      const {
        productName,
        brand,
        categories,
        sku,
        productId,
        productReference,
      } = product

      // Product summary list title. Ex: 'List of products'
      const list = e.data.list ? { actionField: { list: e.data.list } } : {}

      let price

      try {
        price = getSeller(sku?.sellers ?? product.items[0].sellers)
          .commertialOffer.Price
      } catch {
        price = undefined
      }

      const data = {
        event: 'productClick',
        ecommerce: {
          click: {
            ...list,
            products: [
              {
                brand,
                category: getCategory(categories),
                id: productId,
                variant: sku.itemId,
                name: productName,
                dimension1: productReference ?? '',
                dimension2: sku.referenceId?.Value ?? '',
                dimension3: sku.name,
                price,
                position,
              },
            ],
          },
        },
      }

      selectItem(e.data)
      updateEcommerce('productClick', data)

      return
    }

    case 'vtex:addToCart': {
      const { items } = e.data

      const data = {
        ecommerce: {
          add: {
            products: items.map(item => ({
              brand: item.brand,
              category: item.category,
              id: item.productId,
              variant: item.skuId,
              name: item.name, // Product name
              price:
                item.priceIsInt === true
                  ? `${item.price / 100}`
                  : `${item.price}`,
              quantity: item.quantity,
              dimension1: item.productRefId ?? '',
              dimension2: item.referenceId ?? '', // SKU reference id
              dimension3: item.variant, // SKU name (variant)
            })),
          },
          currencyCode: e.data.currency,
        },
        event: 'addToCart',
      }

      addToCart(e.data)
      updateEcommerce('addToCart', data)

      return
    }

    case 'vtex:removeFromCart': {
      const { items } = e.data

      const data = {
        ecommerce: {
          currencyCode: e.data.currency,
          remove: {
            products: items.map(item => ({
              brand: item.brand,
              category: item.category,
              id: item.productId,
              variant: item.skuId,
              name: item.name, // Product name
              price:
                item.priceIsInt === true
                  ? `${item.price / 100}`
                  : `${item.price}`,
              quantity: item.quantity,
              dimension1: item.productRefId ?? '',
              dimension2: item.referenceId ?? '', // SKU reference id
              dimension3: item.variant, // SKU name (variant)
            })),
          },
        },
        event: 'removeFromCart',
      }

      removeFromCart(e.data)
      updateEcommerce('removeFromCart', data)

      return
    }

    case 'vtex:orderPlaced': {
      const order = e.data

      const ecommerce = {
        purchase: {
          actionField: getPurchaseObjectData(order),
          products: order.transactionProducts.map((product: ProductOrder) =>
            getProductObjectData(product)
          ),
        },
      }

      const data = {
        // @ts-ignore
        event: 'orderPlaced',
        ...order,
        // The name ecommerceV2 was introduced as a fix, so it is possible that some clients
        // were using this as it was called before (ecommerce). For that reason,
        // it will also be sent as ecommerce to the dataLayer.
        ecommerce,
        // This variable is called ecommerceV2 so it matches the variable name present on the checkout
        // This way, users can have one single tag for checkout and orderPlaced events
        ecommerceV2: {
          ecommerce,
        },
      }

      purchase(order)
      updateEcommerce('orderPlaced', data)

      return
    }

    case 'vtex:productImpression': {
      const { currency, list, impressions } = e.data

      const parsedImpressions = (impressions || []).map(
        getProductImpressionObjectData(list)
      )

      const data = {
        event: 'productImpression',
        ecommerce: {
          currencyCode: currency,
          impressions: parsedImpressions,
        },
      }

      viewItemList(e.data)
      updateEcommerce('productImpression', data)

      return
    }

    case 'vtex:cartLoaded': {
      const { orderForm } = e.data

      const data = {
        event: 'checkout',
        ecommerce: {
          checkout: {
            actionField: {
              step: 1,
            },
            products: orderForm.items.map(getCheckoutProductObjectData),
          },
        },
      }

      updateEcommerce('checkout', data)

      break
    }

    case 'vtex:promoView': {
      const { promotions } = e.data

      const data = {
        event: 'promoView',
        ecommerce: {
          promoView: {
            promotions,
          },
        },
      }

      viewPromotion(e.data)
      updateEcommerce('promoView', data)

      break
    }

    case 'vtex:promotionClick': {
      const { promotions } = e.data

      const data = {
        event: 'promotionClick',
        ecommerce: {
          promoClick: {
            promotions,
          },
        },
      }

      selectPromotion(e.data)
      updateEcommerce('promotionClick', data)

      break
    }

    case 'vtex:addPaymentInfo': {
      addPaymentInfo(e.data)

      break
    }

    case 'vtex:beginCheckout': {
      beginCheckout(e.data)

      break
    }

    case 'vtex:addShippingInfo': {
      addShippingInfo(e.data)

      break
    }

    case 'vtex:viewCart': {
      viewCart(e.data)

      break
    }

    case 'vtex:login': {
      login(e.data)

      break
    }

    case 'vtex:signUp': {
      signUp(e.data)

      break
    }

    case 'vtex:refund': {
      refund(e.data)

      break
    }

    case 'vtex:addToWishlist': {
      addToWishlist(e.data)

      break
    }

    case 'vtex:search': {
      search(e.data)

      break
    }

    case 'vtex:share': {
      share(e.data)

      break
    }

    default: {
      break
    }
  }
}

function getProductObjectData(product: ProductOrder) {
  const productName = getProductNameWithoutVariant(
    product.name,
    product.skuName
  )

  return {
    brand: product.brand,
    category: product.categoryTree?.join('/'),
    id: product.id, // Product id
    variant: product.sku, // SKU id
    name: productName, // Product name
    price: product.price,
    quantity: product.quantity,
    dimension1: product.productRefId ?? '',
    dimension2: product.skuRefId ?? '',
    dimension3: product.skuName, // SKU name (only variant)
  }
}

function getProductImpressionObjectData(list: string) {
  return ({ product, position }: Impression) => ({
    brand: product.brand,
    category: getCategory(product.categories),
    id: product.productId, // Product id
    variant: product.sku.itemId, // SKU id
    list,
    name: product.productName,
    position,
    price: `${product.sku.seller.commertialOffer.Price}`,
    dimension1: product.productReference ?? '',
    dimension2: product.sku.referenceId?.Value ?? '',
    dimension3: product.sku.name, // SKU name (variation only)
  })
}

function getCheckoutProductObjectData(
  item: CartItem
): AnalyticsEcommerceProduct {
  const productName = getProductNameWithoutVariant(item.name, item.skuName)

  return {
    id: item.productId, // Product id
    variant: item.id, // SKU id
    name: productName, // Product name without variant
    category: Object.keys(item.productCategories ?? {}).reduce(
      (categories, category) =>
        categories ? `${categories}/${category}` : category,
      ''
    ),
    brand: item.additionalInfo?.brandName ?? '',
    price: item.sellingPrice / 100,
    quantity: item.quantity,
    dimension1: item.productRefId ?? '',
    dimension2: item.referenceId ?? '', // SKU reference id
    dimension3: item.skuName,
  }
}

```


[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 speedy-gtm/react/modules/legacyEvents.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
import push from './push'
import { PixelMessage, SearchPageInfoData } from '../typings/events'

export async function sendLegacyEvents(e: PixelMessage) {
  switch (e.data.eventName) {
    case 'vtex:pageInfo': {
      const { eventType } = e.data

      switch (eventType) {
        case 'homeView': {
          push({
            event: 'homeView',
          })
          break
        }

        case 'categoryView': {
          const data = e.data as SearchPageInfoData

          push({
            event: 'categoryView',
            departmentId: data.department?.id,
            departmentName: data.department?.name,
            categoryId: data.category?.id,
            categoryName: data.category?.name,
          })
          break
        }

        case 'departmentView': {
          const data = e.data as SearchPageInfoData

          push({
            event: 'departmentView',
            departmentId: data.department?.id,
            departmentName: data.department?.name,
          })
          break
        }

        case 'emptySearchView':

        // eslint-disable-next-line no-fallthrough
        case 'internalSiteSearchView': {
          const data = e.data as SearchPageInfoData

          push({
            event: 'internalSiteSearchView',
            siteSearchTerm: data.search?.term,
            siteSearchForm: window.location.href,
            siteSearchCategory: data.search?.category?.id,
            siteSearchResults: data.search?.results,
          })

          break
        }

        case 'productView': {
          push({
            event: 'productView',
          })
          break
        }

        default: {
          push({
            event: 'otherView',
          })
          break
        }
      }

      break
    }

    default: {
      break
    }
  }
}

```


[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 speedy-gtm/react/modules/utils.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  CartItem,
  Impression,
  Order,
  ProductOrder,
  Seller,
} from '../typings/events'
import { customDimensions } from './customDimensions'

export function getSeller(sellers: Seller[]) {
  const defaultSeller = sellers.find(seller => seller.sellerDefault)

  if (!defaultSeller) {
    return sellers[0]
  }

  return defaultSeller
}

export function getPrice(seller: Seller) {
  let price

  try {
    price = seller.commertialOffer.Price
  } catch {
    price = undefined
  }

  return price
}

function formatCategoriesHierarchy(
  categories: { [key: string]: string },
  value: string,
  index: number
) {
  const categoryHierarchyNumber = index + 1
  const isFirstCategory = categoryHierarchyNumber === 1
  const key = `item_category${isFirstCategory ? '' : categoryHierarchyNumber}`

  categories[key] = value
}

export function getCategoriesWithHierarchy(categoriesArray: string[]) {
  if (!categoriesArray || !categoriesArray.length) return

  const categoryString = getCategory(categoriesArray)
  const categories = splitIntoCategories(categoryString)

  if (!categories || !categoryString) return {}

  const categoriesFormatted: { [key: string]: string } = {}

  if (!categories || !categories.length) {
    formatCategoriesHierarchy(categoriesFormatted, categoryString, 0)
  } else {
    categories.forEach((category, index) => {
      formatCategoriesHierarchy(categoriesFormatted, category, index)
    })
  }

  return categoriesFormatted
}

function getCategoriesHierarchyByKey(
  categories: Record<string, string> | null,
  keysArray?: string[]
) {
  if (!keysArray || !keysArray.length || !categories) return []
  const categoriesFormatted: string[] = []
  const categoriesHierarchyFormatted = {}

  keysArray.forEach(key => {
    if (key) categoriesFormatted.push(categories[key])
  })

  categoriesFormatted.forEach((category, index) => {
    formatCategoriesHierarchy(categoriesHierarchyFormatted, category, index)
  })

  return categoriesHierarchyFormatted
}

export function getQuantity(seller: Seller) {
  const isAvailable = seller.commertialOffer.AvailableQuantity > 0

  return isAvailable ? 1 : 0
}

export function getImpressions(impressions: Impression[]) {
  if (!impressions || !impressions.length) return []

  const formattedImpressions = impressions.map(impression => {
    const { product, position } = impression
    const {
      productName,
      productId,
      productReference,
      sku,
      brand,
      categories,
    } = product

    const { itemId, seller, referenceId, name } = sku

    const price = getPrice(seller)
    const discount = getDiscount(seller)
    const quantity = getQuantity(seller)

    const categoriesHierarchy = getCategoriesWithHierarchy(categories)

    return {
      item_id: productId,
      item_name: productName,
      item_variant: itemId,
      item_brand: brand,
      index: position,
      discount,
      price,
      quantity,
      ...categoriesHierarchy,
      ...customDimensions({
        productReference,
        skuReference: referenceId?.Value,
        skuName: name,
        quantity,
      }),
    }
  })

  return formattedImpressions
}

export function getDiscount(seller: Seller) {
  if (!seller.commertialOffer.PriceWithoutDiscount) return 0

  const { commertialOffer } = seller
  const { Price, PriceWithoutDiscount } = commertialOffer

  if (PriceWithoutDiscount <= Price) return 0

  let price

  try {
    price = PriceWithoutDiscount - Price
  } catch {
    price = 0
  }

  return price
}

export function getCategory(rawCategories: string[]) {
  if (!rawCategories || !rawCategories.length) {
    return
  }

  return removeStartAndEndSlash(rawCategories[0])
}

// Transform this: "/Apparel & Accessories/Clothing/Tops/"
// To this: "Apparel & Accessories/Clothing/Tops"
function removeStartAndEndSlash(category?: string) {
  return category?.replace(/^\/|\/$/g, '')
}

function splitIntoCategories(category?: string) {
  if (!category) return

  const splitted = category.split('/')

  return splitted
}

export function getPurchaseObjectData(order: Order) {
  return {
    affiliation: order.transactionAffiliation,
    coupon: order.coupon ? order.coupon : null,
    id: order.orderGroup,
    revenue: order.transactionTotal,
    shipping: order.transactionShipping,
    tax: order.transactionTax,
  }
}

export function getProductNameWithoutVariant(
  productNameWithVariant: string,
  variant: string
) {
  const indexOfVariant = productNameWithVariant.lastIndexOf(variant)

  if (indexOfVariant === -1 || indexOfVariant === 0) {
    return productNameWithVariant
  }

  return productNameWithVariant.substring(0, indexOfVariant - 1) // Removes the variant and the whitespace
}

function formatPurchaseProduct(product: ProductOrder) {
  const {
    name,
    skuName,
    id,
    brand,
    sku,
    price,
    quantity,
    categoryTree,
    productRefId,
    skuRefId,
  } = product

  const productName = getProductNameWithoutVariant(name, skuName)

  const item = {
    item_id: id,
    item_name: productName,
    item_brand: brand,
    item_variant: sku,
    price,
    quantity,
    ...getCategoriesWithHierarchy([categoryTree.join('/')]),
    ...customDimensions({
      productReference: productRefId,
      skuReference: skuRefId,
      skuName,
      quantity,
    }),
  }

  return item
}

export function formatCartItemsAndValue(
  cartItems: CartItem[],
  options?: {
    dividePrice: boolean
  }
) {
  let totalValue = 0.0

  if (!cartItems.length) return { items: [], totalValue }

  const items = cartItems.map((item: CartItem) => {
    const productName = getProductNameWithoutVariant(item.name, item.skuName)

    const shouldFormatPrice = item.priceIsInt ?? options?.dividePrice

    const formattedPrice = shouldFormatPrice ? item.price / 100 : item.price

    const itemBrand = item.brand ? item.brand : item.additionalInfo?.brandName

    const categoryIds = splitIntoCategories(item.productCategoryIds)
    const formattedCategories = item.category
      ? getCategoriesWithHierarchy([item.category])
      : getCategoriesHierarchyByKey(item.productCategories, categoryIds)

    totalValue += formattedPrice * item.quantity

    return {
      item_id: item.productId,
      item_brand: itemBrand,
      item_name: productName,
      item_variant: item.skuId,
      quantity: item.quantity,
      price: formattedPrice,
      ...formattedCategories,
      ...customDimensions({
        productReference: item.productRefId,
        skuReference: item.referenceId,
        skuName: item.variant,
        quantity: item.quantity,
      }),
    }
  })

  return { items, totalValue }
}

export function getPurchaseItems(orderProducts: ProductOrder[]) {
  return orderProducts.map(formatPurchaseProduct)
}

```


[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 speedy-gtm/react/modules/extraEvents.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
import push from './push'
import { PixelMessage } from '../typings/events'

async function emailToHash(email:string) {
  const msgUint8 = new TextEncoder().encode(email);
  const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  return hashHex;
}

export async function sendExtraEvents(e: PixelMessage) {

  switch (e.data.eventName) {
    case 'vtex:pageView': {
      push({
        event: 'pageView',
        location: e.data.pageUrl,
        page: e.data.pageUrl.replace(e.origin, ''),
        referrer: e.data.referrer,
        ...(e.data.pageTitle && {
          title: e.data.pageTitle,
        }),
      })

      return
    }

    case 'vtex:userData': {
      const { data } = e

      if (!data.isAuthenticated) {
        return
      }

      const emailHash = data.email ? await emailToHash(data.email) : undefined

      push({
        event: 'userData',
        userId: data.id,
        emailHash: emailHash
      })

      break
    }

    default: {
      break
    }
  }
}

```


[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 speedy-gtm/react/modules/gaEvents.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  AddToCartData,
  RemoveFromCartData,
  PromoViewData,
  OrderPlacedData,
  AddPaymentInfoData,
  ProductClickData,
  ProductViewData,
  ProductImpressionData,
  BeginCheckoutData,
  AddShippingInfoData,
  ViewCartData,
  SearchData,
  LoginData,
  RefundData,
  AddToWishlistData,
  SignUpData,
  ShareData,
  PromotionClickData,
} from '../typings/events'
import updateEcommerce from './updateEcommerce'
import {
  getPrice,
  getSeller,
  getCategoriesWithHierarchy,
  getQuantity,
  getImpressions,
  getDiscount,
  getPurchaseObjectData,
  getPurchaseItems,
  formatCartItemsAndValue,
} from './utils'
import { customDimensions, productViewSkuReference } from './customDimensions'
import shouldSendGA4Events from './utils/shouldSendGA4Events'

export function viewItem(eventData: ProductViewData) {
  if (!shouldSendGA4Events()) return

  const eventName = 'view_item'

  const { currency, product, list } = eventData

  const {
    selectedSku,
    productName,
    productId,
    productReference,
    categories,
    brand,
  } = product

  const { itemId: variant } = selectedSku

  const seller = getSeller(selectedSku.sellers)
  const value = getPrice(seller)
  const categoriesHierarchy = getCategoriesWithHierarchy(categories)
  const discount = getDiscount(seller)
  const quantity = getQuantity(seller)

  const item = {
    item_id: productId,
    item_name: productName,
    item_list_name: list,
    item_brand: brand,
    item_variant: variant,
    discount,
    quantity,
    price: value,
    ...categoriesHierarchy,
    ...customDimensions({
      productReference,
      skuReference: productViewSkuReference(product),
      skuName: selectedSku.name,
      quantity,
    }),
  }

  const data = {
    currency,
    value,
    items: [item],
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function viewItemList(eventData: ProductImpressionData) {
  if (!shouldSendGA4Events()) return

  const eventName = 'view_item_list'

  const { list, impressions } = eventData

  const items = getImpressions(impressions)

  const data = {
    item_list_name: list,
    items,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function selectItem(eventData: ProductClickData) {
  if (!shouldSendGA4Events()) return

  const eventName = 'select_item'

  const { product, list, position } = eventData

  const {
    sku,
    productName,
    productId,
    productReference,
    categories,
    brand,
  } = product

  const { itemId: variant, referenceId, name } = sku

  const seller = getSeller(sku.sellers)
  const price = getPrice(seller)
  const categoriesHierarchy = getCategoriesWithHierarchy(categories)
  const discount = getDiscount(seller)
  const quantity = getQuantity(seller)

  const item = {
    item_id: productId,
    item_name: productName,
    item_list_name: list,
    item_brand: brand,
    item_variant: variant,
    index: position,
    price,
    quantity,
    discount,
    ...categoriesHierarchy,
    ...customDimensions({
      productReference,
      skuReference: referenceId?.Value,
      skuName: name,
      quantity,
    }),
  }

  const data = {
    item_list_name: list,
    items: [item],
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function selectPromotion(eventData: PromotionClickData) {
  if (!shouldSendGA4Events()) return

  const eventName = 'select_promotion'
  const [promotion] = eventData.promotions

  const data = {
    creative_name: promotion.creative,
    creative_slot: promotion.position,
    promotion_id: promotion.id,
    promotion_name: promotion.name,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function viewPromotion(eventData: PromoViewData) {
  if (!shouldSendGA4Events()) return

  const eventName = 'view_promotion'

  const {
    promotions: [{ creative, position, id, name, products }],
  } = eventData

  let items: Array<{ item_id: string; item_name: string }> = []

  if (products?.length) {
    items = products.map(product => ({
      item_id: product.productId,
      item_name: product.productName,
    }))
  }

  const data = {
    creative_name: creative,
    creative_slot: position,
    promotion_id: id,
    promotion_name: name,
    items,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function addToCart(eventData: AddToCartData) {
  if (!shouldSendGA4Events()) return

  const eventName = 'add_to_cart'

  const { items: eventDataItems, currency } = eventData

  const { items, totalValue } = formatCartItemsAndValue(eventDataItems)

  const data = {
    items,
    currency,
    value: totalValue,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function removeFromCart(eventData: RemoveFromCartData) {
  if (!shouldSendGA4Events()) return

  const eventName = 'remove_from_cart'

  const { items: eventDataItems, currency } = eventData

  const { items, totalValue } = formatCartItemsAndValue(eventDataItems)

  const data = {
    items,
    currency,
    value: totalValue,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function purchase(eventData: OrderPlacedData) {
  if (!shouldSendGA4Events()) return

  const eventName = 'purchase'

  const { id, revenue, tax, shipping, coupon } = getPurchaseObjectData(
    eventData
  )

  const { transactionProducts, currency } = eventData

  const items = getPurchaseItems(transactionProducts)

  const data = {
    transaction_id: id,
    value: revenue,
    tax,
    shipping,
    coupon,
    items,
    currency,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function addPaymentInfo(eventData: AddPaymentInfoData) {
  const eventName = 'add_payment_info'

  const { currency, payment, items: eventDataItems } = eventData
  const { value, group } = payment

  const { items } = formatCartItemsAndValue(eventDataItems)

  const formattedValue = value / 100

  const data = {
    currency,
    value: formattedValue,
    payment_type: group,
    items,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function beginCheckout(eventData: BeginCheckoutData) {
  const eventName = 'begin_checkout'

  const { currency, items: eventDataItems } = eventData

  const { items, totalValue } = formatCartItemsAndValue(eventDataItems)

  const data = {
    currency,
    value: totalValue,
    items,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function addShippingInfo(eventData: AddShippingInfoData) {
  const eventName = 'add_shipping_info'

  const { currency, items: eventDataItems, shippingTier, value } = eventData

  const { items } = formatCartItemsAndValue(eventDataItems)

  const data = {
    currency,
    value,
    items,
    shipping_tier: shippingTier,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function viewCart(eventData: ViewCartData) {
  const eventName = 'view_cart'

  const { currency, items: eventDataItems } = eventData

  const { items, totalValue } = formatCartItemsAndValue(eventDataItems, {
    dividePrice: true,
  })

  const data = {
    currency,
    value: totalValue,
    items,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function addToWishlist(eventData: AddToWishlistData) {
  const eventName = 'add_to_wishlist'

  const { currency, items, list } = eventData

  const { product } = items

  const {
    sku,
    productName,
    productId,
    categories,
    brand,
    productReference,
  } = product

  const variant = sku ? sku.itemId : items.selectedItem.itemId

  const seller = getSeller(sku ? sku.sellers : items.selectedItem.sellers)
  const value = getPrice(seller)
  const categoriesHierarchy = getCategoriesWithHierarchy(categories)
  const discount = getDiscount(seller)
  const quantity = getQuantity(seller)

  const item = {
    item_id: productId,
    item_name: productName,
    item_list_name: list,
    item_brand: brand,
    item_variant: variant,
    discount,
    quantity,
    price: value,
    ...categoriesHierarchy,
    ...customDimensions({
      productReference,
      skuReference: sku ? sku.referenceId.Value : items.selectedItem.referenceId,
      skuName: sku ? sku.name : items.selectedItem.name,
      quantity,
    }),
  }

  const data = {
    currency,
    value,
    items: [item],
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function refund(eventData: RefundData) {
  const eventName = 'refund'

  const { currency } = eventData

  const {
    tax,
    shipping,
    value,
    transactionId,
    items: orderItems,
  } = eventData.order

  const items = getPurchaseItems(orderItems)

  const data = {
    transaction_id: transactionId,
    value,
    tax,
    shipping,
    items,
    currency,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function login(eventData: LoginData) {
  const eventName = 'login'

  const { method } = eventData

  const data = {
    method,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function signUp(eventData: SignUpData) {
  const eventName = 'sign_up'

  const { method } = eventData

  const data = {
    method,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function search(eventData: SearchData) {
  const eventName = 'search'

  const { term } = eventData

  const data = {
    search_term: term,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

export function share(eventData: ShareData) {
  const eventName = 'share'

  const { method, contentType, itemId } = eventData

  const data = {
    method,
    content_type: contentType,
    item_id: itemId,
  }

  updateEcommerce(eventName, { ecommerce: data })
}

```


[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 speedy-gtm/react/modules/customDimensions.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { ProductViewData, ProductViewReferenceId } from '../typings/events'

type CustomDimensionSkuAvailability = 'available' | 'unavailable'

interface CustomDimensions {
  /** Product reference */
  dimension1: string
  /** SKU reference */
  dimension2: string
  /** SKU name */
  dimension3: string
  /** SKU availability */
  dimension4: CustomDimensionSkuAvailability
}

interface CustomDimensionParams {
  productReference?: string
  skuReference?: string
  skuName?: string
  quantity: number
}

export function customDimensions(
  params: CustomDimensionParams
): CustomDimensions {
  return {
    dimension1: params.productReference ?? '',
    dimension2: params.skuReference ?? '',
    dimension3: params.skuName ?? '',
    dimension4: customDimensionSkuAvailability(params.quantity),
  }
}

export function customDimensionSkuAvailability(
  quantity: number
): CustomDimensionSkuAvailability {
  return quantity ? 'available' : 'unavailable'
}

export function productViewSkuReference(
  product: ProductViewData['product']
): string {
  const defaultReference = { Value: '' }

  // The `vtex:productView` is the only event where the
  // `product.selectedSku.referenceId` is different between its type and actual
  // data. Type `Item` (for `selectedSku`) specifies `referenceId` as a single
  // `{ Key: string; Value: string }` object type. However, in this event,
  // `referenceId` comes as an array of that object.
  //
  // This type conversion is needed because vtex.store does not normalize the
  // SKU Reference ID. Doing that there could possibly break some apps or
  // stores, so it's better doing it here.
  const skuReferenceId = (
    ((product.selectedSku
      .referenceId as unknown) as ProductViewReferenceId)?.[0] ??
    defaultReference
  ).Value

  return skuReferenceId ?? ''
}

```


[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 speedy-gtm/react/modules/utils/shouldSendGA4Events.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
export default function shouldSendGA4Events() {
  return Boolean(window?.__gtm__?.sendGA4Events)
}

```


[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 speedy-gtm/react/modules/analytics/utils.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
// Almost all of the logic of expiration and campaign invalidation is based on this official GA article https://support.google.com/analytics/answer/2731565

export interface AnalyticsData {
  location: string
  referrer: string
  expires: number
}

export interface AnalyticsDataWithOrigin extends AnalyticsData {
  origin: 'storage' | 'fresh'
}

export const UTM_SOURCE_PARAM = 'utm_source'
export const UTM_MEDIUM_PARAM = 'utm_medium'

// All of these parameters trigger a new session storage of the url
// This list was inspired by Simo Ahava's list used on his cookie implementation of this feature
// https://www.simoahava.com/analytics/persist-campaign-data-landing-page/#setup-the-persist-campaign-data-tag
export const analyticsURLParams = [
  UTM_SOURCE_PARAM,
  UTM_MEDIUM_PARAM,
  'utm_campaign',
  'gclid',
  'utm_term',
  'utm_content',
  'utm_id',
] as const

export function shouldInvalidateCurrentCampaign(
  location: Location,
  referrer: string,
  storageReferrer: string
) {
  const referrerURL = referrer ? new URL(referrer) : null

  // if user comes from a referring website
  if (
    referrerURL &&
    referrer !== storageReferrer &&
    referrerURL.host !== location.host
  ) {
    return true
  }

  // the slice removes the initial "?" character
  const individualLocationParams = location.search.slice(1).split('&')

  return individualLocationParams.some(locationParam => {
    return analyticsURLParams.some(campaignParam =>
      locationParam.startsWith(`${campaignParam}=`)
    )
  })
}

const STORAGE_KEY_ANALYTICS_DATA = 'analytics:session'

export function getAnalyticsFromStorage(): AnalyticsData | null {
  let analyticsData = null

  try {
    const rawAnalyticsData = window.localStorage.getItem(
      STORAGE_KEY_ANALYTICS_DATA
    )

    analyticsData = rawAnalyticsData ? JSON.parse(rawAnalyticsData) : null
  } catch (e) {
    console.error(
      'Error while fetching original browser location from localStorage storage'
    )
  }

  return analyticsData
}

export async function saveAnalyticsOnStorage(analyticsData: AnalyticsData) {
  try {
    window.localStorage.setItem(
      STORAGE_KEY_ANALYTICS_DATA,
      JSON.stringify(analyticsData)
    )
  } catch (e) {
    console.error(
      'Error while saving original browser location on localStorage storage'
    )
  }
}

export function currentDateInSeconds() {
  return Math.floor(Date.now() / 1000)
}

export const THIRTY_MIN_IN_SECONDS = 30 * 60

export function isStorageExpired(expiresIn: number, now: number) {
  return now - expiresIn > 0
}

export function renewExpirationDate() {
  return currentDateInSeconds() + THIRTY_MIN_IN_SECONDS
}

```


[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 speedy-gtm/react/modules/analytics/index.ts from the speedy-gtm VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  AnalyticsData,
  AnalyticsDataWithOrigin,
  currentDateInSeconds,
  getAnalyticsFromStorage,
  isStorageExpired,
  renewExpirationDate,
  saveAnalyticsOnStorage,
  shouldInvalidateCurrentCampaign,
} from './utils'

export function createOrGetAnalyticsData(): AnalyticsDataWithOrigin {
  let analyticsData = getAnalyticsData()
  let origin: AnalyticsDataWithOrigin['origin'] = 'storage'

  if (!analyticsData) {
    analyticsData = createAnalyticsData()
    origin = 'fresh'
  }

  saveAnalyticsOnStorage(analyticsData)

  return { ...analyticsData, origin }
}

function getAnalyticsData(): AnalyticsData | null {
  const analyticsFromStorage = getAnalyticsFromStorage()

  if (
    !analyticsFromStorage ||
    isStorageExpired(analyticsFromStorage.expires, currentDateInSeconds()) ||
    shouldInvalidateCurrentCampaign(
      window.location,
      document.referrer,
      analyticsFromStorage.referrer
    )
  ) {
    return null
  }

  return {
    ...analyticsFromStorage,
    expires: renewExpirationDate(),
  }
}

function createAnalyticsData(): AnalyticsData {
  return {
    location: window.location.href,
    referrer: document.referrer,
    expires: renewExpirationDate(),
  }
}

```


[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 training-week-demo/admin-app/README.md from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```markdown
# Admin App

## Objetivo

Desenvolver componente com experiência parecida ao apresentado na Demo. O app deve expor uma rota no Admin que possua um botão de upload que abre um modal com uma área de input de arquivo. Ao receber um arquivo CSV com o formato que você determinar, ele deve fechar o modal e popular uma tabela com os dados. O App deve possuir tratamentos para formatos inesperados, tanto de arquivo quanto para colunas no CSV. O App deve possuir uma funcionalidade adicional de popular a tabela com dados do Master Data, da entidade CL.

## Etapas

- [ ] Linkar o `admin-app`;
- [ ] Adicionar o app `vtex.styleguide` nas dependências;
- [ ] Alterar a `section` em `navigate.json` para `customer`;
- [ ] Alterar a rota do App para `/admin/training-week/users` em `navigation.json`;
- [ ] Alterar a rota do App para `/admin/app/training-week/users` em `routes.json`;
- [ ] Adicionar lógica de abertura do `Modal` com base no clique do botão;
- [ ] Adicionar mensagem informando o tipo de arquivo aceito no `Dropzone` e outras instruções;
- [ ] Adicionar `handlers` de upload de arquivo (`onDropAccepted`) e cancelamento (`onDropReject`) no `Dropzone`;
- [ ] Adicionar botão que iniciar o processo de população da tabela, isto é, transforme o arquivo CSV em um array de objetos. Para esta transformação, podemos utilizar uma lib como por exemplo [csvtojson](https://www.npmjs.com/package/csvtojson) (não é o melhor, mas é simples o suficiente para a tarefa). Não esqueça de rodar a instalação do pacote na pasta correta (`react`);
- [ ] Implementar `Table`;
- [ ] Popular tabela com dados anteriores;
- [ ] Implementar alertas ao usuário para erros de operação (arquivo inválido, erros desconhecidos, etc);
- [ ] Alterar as `labels` das mensagens em `messages/pt.json` (deve ter notado que elas estão bem estranhas);
- [ ] Implementar query para requisição de dados da entidade CL. Dica: o app que implementa é o `vtex.store-graphql`;
- [ ] Adicionar botão que efetua a query e popula a tabela. Dica: lembrar do `useQuery` e `useLazyQuery`;

## Links úteis

- https://github.com/vtex-apps/admin-example#readme
- https://styleguide.vtex.com/
- https://github.com/vtex-apps/render-runtime#readme
- https://learn.vtex.com/docs/course-admin-lang-en
- https://www.apollographql.com/docs/react/data/queries/#executing-a-query
- https://www.apollographql.com/docs/react/data/queries/#manual-execution-with-uselazyquery
- https://tachyons.io/docs/

```


[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 training-week-demo/admin-app/package.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "admin-example",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "lint": "eslint --ext js,jsx,ts,tsx .",
    "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json}\""
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{ts,js,tsx,jsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,graphql,gql}": [
      "prettier --write"
    ]
  },
  "devDependencies": {
    "@types/node": "^12.12.21",
    "@vtex/prettier-config": "^0.2.0",
    "eslint": "^6.8.0",
    "eslint-config-vtex": "^12.4.0",
    "eslint-config-vtex-react": "^6.4.0",
    "husky": "^4.2.5",
    "lint-staged": "^10.2.2",
    "prettier": "^2.0.5",
    "typescript": "^3.8.3",
    "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.131.0/public/@types/vtex.render-runtime"
  },
  "dependencies": {
    "react-apollo": "^3.1.5"
  }
}

```


[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 training-week-demo/admin-app/manifest.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "admin-app",
  "vendor": "trainingweekdev21",
  "version": "0.1.0",
  "title": "Example Admin App",
  "description": "",
  "mustUpdateAt": "2022-08-28",
  "scripts": {
    "postreleasy": "vtex publish"
  },
  "dependencies": {

  },
  "builders": {
    "react": "3.x",
    "admin": "0.x",
    "messages": "1.x"
  },
  "policies": [
    {
      "name": "outbound-access",
      "attrs": {
        "host": "portal.vtexcommercestable.com.br",
        "path": "/api/license-manager/*"
      }
    }
  ],
  "$schema": "https://raw.githubusercontent.com/vtex/node-vtex-api/master/gen/manifest.schema"
}

```


[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 training-week-demo/store-theme/manifest.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "vendor": "trainingweekdev21",
  "name": "store-theme",
  "version": "4.4.2",
  "builders": {
    "styles": "2.x",
    "store": "0.x",
    "docs": "0.x"
  },
  "mustUpdateAt": "2018-09-05",
  "scripts": {
    "postreleasy": "vtex publish --verbose"
  },
  "dependencies": {
    "vtex.store": "2.x",
    "vtex.store-header": "2.x",
    "vtex.product-summary": "2.x",
    "vtex.store-footer": "2.x",
    "vtex.store-components": "3.x",
    "vtex.styleguide": "9.x",
    "vtex.slider": "0.x",
    "vtex.carousel": "2.x",
    "vtex.shelf": "1.x",
    "vtex.menu": "2.x",
    "vtex.minicart": "2.x",
    "vtex.product-details": "1.x",
    "vtex.product-kit": "1.x",
    "vtex.search-result": "3.x",
    "vtex.login": "2.x",
    "vtex.my-account": "1.x",
    "vtex.flex-layout": "0.x",
    "vtex.rich-text": "0.x",
    "vtex.store-drawer": "0.x",
    "vtex.locale-switcher": "0.x",
    "vtex.product-quantity": "1.x",
    "vtex.product-identifier": "0.x",
    "vtex.breadcrumb": "1.x",
    "vtex.sticky-layout": "0.x",
    "vtex.product-customizer": "2.x",
    "vtex.stack-layout": "0.x",
    "vtex.product-specification-badges": "0.x",
    "vtex.product-review-interfaces": "1.x",
    "vtex.telemarketing": "2.x",
    "vtex.order-placed": "2.x",
    "vtex.checkout-summary": "0.x",
    "vtex.product-list": "0.x",
    "vtex.add-to-cart-button": "0.x",
    "vtex.product-bookmark-interfaces": "1.x",
    "vtex.responsive-layout": "0.x",
    "vtex.slider-layout": "0.x",
    "vtex.store-image": "0.x",
    "vtex.store-icons": "0.x",
    "vtex.modal-layout": "0.x",
    "vtex.store-link": "0.x",
    "vtex.store-video": "1.x",
    "vtex.product-gifts": "0.x",
    "vtex.product-price": "1.x",
    "vtex.disclosure-layout": "1.x",
    "vtex.store-form": "0.x",
    "vtex.product-highlights": "2.x",
    "vtex.product-specifications": "1.x",
    "vtex.tab-layout": "0.x",
    "vtex.condition-layout": "2.x",
    "vtex.css-handles": "1.x"
  },
  "peerDependencies": {
    "vtex.reviews-and-ratings": "2.x"
  },
  "$schema": "https://raw.githubusercontent.com/vtex/node-vtex-api/master/gen/manifest.schema"
}

```


[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 training-week-demo/storefront-app/dangerfile.js from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```javascript
// dangerfile.js
const { verify } = require('@vtex/danger')

const config = {
  // Set to true to enforce keepachangelog rules.
  keepachangelog: {
    changeVersion: false,
  },
  rules: {
    description: ['fail', { minLength: 20 }],
    assignee: 'fail',
    reviewers: 'fail',
    pr_size: ['warn', { additionLimit: 800, deletionLimit: -1 }],
    lock_file: 'warn',
    no_ignored_test: ['fail', { pattern: /test|spec/i }],
    no_debugger: ['fail', { pattern: /\.[tj]sx?$/i }],
    enforce_graphql_provider: 'off',
  },
}

verify(config)

```


[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 training-week-demo/storefront-app/README.md from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```markdown
# Storefront App

## Objetivo

Replicar os componentes apresentados na Demo: `product-draw` e `drawn-badge`. O componente `product-draw` deve receber uma lista de IDs de produto via `props` ou Site Editor e, ao clicar em um botão, sortear um produto e apresentar pelo menos o nome e imagem do produto, que ao clicar, redireciona para a página de produto do mesmo. O `drawn-badge` deve aparecer na página do último produto sorteado somente, para os demais não deve aparecer.

## Etapas

- [ ] Linkar o `storefront-app` e em seguida o `store-theme`;
- [ ] Adicionar o app `trainingweekdev21.storefront-app` como dependência no `store-theme`. Somente haverá essa manipulação no `manifest` da `store-theme`, as demais serão no `storefront-app`;
- [ ] Posicionar o componente `product-draw` no bloco `store.home`
- [ ] Criar query GraphQL para requisição de produtos via `productId`. Utilize o GraphiQL IDE;
- [ ] Adicionar o app `vtex.search-graphql` nas dependências;
- [ ] Implementar componente React. Não esqueça de adicionar as dependências no `manifest.json` caso utilize `vtex.render-runtime`, `vtex.store-image`, entre outros (use sua imaginação);
- [ ] Implementar lógica de sorteio de `productId` dado uma lista;
- [ ] Usar o `useQuery` ou o `useLazyQuery` para executar a query criada, utilizando como variável o `productId` sorteado;
- [ ] Adicionar `prop` para receber a lista de produtos via `blocks` e preenchê-la via `blocks`;
- [ ] Adicionar interface Site Editor para o input de `props`;
- [ ] Persistir o `productId` sorteado. Sugerimos o `localStorage`, dica: ele só existe no `client-side`;
- [ ] Posicionar o componente `product-draw` no block `store.product`;
- [ ] Implementar componente React;
- [ ] Adicionar o app `vtex.product-context` como dependência e utilizar o hook `useProduct` para capturar os dados do produto da página;
- [ ] Adicionar lógica de exibição do componente caso seja o último produto sorteado;

## Links úteis

- https://developers.vtex.com/vtex-developer-docs/docs/vtex-io-documentation-3-creating-the-new-app
- https://github.com/vtex-apps/render-runtime#readme
- https://github.com/vtex-apps/store-image#readme
- https://www.apollographql.com/docs/react/data/queries/#executing-a-query
- https://www.apollographql.com/docs/react/data/queries/#manual-execution-with-uselazyquery
- https://github.com/vtex-apps/product-context#product-context
- https://tachyons.io/docs/

```


[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 training-week-demo/storefront-app/package.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "CHANGE_ME",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "lint": "eslint --ext js,jsx,ts,tsx .",
    "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json}\"",
    "lint:locales": "intl-equalizer",
    "test": "cd ./react && yarn test"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{ts,js,tsx,jsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,graphql,gql}": [
      "prettier --write"
    ],
    "*.json": [
      "prettier --write"
    ],
    "messages/*.json": [
      "yarn lint:locales"
    ]
  },
  "devDependencies": {
    "@vtex/intl-equalizer": "^2.5.0",
    "@vtex/prettier-config": "^0.3.5",
    "eslint": "^7.14.0",
    "eslint-config-vtex": "^12.8.11",
    "eslint-config-vtex-react": "^6.8.3",
    "husky": "^4.3.0",
    "lint-staged": "^10.5.1",
    "prettier": "^2.2.0",
    "typescript": "^3.9.6",
    "@vtex/danger": "^0.2.7"
  },
  "version": "0.0.0",
  "intl-equalizer": {
    "referenceLocale": "en",
    "localeDirectory": "messages"
  }
}
```


[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 training-week-demo/storefront-app/manifest.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "vendor": "trainingweekdev21",
  "name": "storefront-app",
  "version": "0.1.0",
  "title": "Taining week component",
  "description": "Training week component example",
  "builders": {
    "react": "3.x",
    "store": "0.x",
    "messages": "1.x"
  },
  "dependencies": {

  },
  "registries": [
    "smartcheckout"
  ],
  "policies": [],
  "$schema": "https://raw.githubusercontent.com/vtex/node-vtex-api/master/gen/manifest.schema"
}

```


[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 training-week-demo/service-app/dangerfile.js from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```javascript
// dangerfile.js
const { verify } = require('@vtex/danger')

const config = {
  // Set to true to enforce keepachangelog rules.
  keepachangelog: {
    changeVersion: false,
  },
  rules: {
    description: ['fail', { minLength: 20 }],
    assignee: 'fail',
    reviewers: 'fail',
    pr_size: ['warn', { additionLimit: 800, deletionLimit: -1 }],
    lock_file: 'warn',
    no_ignored_test: ['fail', { pattern: /test|spec/i }],
    no_debugger: ['fail', { pattern: /\.[tj]sx?$/i }],
    enforce_graphql_provider: 'off',
  },
}

verify(config)

```


[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 training-week-demo/service-app/README.md from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```markdown
# Service App

## Objetivo

Replicar as rotas e queries apresentadas na Demo. O app deve possuir duas rotas: `/_v/jsonplaceholder/users` e `/_v/jsonplaceholder/users/:userId`. A primeira deve responder para os métodos `GET`, que responde como um proxy para `https://jsonplaceholder.typicode.com/users/`, e `POST`, que deve receber um `body` e subir um erro caso não seja um `JSON` com a propriedade `id`. Para as queries, devem existir pelo menos duas: uma para os `users` da API do `jsonplaceholder` e outra para os clientes da entidade CL do Master Data;

## Etapas

- [ ] Linkar o `storefront-service`;
- [ ] Adicionar a rota `/_v/jsonplaceholder/users` no arquivo `service.json`;
- [ ] No objeto `export default` do arquivo `node/index.ts`, adicionar a rota anterior dentro de `routes`;
- [ ] Criar middleware para `GET` na rota anterior e configurá-lo no mesmo lugar mencionado anteriormente;
- [ ] Implementar o `client` para o JsonPlaceholder e adicioná-lo no arquivo `node/clients/index.ts`;
- [ ] Adicionar policy de `outbound-access` no `manifest` para o host `jsonplaceholder.typicode.com` e path `/*`;
- [ ] Aprimorar middleware anterior para consumir o `client` implementado;
- [ ] Implementar middleware para `POST` na rota anterior, com a lógica de encontrar o `user` para o `userId` passado;
- [ ] Adicionar os `schemas` das queries e typagem no arquivo `graphql/schema.graphql`;
- [ ] Adicionar configuração das queries no objeto `export default` do arquivo `node/index.ts` dentro da propriedade `graphql` > `resolvers`;
- [ ] Implementar resolver para a query;
- [ ] Implementar `client` para o Master Data utilizando o `ExternalClient`;
- [ ] Adicionar policy de `outbound-access` no `manifest` para o host `{{account}}.vtexcommercestable.com.br` e path `/*`;
- [ ] Adicionar policy de `POWER_USER_DS` no `manifest` para permitir nosso `client` utilizar o `authToken` para chamadas ao Master Data;
- [ ] Implementar resolver para a query, utilizando o `client` anterior;

## Links úteis

- https://developers.vtex.com/vtex-developer-docs/docs/how-to-use-and-create-clients-on-vtex-io
- https://github.com/vtex-apps/service-example#readme
- https://github.com/vtex-apps/graphql-example#readme
- https://developers.vtex.com/vtex-rest-api/reference/master-data-api-v1-overview
- https://koajs.com/

```


[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 training-week-demo/service-app/package.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "CHANGE_ME",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "lint": "eslint --ext js,jsx,ts,tsx .",
    "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json}\""
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{ts,js,tsx,jsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,graphql,gql}": [
      "prettier --write"
    ]
  },
  "devDependencies": {
    "@types/node": "^12.12.21",
    "@vtex/danger": "^0.2.7",
    "@vtex/prettier-config": "^0.3.1",
    "eslint": "^6.8.0",
    "eslint-config-vtex": "^12.7.0",
    "husky": "^4.2.0",
    "lint-staged": "^10.0.2",
    "prettier": "^2.0.5",
    "typescript": "^3.9.6"
  }
}

```


[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 training-week-demo/service-app/manifest.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "service-app",
  "vendor": "trainingweekdev21",
  "version": "0.1.0",
  "title": "GraphQL Example",
  "description": "Reference app for VTEX IO GraphQL Services",
  "mustUpdateAt": "2022-01-04",
  "categories": [],
  "dependencies": {},
  "builders": {
    "docs": "0.x",
    "graphql": "1.x",
    "node": "6.x"
  },
  "scripts": {
    "prereleasy": "bash lint.sh"
  },
  "credentialType": "absolute",
  "policies": [
    {
      "name": "colossus-fire-event"
    },
    {
      "name": "colossus-write-logs"
    },
    {
      "name": "graphql-query"
    },
    {
      "name": "outbound-access",
      "attrs": {
        "host": "api.vtex.com",
        "path": "/dataentities/*"
      }
    }
  ],
  "$schema": "https://raw.githubusercontent.com/vtex/node-vtex-api/master/gen/manifest.schema"
}

```


[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 training-week-demo/admin-app/messages/pt.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "admin-trainingweek.navigation.label-group": "I'm not right",
  "admin-trainingweek.navigation.label-users": "Me neither",
  "admin-trainingweek.navigation.search.kws": "teste, mock, fake",
  "admin-trainingweek.navigation.details.search.kws": "detalhe, mock, test, oi",
  "admin-trainingweek.navigation.table.search.kws": "tabela, mock, test, tchau",
  "admin-trainingweek.users-title": "I think I'm not right too"
}

```


[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 training-week-demo/admin-app/admin/navigation.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "section": "order",
  "titleId": "admin-trainingweek.navigation.label-group",
  "searchKeyWordsHelpers": "admin-trainingweek.navigation.search.kws",
  "subSectionItems": [
    {
      "labelId": "admin-trainingweek.navigation.label-users",
      "path": "/admin/change-me/",
      "searchKeyWordsHelpers": "admin-trainingweek.navigation.table.search.kws"
    }
  ]
}

```


[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 training-week-demo/admin-app/admin/routes.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "admin.app.users": {
    "component": "UsersContainer",
    "path": "/admin/app/change-me"
  }
}

```


[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 training-week-demo/admin-app/react/package.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "vtex.admin-example",
  "version": "0.0.12",
  "devDependencies": {
    "@types/node": "^11.12.1",
    "@types/react": "^16.8.10",
    "@types/react-intl": "^2.3.17",
    "@vtex/tsconfig": "^0.4.4",
    "apollo-client": "^2.4.7",
    "graphql": "^14.0.2",
    "prop-types": "^15.7.2",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-intl": "^2.7.2",
    "trainingweekdev21.admin-app": "https://brunomoreira--trainingweekdev21.myvtex.com/_v/private/typings/linked/v1/trainingweekdev21.admin-app@0.1.0+build1632880086/public/@types/trainingweekdev21.admin-app",
    "typescript": "3.9.7",
    "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.132.1/public/@types/vtex.render-runtime",
    "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.145.0/public/@types/vtex.styleguide"
  },
  "dependencies": {
    "@types/faker": "^4.1.5",
    "faker": "^4.1.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 training-week-demo/admin-app/react/styles.global.css from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```css
html,
body,
#render-container,
#render-provider,
.render-container,
.render-provider {
  height: 100%;
}

```


[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 training-week-demo/admin-app/react/UsersContainer.tsx from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FC } from 'react'
import { FormattedMessage } from 'react-intl'
import { Layout, PageBlock, PageHeader } from 'vtex.styleguide'
import UsersPage from './pages/UsersPage'

import './styles.global.css'

const UsersContainer: FC = () => {
  return (
    <Layout
      pageHeader={
        <PageHeader
          title={<FormattedMessage id="admin-trainingweek.users-title" />}
        />
      }
    >
      <PageBlock variation="full">
        <UsersPage />
      </PageBlock>
    </Layout>
  )
}

export default UsersContainer

```


[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 training-week-demo/admin-app/react/graphql/crazyQuery.graphql from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```graphql
mutation MyCrazyQuery($someVariable: String) {
  aQuery(prop: $someVariable)
}

```


[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 training-week-demo/admin-app/react/pages/UsersPage.tsx from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { useState } from 'react'
import { Button, /** IconUpload, */ Modal, Dropzone, /** Table */ } from 'vtex.styleguide'
// import csv from 'csvtojson'
// import { useQuery, useMutation } from 'react-apollo'

const UsersPage = () => {
  const [isModalOpen, /** setModalOpen */] = useState(false)

  return (
    <>
      <Modal centered isOpen={isModalOpen} onClose={() => alert('close')}>
        <div className="flex flex-column dark-gray">
          <Dropzone
            onDropAccepted={() => alert('accepted')}
            onDropReject={() => alert('rejected')}
            isLoading={false}
           ></Dropzone>
        </div>
      </Modal>
      <div className="flex justify-center">
        <div className="mb4">
          <Button variation="primary" onClick={() => alert('clicked')}></Button>
        </div>
      </div>
    </>
  )
}

export default UsersPage

```


[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 training-week-demo/store-theme/docs/README.md from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```markdown
# Store theme
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
Our boilerplate theme to create stores in the VTEX IO platform.

## Preview
![store-theme-default](https://user-images.githubusercontent.com/1354492/63937047-e8d81c80-ca37-11e9-86fc-61e88847bbfb.png)

## Tutorial
To understand how things work check our tutorial [Build a store using VTEX IO](https://vtex.io/docs/getting-started/build-stores-with-store-framework/1/)

## Dependencies
All store components that you see on this document are open source too. Production ready, you can found those apps in this GitHub organization.

Store framework is the baseline to create any store using _VTEX IO Web Framework_.
- [Store](https://github.com/vtex-apps/store/blob/master/README.md)

Store GraphQL is a middleware to access all VTEX APIs.
- [Store GraphQL](https://github.com/vtex-apps/store-graphql/blob/master/docs/README.md)

### Store Component Apps
- [Header](https://github.com/vtex-apps/store-header/blob/master/docs/README.md)
- [Footer](https://github.com/vtex-apps/store-footer/blob/master/docs/README.md)
- [Slider Layout](https://github.com/vtex-apps/slider-layout/blob/master/docs/README.md)
- [Shelf](https://github.com/vtex-apps/shelf/blob/master/docs/README.md)
- [Telemarketing](https://github.com/vtex-apps/telemarketing/blob/master/docs/README.md)
- [Menu](https://github.com/vtex-apps/menu/blob/master/docs/README.md)
- [Login](https://github.com/vtex-apps/login/blob/master/docs/README.md)
- [Minicart](https://github.com/vtex-apps/minicart/blob/master/docs/README.md)
- [Category Menu](https://github.com/vtex-apps/category-menu/blob/master/docs/README.md)
- [Product Summary](https://github.com/vtex-apps/product-summary/blob/master/docs/README.md)
- [Breadcrumb](https://github.com/vtex-apps/breadcrumb/blob/master/docs/README.md)
- [Search Result](https://github.com/vtex-apps/search-result/blob/master/docs/README.md)
- [Product Details](https://github.com/vtex-apps/product-details/blob/master/docs/README.md)
- [Store Components](https://github.com/vtex-apps/store-components/blob/master/docs/README.md)
- [Order Placed](https://github.com/vtex-apps/order-placed/blob/master/docs/README.md)

### Store Pixel Apps

 - [Facebook Pixel](https://github.com/vtex-apps/facebook-pixel/blob/master/docs/README.md)
 - [Google Tag Manager](https://github.com/vtex-apps/google-tag-manager/blob/master/docs/README.md)

## Contributing

Check it out [how to contribute](https://github.com/vtex-apps/awesome-io#contributing) with this project.

## Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
  <tr>
    <td align="center"><a href="http://www.hugoccosta.com"><img src="https://avatars2.githubusercontent.com/u/20212776?v=4" width="100px;" alt=""/><br /><sub><b>Hugo Costa</b></sub></a><br /><a href="https://github.com/vtex-apps/store-theme/commits?author=hugocostadev" title="Documentation">📖</a></td>
  </tr>
</table>

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

```


[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 training-week-demo/store-theme/docs/contact-schema.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "title": "Contact",
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "title": "Title"
    },
    "name": {
      "type": "string",
      "title": "Name"
    },
    "email": {
      "type": "string",
      "format": "email",
      "title": "Email address"
    },
    "message": {
      "type": "string",
      "title": "Message"
    }
  },
  "required": ["email", "message"],
  "v-security": {
    "publicJsonSchema": 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 training-week-demo/store-theme/store/blocks.jsonc from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
/* Blocks can be defined both on the store/blocks.json file, and in any number of json
 * files inside the store/blocks directory, with any arbitrary directory structure.
 *
 * Additionally, all blocks files can support comments, such as this one, if given the
 * .jsonc file extension.
 *
 * The store/blocks.json file is intended for backwards compatibility, for miscellaneous
 * blocks, or if the app is simple enough that it doesn't warrant splitting the file.
 */
{
  "store.orderplaced": {
    "blocks": ["order-placed"]
  },

  "store.account": {
    "blocks": ["my-account"],
    "parent": {
      "challenge": "challenge.profile"
    }
  },

  "store.login": {
    "blocks": ["login-content#default"]
  },

  "login-content#default": {
    "props": {
      "isInitialScreenOptionOnly": false,
      "defaultOption": 1,
      "showPasswordVerificationIntoTooltip": true
    }
  },

  "store.product-review-form": {
    "blocks": ["product-review-form"]
  },

  "store.not-found#product": {
    "blocks": ["rich-text#not-found"]
  },

  "store.not-found#search": {
    "blocks": ["rich-text#not-found"]
  },

  "rich-text#not-found": {
    "props": {
      "textAlignment": "CENTER",
      "textPosition": "CENTER",
      "text": "**PAGE NOT FOUND**",
      "font": "t-heading-1"
    }
  }
}

```


[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 training-week-demo/store-theme/store/routes.json from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store.custom#about-us": {
    "path": "/about-us"
  },
  "store.custom#faq": {
    "path": "/faq"
  },
  "store.custom#contact-us": {
    "path": "/contact-us"
  }
}

```


[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 training-week-demo/store-theme/styles/css/vtex.store-video.css from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```css
.videoContainer {
  width: 100%;
  height: 300px;
  margin-top: 2%;
}

@media only screen and (min-width: 640px) {
  .videoContainer {
    height: 700px;
  }
}

```


[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 training-week-demo/store-theme/styles/css/vtex.product-price.css from the training-week-demo VTEX app.

[Assistant]
Here's the implementation:

```css
.listPrice {
  color: #727273;
  margin-bottom: .25rem;
  font-size: 1rem;
}

.sellingPrice {
  color: #3f3f40;
  font-size: 1.25rem;
}

.sellingPriceValue {
  font-size: 2.25rem;
  font-weight: 700;
}

.installments {
  color: #727273;
  margin-bottom: 1rem;
}

.savings {
  font-weight: 500;
  color: #79B03A;
}

.sellingPriceValue--summary {
  font-size: 1.25rem;
  font-weight: 600;
  color: #2E2E2E;
}

.savings--summary {
  background: #8BC34A;
  border-radius: 1000px;
  align-items: center;
  display: flex;

  padding-left: 0.5rem;
  padding-right: 0.5rem;
  font-size: 0.875rem;
  font-weight: 600;
  vertical-align: baseline;
  color: #FFFFFF;
}

.savings-discount--summary {
  font-size: 0.875rem;
  font-weight: 600;
  vertical-align: baseline;
  color: #FFFFFF;
  padding-left: 0.5rem;
  padding-right: 0.5rem;
}

.listPrice--summary {
  margin-bottom: 0.25rem;
  font-size: .875rem;
}

.installments--summary {
  margin-bottom: 2rem;
  font-size: 0.875rem;
}

.savings--summaryPercentage {
  background: #0f3e99;
  border-radius: 1000px;
  align-items: center;
  display: flex;
}

.savingsPercentage--summaryPercentage {
  font-size: 0.875rem;
  font-weight: 600;
  vertical-align: baseline;
  color: #FFFFFF;
  padding: 0.25rem 0.5rem 0.25rem 0.5rem;
}

```