diff --git a/.eslintrc b/.eslintrc index 3ccd305..9713dfd 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,8 +2,6 @@ "extends": "vtex", "root": true, "env": { - "node": true, - "es6": true, - "jest": true + "node": true } } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ac781d7..b8f76e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Fixed +- Performance improvements + ## [1.0.10] - 2020-11-06 ### Fixed diff --git a/package.json b/package.json index 414cdb1..4a2f6b5 100644 --- a/package.json +++ b/package.json @@ -30,4 +30,4 @@ "prettier": "^1.19.1", "typescript": "^3.7.5" } -} +} \ No newline at end of file diff --git a/react/AddProductBtn.tsx b/react/AddProductBtn.tsx index ae5e728..ef3b7ef 100644 --- a/react/AddProductBtn.tsx +++ b/react/AddProductBtn.tsx @@ -1,20 +1,25 @@ -/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-use-before-define */ import React, { FC, useState, useContext, useEffect } from 'react' +import { useMutation, useLazyQuery } from 'react-apollo' +import { WrappedComponentProps, defineMessages, injectIntl } from 'react-intl' import { ProductContext } from 'vtex.product-context' import { Button, ToastContext } from 'vtex.styleguide' -import { compose, graphql, useApolloClient, useMutation } from 'react-apollo' -import { injectIntl, WrappedComponentProps, defineMessages } from 'react-intl' -import userProfile from './queries/userProfile.gql' +import { useRuntime } from 'vtex.render-runtime' +import { useCssHandles } from 'vtex.css-handles' + +import { getSession } from './modules/session' +import storageFactory from './utils/storage' import checkItem from './queries/checkItem.gql' import addToList from './queries/addToList.gql' import removeFromList from './queries/removeFromList.gql' import styles from './styles.css' -import { useRuntime } from 'vtex.render-runtime' -import { useCssHandles } from 'vtex.css-handles' -const CSS_HANDLES = ['wishlistIconContainer','wishlistIcon'] as const +const localStore = storageFactory(() => sessionStorage) +const CSS_HANDLES = ['wishlistIconContainer', 'wishlistIcon'] as const -let isAuthenticated = false +let isAuthenticated = + JSON.parse(String(localStore.getItem('wishlist_isAuthenticated'))) ?? false +let shopperId = localStore.getItem('wishlist_shopperId') ?? null const productCheck = {} const defaultValues = { @@ -52,35 +57,90 @@ const messages = defineMessages({ }, }) -const AddBtn: FC = ({ - data: { loading: sessionLoading, getSession }, - intl, -}: any) => { +const useSessionResponse = () => { + const [session, setSession] = useState() + const sessionPromise = getSession() + + useEffect(() => { + if (!sessionPromise) { + return + } + + sessionPromise.then(sessionResponse => { + const { response } = sessionResponse + setSession(response) + }) + }, [sessionPromise]) + + return session +} + +const AddBtn: FC = ({ intl }) => { const [state, setState] = useState({ isLoading: true, isWishlisted: false, + isWishlistPage: null, wishListId: null, }) + const [removeProduct, { loading: removeLoading }] = useMutation( + removeFromList, + { + onCompleted: (res: any) => { + setState({ + ...state, + isWishlisted: !res.removeFromList, + isWishlistPage: false, + wishListId: res.removeFromList ? null : wishListId, + }) + }, + } + ) const { navigate, history } = useRuntime() - const client = useApolloClient() const handles = useCssHandles(CSS_HANDLES) const { showToast } = useContext(ToastContext) const { product } = useContext(ProductContext) as any + const sessionResponse: any = useSessionResponse() + const [handleCheck, { data, loading, called }] = useLazyQuery(checkItem) + const [addProduct, { loading: addLoading }] = useMutation(addToList, { + onCompleted: (res: any) => { + setState({ + ...state, + isWishlisted: !!res.addToList, + wishListId: res.addToList, + }) + if (res.addToList) { + toastMessage('productAddedToList') + } else { + toastMessage('addProductFail') + } + }, + }) - if(!product) return null - - const toastMessage = (messsageKey: string) => { - let action: any = undefined + if (sessionResponse) { + isAuthenticated = + sessionResponse?.namespaces?.profile?.isAuthenticated?.value === 'true' + shopperId = sessionResponse?.namespaces?.profile?.email?.value ?? null + + localStore.setItem( + 'wishlist_isAuthenticated', + JSON.stringify(isAuthenticated) + ) + localStore.setItem('wishlist_shopperId', String(shopperId)) + } + const toastMessage = (messsageKey: string) => { + let action: any if (messsageKey === 'notLogged') { action = { label: intl.formatMessage(messages.login), onClick: () => navigate({ page: 'store.login', - query: `returnUrl=${encodeURIComponent(history.location.pathname)}`, + query: `returnUrl=${encodeURIComponent( + history?.location?.pathname + )}`, }), } } @@ -90,6 +150,7 @@ const AddBtn: FC = ({ onClick: () => navigate({ page: 'store.wishlist', + fetchPage: true, }), } } @@ -99,16 +160,17 @@ const AddBtn: FC = ({ action, }) } - + const { isWishlisted, wishListId, isWishlistPage } = state - const { isLoading, isWishlisted, wishListId } = state + if (!product) return null - if (getSession?.profile && !isAuthenticated) { - isAuthenticated = getSession.profile.email + if (isWishlistPage === null && product?.wishlistPage) { + setState({ + ...state, + isWishlistPage: true, + }) } - - const getIdFromList = (list: string, item: any) => { const pos = item.listNames.findIndex((listName: string) => { return list === listName @@ -116,89 +178,32 @@ const AddBtn: FC = ({ return item.listIds[pos] } - const handleCheck = async variables => { - const { data } = await client.query({ - query: checkItem, - variables, - fetchPolicy: 'no-cache', + if ( + isAuthenticated && + product && + !called && + !productCheck[product.productId] + ) { + handleCheck({ + variables: { + shopperId: String(shopperId), + productId: String(product.productId), + }, }) - if (data?.checkList?.inList) { - setState({ - ...state, - isWishlisted: data.checkList.inList, - isLoading: false, - wishListId: getIdFromList(defaultValues.LIST_NAME, data.checkList), - }) - } else { - setState({ - ...state, - isLoading: false, - }) - } } - useEffect(() => { - if (!sessionLoading && isAuthenticated && product && !productCheck[product.productId]) { - productCheck[product.productId] = product - if (product) { - handleCheck({ - shopperId: String(isAuthenticated), - productId: String(product.productId), - }) - } - } else { - if(!isAuthenticated && state.isLoading) { - setState({ - ...state, - isLoading: false, - }) - } - } - }) - - const [addProduct] = useMutation(addToList, { - onCompleted: (res: any) => { - setState({ - ...state, - isLoading: false, - isWishlisted: !!res.addToList, - wishListId: res.addToList, - }) - if(!!res.addToList) { - toastMessage('productAddedToList') - } else { - toastMessage('addProductFail') - } - }, - }) - - const [removeProduct] = useMutation(removeFromList, { - onCompleted: (res: any) => { - setState({ - ...state, - isLoading: false, - isWishlisted: !res.removeFromList, - wishListId: res.removeFromList ? null : wishListId, - }) - }, - }) - const handleAddProductClick = e => { e.preventDefault() e.stopPropagation() if (isAuthenticated) { - setState({ - ...state, - isLoading: true, - }) - if (!isWishlisted) { + if (isWishlistPage !== true && !isWishlisted) { addProduct({ variables: { listItem: { productId: product.productId, title: product.productName, }, - shopperId: getSession.profile.email, + shopperId, name: defaultValues.LIST_NAME, }, }) @@ -206,38 +211,59 @@ const AddBtn: FC = ({ removeProduct({ variables: { id: wishListId, - shopperId: getSession.profile.email, + shopperId, name: defaultValues.LIST_NAME, }, }) + if (productCheck[product.productId]) { + productCheck[product.productId].isWishlisted = false + } } } else { toastMessage('notLogged') } } + + if ( + data?.checkList?.inList && + !wishListId && + (!productCheck[product.productId] || + productCheck[product.productId].wishListId === null) + ) { + const itemWishListId = getIdFromList( + defaultValues.LIST_NAME, + data.checkList + ) + setState({ + ...state, + isWishlisted: data.checkList.inList, + wishListId: itemWishListId, + }) + productCheck[product.productId] = { + isWishlisted: data.checkList.inList, + wishListId: itemWishListId, + } + } + + const checkFill = () => { + return isWishlisted || (isWishlistPage && wishListId === null) + } + return (
) } -export default injectIntl( - compose( - graphql(userProfile, { - options: { - ssr: false, - }, - }) - )(AddBtn) -) +export default injectIntl(AddBtn) diff --git a/react/ProductSummaryWishlist.jsx b/react/ProductSummaryWishlist.jsx deleted file mode 100644 index e3893e1..0000000 --- a/react/ProductSummaryWishlist.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useMemo } from 'react' -import { ExtensionPoint, useTreePath } from 'vtex.render-runtime' -import { useListContext, ListContextProvider } from 'vtex.list-context' -import { ProductListContext } from 'vtex.product-list-context' -import { mapCatalogProductToProductSummary } from './utils/normalize' -import ProductListEventCaller from './components/ProductListEventCaller' -import { useQuery } from 'react-apollo' -import productsQuery from './queries/productById.gql' -import userProfile from './queries/userProfile.gql' -import ViewLists from './queries/viewLists.gql' -import { useRuntime } from 'vtex.render-runtime' - -const ProductSummaryList = ({ children }) => { - const { list } = useListContext() || [] - const { treePath } = useTreePath() - const { navigate, history } = useRuntime() - - const { data: profileData, loading: profileLoading } = useQuery(userProfile, { - ssr: false, - }) - - const { data: dataLists } = useQuery(ViewLists, { - ssr: false, - skip: !profileData || !profileData.getSession, - fetchPolicy: 'no-cache', - variables: { - shopperId: profileData && profileData.getSession && profileData.getSession.profile && profileData.getSession.profile.email, - }, - }) - - const { data, loading, error } = useQuery(productsQuery, { - ssr: false, - skip: !dataLists || !dataLists.viewLists, - variables: { - ids: - dataLists && - dataLists.viewLists[0].data.map(item => { - return item.productId - }), - }, - }) - - const { productsByIdentifier: products } = data || {} - - const newListContextValue = useMemo(() => { - const componentList = - products && - products.map(product => { - const normalizedProduct = mapCatalogProductToProductSummary(product) - return ( - - ) - }) - return list.concat(componentList) - }, [products, treePath, list]) - - if( !profileLoading && (!profileData || !profileData.getSession.profile) ) { - navigate({ - page: 'store.login', - query: `returnUrl=${encodeURIComponent(history.location.pathname)}`, - }) - } - - if (!data || loading || error) { - return null - } - - return ( - - {children} - - ) -} - -const EnhancedProductList = ({ children }) => { - const { ProductListProvider } = ProductListContext - return ( - - {children} - - - ) -} - -export default EnhancedProductList diff --git a/react/ProductSummaryWishlist.tsx b/react/ProductSummaryWishlist.tsx new file mode 100644 index 0000000..484f20b --- /dev/null +++ b/react/ProductSummaryWishlist.tsx @@ -0,0 +1,127 @@ +import React, { useMemo, useState, useEffect } from 'react' +import { useQuery } from 'react-apollo' +import { ExtensionPoint, useTreePath, useRuntime } from 'vtex.render-runtime' +import { useListContext, ListContextProvider } from 'vtex.list-context' +import { ProductListContext } from 'vtex.product-list-context' +import { Spinner } from 'vtex.styleguide' + +import { mapCatalogProductToProductSummary } from './utils/normalize' +import ProductListEventCaller from './components/ProductListEventCaller' +import productsQuery from './queries/productById.gql' +import ViewLists from './queries/viewLists.gql' +import { getSession } from './modules/session' +import storageFactory from './utils/storage' + +const localStore = storageFactory(() => localStorage) + +let isAuthenticated = + JSON.parse(String(localStore.getItem('wishlist_isAuthenticated'))) ?? false +let shopperId = localStore.getItem('wishlist_shopperId') ?? null + +const useSessionResponse = () => { + const [session, setSession] = useState() + const sessionPromise = getSession() + + useEffect(() => { + if (!sessionPromise) { + return + } + + sessionPromise.then(sessionResponse => { + const { response } = sessionResponse + + setSession(response) + }) + }, [sessionPromise]) + + return session +} + +const ProductSummaryList = ({ children }) => { + const { list } = useListContext() || [] + const { treePath } = useTreePath() + const { navigate, history } = useRuntime() + + const sessionResponse: any = useSessionResponse() + + const { data: dataLists } = useQuery(ViewLists, { + ssr: false, + skip: !isAuthenticated, + fetchPolicy: 'no-cache', + variables: { + shopperId, + }, + }) + + const { data, loading, error } = useQuery(productsQuery, { + ssr: false, + skip: !dataLists || !dataLists.viewLists, + variables: { + ids: dataLists?.viewLists[0].data.map(item => { + return item.productId + }), + }, + }) + + if (sessionResponse) { + isAuthenticated = + sessionResponse?.namespaces?.profile?.isAuthenticated?.value === 'true' + shopperId = sessionResponse?.namespaces?.profile?.email?.value ?? null + + localStore.setItem( + 'wishlist_isAuthenticated', + JSON.stringify(isAuthenticated) + ) + localStore.setItem('wishlist_shopperId', String(shopperId)) + } + + const { productsByIdentifier: products } = data || {} + + const newListContextValue = useMemo(() => { + const componentList = products?.map(product => { + const normalizedProduct = mapCatalogProductToProductSummary(product) + return ( + + ) + }) + return list.concat(componentList) + }, [products, treePath, list]) + + if (sessionResponse && !isAuthenticated) { + navigate({ + page: 'store.login', + query: `returnUrl=${encodeURIComponent(history?.location?.pathname)}`, + }) + } + + if (loading) { + return + } + + if (!data || error) { + return null + } + + return ( + + {children} + + ) +} + +const EnhancedProductList = ({ children }) => { + const { ProductListProvider } = ProductListContext + return ( + + {children} + + + ) +} + +export default EnhancedProductList diff --git a/react/modules/session.ts b/react/modules/session.ts new file mode 100644 index 0000000..fa01c54 --- /dev/null +++ b/react/modules/session.ts @@ -0,0 +1,11 @@ +import { SessionPromise } from 'vtex.render-runtime' + +export function getSession() { + return window && + (window as any).__RENDER_8_SESSION__ && + (window as any).__RENDER_8_SESSION__.sessionPromise + ? ((window as any).__RENDER_8_SESSION__.sessionPromise as Promise< + SessionPromise + >) + : null +} \ No newline at end of file diff --git a/react/package.json b/react/package.json index ce5db85..8279b27 100644 --- a/react/package.json +++ b/react/package.json @@ -24,7 +24,25 @@ "react": "^16.9.2", "react-apollo": "^3.1.3", "tslint-eslint-rules": "^5.4.0", - "typescript": "3.9.7" + "typescript": "3.9.7", + "vtex.add-to-cart-button": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.add-to-cart-button@0.20.1/public/@types/vtex.add-to-cart-button", + "vtex.css-handles": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.css-handles@0.4.4/public/@types/vtex.css-handles", + "vtex.flex-layout": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.flex-layout@0.15.1/public/@types/vtex.flex-layout", + "vtex.list-context": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.list-context@0.1.1/public/@types/vtex.list-context", + "vtex.product-context": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-context@0.9.2/public/@types/vtex.product-context", + "vtex.product-list-context": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-list-context@0.3.0/public/@types/vtex.product-list-context", + "vtex.product-quantity": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-quantity@1.5.0/public/@types/vtex.product-quantity", + "vtex.product-specification-badges": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-specification-badges@0.2.0/public/@types/vtex.product-specification-badges", + "vtex.product-summary": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-summary@2.63.0/public/@types/vtex.product-summary", + "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.124.3/public/@types/vtex.render-runtime", + "vtex.rich-text": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.rich-text@0.11.2/public/@types/vtex.rich-text", + "vtex.search-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.search-graphql@0.36.0/public/_types/react", + "vtex.slider-layout": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.slider-layout@0.15.2/public/@types/vtex.slider-layout", + "vtex.store": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store@2.109.2/public/@types/vtex.store", + "vtex.store-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-graphql@2.136.0/public/@types/vtex.store-graphql", + "vtex.store-icons": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-icons@0.18.0/public/@types/vtex.store-icons", + "vtex.store-resources": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-resources@0.73.0/public/_types/react", + "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.133.1/public/@types/vtex.styleguide" }, "version": "1.0.10" } diff --git a/react/queries/userProfile.gql b/react/queries/userProfile.gql deleted file mode 100644 index b320f6e..0000000 --- a/react/queries/userProfile.gql +++ /dev/null @@ -1,8 +0,0 @@ -query { - getSession @context(provider: "vtex.store-graphql") { - profile { - id - email - } - } -} diff --git a/react/tsconfig.json b/react/tsconfig.json index 287c3a0..ab02918 100644 --- a/react/tsconfig.json +++ b/react/tsconfig.json @@ -3,8 +3,12 @@ "alwaysStrict": true, "esModuleInterop": true, "jsx": "react", - "lib": ["es2017", "dom", "es2018.promise"], - "module": "es6", + "lib": [ + "es2017", + "dom", + "es2018.promise" + ], + "module": "esnext", "moduleResolution": "node", "noImplicitAny": false, "noImplicitReturns": true, @@ -17,12 +21,24 @@ "strictNullChecks": true, "strictPropertyInitialization": true, "target": "es2017", - "typeRoots": ["node_modules/@types"], - "types": ["node", "jest", "graphql"] + "typeRoots": [ + "node_modules/@types" + ], + "types": [ + "node", + "jest", + "graphql" + ] }, - "exclude": ["node_modules"], - "include": ["./typings/*.d.ts", "./**/*.tsx", "./**/*.ts"], + "exclude": [ + "node_modules" + ], + "include": [ + "./typings/*.d.ts", + "./**/*.tsx", + "./**/*.ts" + ], "typeAcquisition": { "enable": false } -} +} \ No newline at end of file diff --git a/react/utils/normalize.js b/react/utils/normalize.js index 1c0e877..b7e733e 100644 --- a/react/utils/normalize.js +++ b/react/utils/normalize.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-restricted-imports import { pathOr } from 'ramda' export const DEFAULT_WIDTH = 'auto' @@ -67,11 +68,11 @@ const defaultImage = { imageUrl: '', imageLabel: '' } const defaultReference = { Value: '' } const defaultSeller = { commertialOffer: { Price: 0, ListPrice: 0 } } -const getPath = (url) => { - return url.replace(/^[a-zA-Z]{3,5}\:\/{2}[a-zA-Z0-9_.:-]+/, '' ) +const getPath = url => { + return url.replace(/^[a-zA-Z]{3,5}:\/{2}[a-zA-Z0-9_.:-]+/, '') } -const getSlug = (url) => { +const getSlug = url => { return getPath(url).split('/')[1] } @@ -80,11 +81,12 @@ const resizeImage = (url, imageSize) => export function mapCatalogProductToProductSummary(product, imageSize = 500) { if (!product) return null - const normalizedProduct = { + const normalizedProduct = { ...product, link: getPath(product.link), - linkText: getSlug(product.link) - } + linkText: getSlug(product.link), + wishlistPage: true, + } const items = normalizedProduct.items || [] const sku = items.find(findAvailableProduct) || items[0] if (sku) { @@ -105,4 +107,4 @@ export function mapCatalogProductToProductSummary(product, imageSize = 500) { } } return normalizedProduct -} \ No newline at end of file +} diff --git a/react/utils/storage.ts b/react/utils/storage.ts new file mode 100644 index 0000000..82537ba --- /dev/null +++ b/react/utils/storage.ts @@ -0,0 +1,74 @@ +export default function storageFactory(getStorage: () => Storage): Storage { + let inMemoryStorage: { [key: string]: string } = {} + + function isSupported() { + try { + const testKey = '__some_random_key_you_are_not_going_to_use__' + getStorage().setItem(testKey, testKey) + getStorage().removeItem(testKey) + return true + } catch (e) { + return false + } + } + + function clear(): void { + if (isSupported()) { + getStorage().clear() + } else { + inMemoryStorage = {} + } + } + + function getItem(name: string): string | null { + if (isSupported()) { + return getStorage().getItem(name) + } + // eslint-disable-next-line no-prototype-builtins + if (inMemoryStorage.hasOwnProperty(name)) { + return inMemoryStorage[name] + } + return null + } + + function key(index: number): string | null { + if (isSupported()) { + return getStorage().key(index) + } + return Object.keys(inMemoryStorage)[index] || null + } + + function removeItem(name: string): void { + if (isSupported()) { + getStorage().removeItem(name) + } else { + delete inMemoryStorage[name] + } + } + + function setItem(name: string, value: string): void { + if (isSupported()) { + getStorage().setItem(name, value) + } else { + inMemoryStorage[name] = String(value) // not everyone uses TypeScript + } + } + + function length(): number { + if (isSupported()) { + return getStorage().length + } + return Object.keys(inMemoryStorage).length + } + + return { + getItem, + setItem, + removeItem, + clear, + key, + get length() { + return length() + }, + } +} diff --git a/react/yarn.lock b/react/yarn.lock index a8cd1e7..ddbafeb 100644 --- a/react/yarn.lock +++ b/react/yarn.lock @@ -5668,6 +5668,78 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +"vtex.add-to-cart-button@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.add-to-cart-button@0.20.1/public/@types/vtex.add-to-cart-button": + version "0.20.1" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.add-to-cart-button@0.20.1/public/@types/vtex.add-to-cart-button#ccc593321b0695fc7d674b9678dd84c515632de3" + +"vtex.css-handles@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.css-handles@0.4.4/public/@types/vtex.css-handles": + version "0.4.4" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.css-handles@0.4.4/public/@types/vtex.css-handles#8c45c6decf9acd2b944e07261686decff93d6422" + +"vtex.flex-layout@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.flex-layout@0.15.1/public/@types/vtex.flex-layout": + version "0.15.1" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.flex-layout@0.15.1/public/@types/vtex.flex-layout#b5e99e063dc79cf86c4a1167383e6661cfbc8e61" + +"vtex.list-context@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.list-context@0.1.1/public/@types/vtex.list-context": + version "0.1.1" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.list-context@0.1.1/public/@types/vtex.list-context#c8ac9fc35b3f82b782562fa96e487ffc5ff02ca6" + +"vtex.product-context@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-context@0.9.2/public/@types/vtex.product-context": + version "0.9.2" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-context@0.9.2/public/@types/vtex.product-context#f4387e4b4ec59cf7e63b8f68202426a279693dd1" + +"vtex.product-list-context@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-list-context@0.3.0/public/@types/vtex.product-list-context": + version "0.3.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-list-context@0.3.0/public/@types/vtex.product-list-context#830570426aa5e0286e2510ac04ddca054522e4fc" + +"vtex.product-quantity@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-quantity@1.5.0/public/@types/vtex.product-quantity": + version "1.5.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-quantity@1.5.0/public/@types/vtex.product-quantity#26411d258d1d62bf2fa87a8b7a07aacd7d1388c4" + +"vtex.product-specification-badges@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-specification-badges@0.2.0/public/@types/vtex.product-specification-badges": + version "0.2.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-specification-badges@0.2.0/public/@types/vtex.product-specification-badges#0548250b4cec8b2005972fd589e3f34b30bf5ea7" + +"vtex.product-summary@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-summary@2.63.0/public/@types/vtex.product-summary": + version "2.63.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-summary@2.63.0/public/@types/vtex.product-summary#e01cb4b31104c55d5494dc9a50b03c714e2920e8" + +"vtex.render-runtime@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.124.3/public/@types/vtex.render-runtime": + version "8.124.3" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.124.3/public/@types/vtex.render-runtime#8158e34bd24b4a51cb5d463eeef4e37af626c2d5" + +"vtex.rich-text@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.rich-text@0.11.2/public/@types/vtex.rich-text": + version "0.11.2" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.rich-text@0.11.2/public/@types/vtex.rich-text#0087d1e1d18adbf054a4f3b12270d2c068f30d00" + +"vtex.search-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.search-graphql@0.36.0/public/_types/react": + version "0.0.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.search-graphql@0.36.0/public/_types/react#fa7a0347e046eab3dd768998fc9252b2c0dd5aef" + +"vtex.slider-layout@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.slider-layout@0.15.2/public/@types/vtex.slider-layout": + version "0.15.2" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.slider-layout@0.15.2/public/@types/vtex.slider-layout#33a7512b606cdbfedda9a64900d97a0ce4fa0279" + +"vtex.store-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-graphql@2.136.0/public/@types/vtex.store-graphql": + version "2.136.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-graphql@2.136.0/public/@types/vtex.store-graphql#7918a2990f0f33d9d53e4d2a35eb741ca03bd485" + +"vtex.store-icons@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-icons@0.18.0/public/@types/vtex.store-icons": + version "0.18.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-icons@0.18.0/public/@types/vtex.store-icons#0ee94d549aa283ce3a13ab987c13eac4fdfd1bba" + +"vtex.store-resources@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-resources@0.73.0/public/_types/react": + version "0.0.0" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store-resources@0.73.0/public/_types/react#fa7a0347e046eab3dd768998fc9252b2c0dd5aef" + +"vtex.store@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store@2.109.2/public/@types/vtex.store": + version "2.109.2" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.store@2.109.2/public/@types/vtex.store#2733a4c11767a17432447b57508eaa8a0b747c4a" + +"vtex.styleguide@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.133.1/public/@types/vtex.styleguide": + version "9.133.1" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.133.1/public/@types/vtex.styleguide#d81b246a0942248f0fc1a71ad9fe20928e6ddb5f" + w3c-hr-time@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"