diff --git a/framework/bigcommerce/product/get-product.ts b/framework/bigcommerce/product/get-product.ts index 7d77eb194..794d89bdf 100644 --- a/framework/bigcommerce/product/get-product.ts +++ b/framework/bigcommerce/product/get-product.ts @@ -2,7 +2,7 @@ import type { GetProductQuery, GetProductQueryVariables } from '../schema' import setProductLocaleMeta from '../api/utils/set-product-locale-meta' import { productInfoFragment } from '../api/fragments/product' import { BigcommerceConfig, getConfig } from '../api' -import { normalizeProduct } from '@framework/lib/normalize' +import { normalizeProduct } from '@framework/utils/normalize' import type { Product } from '@commerce/types' export const getProductQuery = /* GraphQL */ ` diff --git a/framework/shopify/cart/use-add-item.tsx b/framework/shopify/cart/use-add-item.tsx index b5b34adc3..162627057 100644 --- a/framework/shopify/cart/use-add-item.tsx +++ b/framework/shopify/cart/use-add-item.tsx @@ -1,13 +1,16 @@ import { useCallback } from 'react' import useCart from './use-cart' + import useCartAddItem, { AddItemInput as UseAddItemInput, } from '@commerce/cart/use-add-item' + import type { HookFetcher } from '@commerce/utils/types' import type { Cart } from '@commerce/types' -import checkoutLineItemAddMutation from '../utils/mutations/checkout-line-item-add' -import getCheckoutId from '@framework/utils/get-checkout-id' + +import { checkoutLineItemAddMutation, getCheckoutId } from '@framework/utils' import { checkoutToCart } from './utils' + import { AddCartItemBody, CartItemBody } from '@framework/types' import { MutationCheckoutLineItemsAddArgs } from '@framework/schema' diff --git a/framework/shopify/cart/use-cart.tsx b/framework/shopify/cart/use-cart.tsx index e5ab8cafb..f5749731f 100644 --- a/framework/shopify/cart/use-cart.tsx +++ b/framework/shopify/cart/use-cart.tsx @@ -1,4 +1,44 @@ -import useCommerceCart, { UseCart } from '@commerce/cart/use-cart' +import { useMemo } from 'react' import type { ShopifyProvider } from '..' +import useCommerceCart, { + FetchCartInput, + UseCart, +} from '@commerce/cart/use-cart' + +import { Cart } from '@commerce/types' +import { HookHandler } from '@commerce/utils/types' + +import fetcher from './utils/fetcher' +import getCheckoutQuery from '@framework/utils/queries/get-checkout-query' + export default useCommerceCart as UseCart + +export const handler: HookHandler< + Cart | null, + {}, + FetchCartInput, + { isEmpty?: boolean } +> = { + fetchOptions: { + query: getCheckoutQuery, + }, + fetcher, + useHook({ input, useData }) { + const response = useData({ + swrOptions: { revalidateOnFocus: false, ...input.swrOptions }, + }) + return useMemo( + () => + Object.create(response, { + isEmpty: { + get() { + return (response.data?.lineItems.length ?? 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, +} diff --git a/framework/shopify/cart/utils/checkout-to-cart.ts b/framework/shopify/cart/utils/checkout-to-cart.ts index 4c64d23e6..a8e91fbe1 100644 --- a/framework/shopify/cart/utils/checkout-to-cart.ts +++ b/framework/shopify/cart/utils/checkout-to-cart.ts @@ -1,6 +1,6 @@ import { Cart } from '@commerce/types' import { CommerceError, ValidationError } from '@commerce/utils/errors' -import { normalizeCart } from '@framework/lib/normalize' +import { normalizeCart } from '@framework/utils/normalize' import { Checkout, Maybe, UserError } from '@framework/schema' const checkoutToCart = (checkoutResponse?: { diff --git a/framework/shopify/cart/utils/fetcher.ts b/framework/shopify/cart/utils/fetcher.ts new file mode 100644 index 000000000..6621b7fde --- /dev/null +++ b/framework/shopify/cart/utils/fetcher.ts @@ -0,0 +1,30 @@ +import { HookFetcherFn } from '@commerce/utils/types' +import { Cart } from '@commerce/types' +import { checkoutCreate, checkoutToCart } from '.' +import { FetchCartInput } from '@commerce/cart/use-cart' + +const fetcher: HookFetcherFn = async ({ + options, + input: { cartId }, + fetch, +}) => { + let checkout + + if (cartId) { + const data = await fetch({ + ...options, + variables: { + cartId, + }, + }) + checkout = data?.node + } + + if (checkout?.completedAt || !cartId) { + checkout = await checkoutCreate(fetch) + } + + return checkoutToCart({ checkout }) +} + +export default fetcher diff --git a/framework/shopify/cart/utils/index.ts b/framework/shopify/cart/utils/index.ts index 20d04955d..0f2b4a6ca 100644 --- a/framework/shopify/cart/utils/index.ts +++ b/framework/shopify/cart/utils/index.ts @@ -1,2 +1,3 @@ export { default as checkoutToCart } from './checkout-to-cart' export { default as checkoutCreate } from './checkout-create' +export { default as fetcher } from './fetcher' diff --git a/framework/shopify/customer/use-customer.tsx b/framework/shopify/customer/use-customer.tsx index 652188bbe..55151801b 100644 --- a/framework/shopify/customer/use-customer.tsx +++ b/framework/shopify/customer/use-customer.tsx @@ -1,4 +1,25 @@ import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' +import { Customer } from '@commerce/types' +import { HookHandler } from '@commerce/utils/types' +import { getCustomerQuery } from '@framework/utils' import type { ShopifyProvider } from '..' export default useCustomer as UseCustomer + +export const handler: HookHandler = { + fetchOptions: { + query: getCustomerQuery, + }, + async fetcher({ options, fetch }) { + const data = await fetch(options) + return data?.customer ?? null + }, + useHook({ input, useData }) { + return useData({ + swrOptions: { + revalidateOnFocus: false, + ...input.swrOptions, + }, + }) + }, +} diff --git a/framework/shopify/fetcher.ts b/framework/shopify/fetcher.ts new file mode 100644 index 000000000..9c4fe9a9e --- /dev/null +++ b/framework/shopify/fetcher.ts @@ -0,0 +1,18 @@ +import { Fetcher } from '@commerce/utils/types' +import { API_TOKEN, API_URL } from './const' +import { handleFetchResponse } from './utils' + +const fetcher: Fetcher = async ({ method = 'POST', variables, query }) => { + return handleFetchResponse( + await fetch(API_URL, { + method, + body: JSON.stringify({ query, variables }), + headers: { + 'X-Shopify-Storefront-Access-Token': API_TOKEN!, + 'Content-Type': 'application/json', + }, + }) + ) +} + +export default fetcher diff --git a/framework/shopify/product/get-all-products.ts b/framework/shopify/product/get-all-products.ts index a7cc3043c..34480e90a 100644 --- a/framework/shopify/product/get-all-products.ts +++ b/framework/shopify/product/get-all-products.ts @@ -2,7 +2,7 @@ import { GraphQLFetcherResult } from '@commerce/api' import { getConfig, ShopifyConfig } from '../api' import { Product, ProductEdge } from '../schema' import { getAllProductsQuery } from '../utils/queries' -import { normalizeProduct } from '@framework/lib/normalize' +import { normalizeProduct } from '@framework/utils/normalize' export type ProductNode = Product diff --git a/framework/shopify/product/get-product.ts b/framework/shopify/product/get-product.ts index 84e74c611..191706123 100644 --- a/framework/shopify/product/get-product.ts +++ b/framework/shopify/product/get-product.ts @@ -3,7 +3,7 @@ import { GraphQLFetcherResult } from '@commerce/api' import { getConfig, ShopifyConfig } from '../api' import { Product } from '../schema' import getProductQuery from '../utils/queries/get-product-query' -import { normalizeProduct } from '@framework/lib/normalize' +import { normalizeProduct } from '@framework/utils/normalize' export type ProductNode = Product diff --git a/framework/shopify/product/use-search.tsx b/framework/shopify/product/use-search.tsx index 4f83baa63..174466fdb 100644 --- a/framework/shopify/product/use-search.tsx +++ b/framework/shopify/product/use-search.tsx @@ -1,4 +1,55 @@ import useSearch, { UseSearch } from '@commerce/products/use-search' +import { SearchProductsData } from '@commerce/types' +import { HookHandler } from '@commerce/utils/types' +import { ProductEdge } from '@framework/schema' +import { + getAllProductsQuery, + getSearchVariables, + normalizeProduct, +} from '@framework/utils' import type { ShopifyProvider } from '..' export default useSearch as UseSearch + +export type SearchProductsInput = { + search?: string + categoryId?: number + brandId?: number + sort?: string +} + +export const handler: HookHandler< + SearchProductsData, + SearchProductsInput, + SearchProductsInput +> = { + fetchOptions: { + query: getAllProductsQuery, + }, + async fetcher({ input, options, fetch }) { + const resp = await fetch({ + query: options?.query, + method: options?.method, + variables: getSearchVariables(input), + }) + const edges = resp.products?.edges + return { + products: edges?.map(({ node: p }: ProductEdge) => normalizeProduct(p)), + found: !!edges?.length, + } + }, + useHook({ input, useData }) { + return useData({ + input: [ + ['search', input.search], + ['categoryId', input.categoryId], + ['brandId', input.brandId], + ['sort', input.sort], + ], + swrOptions: { + revalidateOnFocus: false, + ...input.swrOptions, + }, + }) + }, +} diff --git a/framework/shopify/provider.ts b/framework/shopify/provider.ts new file mode 100644 index 000000000..6da831e46 --- /dev/null +++ b/framework/shopify/provider.ts @@ -0,0 +1,18 @@ +import { SHOPIFY_CHECKOUT_ID_COOKIE, STORE_DOMAIN } from './const' + +import { handler as useCart } from '@framework/cart/use-cart' +import { handler as useSearch } from '@framework/product/use-search' +import { handler as useCustomer } from '@framework/customer/use-customer' +import fetcher from './fetcher' + +export const shopifyProvider = { + locale: 'en-us', + cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE, + storeDomain: STORE_DOMAIN, + fetcher, + cart: { useCart }, + customer: { useCustomer }, + products: { useSearch }, +} + +export type ShopifyProvider = typeof shopifyProvider diff --git a/framework/shopify/provider.tsx b/framework/shopify/provider.tsx deleted file mode 100644 index 4a1f01525..000000000 --- a/framework/shopify/provider.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { useMemo } from 'react' -import { Fetcher, HookFetcherFn, HookHandler } from '@commerce/utils/types' - -import { - API_TOKEN, - API_URL, - SHOPIFY_CHECKOUT_ID_COOKIE, - STORE_DOMAIN, -} from './const' - -import { Cart } from './types' -import { Customer } from '@commerce/types' -import { normalizeCart, normalizeProduct } from './lib/normalize' -import { FetchCartInput } from '@commerce/cart/use-cart' -import { checkoutCreate, checkoutToCart } from './cart/utils' - -import { - getAllProductsQuery, - getCustomerQuery, - getCheckoutQuery, - handleFetchResponse, - getSearchVariables, -} from './utils' - -import { ProductEdge } from './schema' -import { SearchProductsInput } from 'framework/bigcommerce/provider' -import { SearchProductsData } from 'framework/bigcommerce/api/catalog/products' - -const fetcher: Fetcher = async ({ method = 'POST', variables, query }) => { - return handleFetchResponse( - await fetch(API_URL, { - method, - body: JSON.stringify({ query, variables }), - headers: { - 'X-Shopify-Storefront-Access-Token': API_TOKEN!, - 'Content-Type': 'application/json', - }, - }) - ) -} - -export const cartFetcher: HookFetcherFn = async ({ - options, - input: { cartId }, - fetch, -}) => { - let checkout - - if (cartId) { - const data = await fetch({ - ...options, - variables: { - cartId, - }, - }) - checkout = data?.node - } - - if (checkout?.completedAt || !cartId) { - checkout = await checkoutCreate(fetch) - } - - return checkoutToCart({ checkout }) -} - -const useCart: HookHandler< - Cart | null, - {}, - FetchCartInput, - { isEmpty?: boolean } -> = { - fetchOptions: { - query: getCheckoutQuery, - }, - fetcher: cartFetcher, - useHook({ input, useData }) { - const response = useData({ - swrOptions: { revalidateOnFocus: false, ...input.swrOptions }, - }) - - return useMemo( - () => - Object.create(response, { - isEmpty: { - get() { - return (response.data?.lineItems.length ?? 0) <= 0 - }, - enumerable: true, - }, - }), - [response] - ) - }, -} - -const useSearch: HookHandler< - SearchProductsData, - SearchProductsInput, - SearchProductsInput -> = { - fetchOptions: { - query: getAllProductsQuery, - }, - async fetcher({ input, options, fetch }) { - const resp = await fetch({ - query: options?.query, - method: options?.method, - variables: getSearchVariables(input), - }) - const edges = resp.products?.edges - return { - products: edges?.map(({ node: p }: ProductEdge) => normalizeProduct(p)), - found: !!edges?.length, - } - }, - useHook({ input, useData }) { - return useData({ - input: [ - ['search', input.search], - ['categoryId', input.categoryId], - ['brandId', input.brandId], - ['sort', input.sort], - ], - swrOptions: { - revalidateOnFocus: false, - ...input.swrOptions, - }, - }) - }, -} - -const useCustomerHandler: HookHandler = { - fetchOptions: { - query: getCustomerQuery, - }, - async fetcher({ options, fetch }) { - const data = await fetch(options) - return data?.customer ?? null - }, - useHook({ input, useData }) { - return useData({ - swrOptions: { - revalidateOnFocus: false, - ...input.swrOptions, - }, - }) - }, -} - -export const shopifyProvider = { - locale: 'en-us', - cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE, - storeDomain: STORE_DOMAIN, - fetcher, - cartNormalizer: normalizeCart, - cart: { useCart }, - customer: { useCustomer: useCustomerHandler }, - products: { useSearch }, -} - -export type ShopifyProvider = typeof shopifyProvider diff --git a/framework/shopify/utils/get-search-variables.ts b/framework/shopify/utils/get-search-variables.ts index 9bc91eca3..90d35ba50 100644 --- a/framework/shopify/utils/get-search-variables.ts +++ b/framework/shopify/utils/get-search-variables.ts @@ -1,5 +1,5 @@ -import { SearchProductsInput } from '@framework/product/use-search' import getSortVariables from './get-sort-variables' +import type { SearchProductsInput } from '@framework/product/use-search' export const getSearchVariables = ({ categoryId, diff --git a/framework/shopify/utils/index.ts b/framework/shopify/utils/index.ts index cecf06fd1..99aa9be68 100644 --- a/framework/shopify/utils/index.ts +++ b/framework/shopify/utils/index.ts @@ -3,7 +3,9 @@ export { default as getSearchVariables } from './get-search-variables' export { default as getSortVariables } from './get-sort-variables' export { default as getVendors } from './get-vendors' export { default as getCategories } from './get-categories' +export { default as getCheckoutId } from './get-checkout-id' -export * from './customer-token' export * from './queries' export * from './mutations' +export * from './normalize' +export * from './customer-token' diff --git a/framework/shopify/utils/mutations/index.ts b/framework/shopify/utils/mutations/index.ts index 5ccf5b1dd..c9c6ee100 100644 --- a/framework/shopify/utils/mutations/index.ts +++ b/framework/shopify/utils/mutations/index.ts @@ -1,8 +1,10 @@ export { default as createCustomerMutation } from './customer-create' export { default as checkoutCreateMutation } from './checkout-create' + export { default as checkoutLineItemAddMutation } from './checkout-line-item-add' export { default as checkoutLineItemUpdateMutation } from './checkout-create' export { default as checkoutLineItemRemoveMutation } from './checkout-line-item-remove' + export { default as customerCreateMutation } from './customer-create' export { default as customerAccessTokenCreateMutation } from './customer-access-token-create' export { default as customerAccessTokenDeleteMutation } from './customer-access-token-delete' diff --git a/framework/shopify/lib/normalize.ts b/framework/shopify/utils/normalize.ts similarity index 100% rename from framework/shopify/lib/normalize.ts rename to framework/shopify/utils/normalize.ts