diff --git a/components/cart/CartItem/CartItem.tsx b/components/cart/CartItem/CartItem.tsx index e6820d32c9..bc614722f5 100644 --- a/components/cart/CartItem/CartItem.tsx +++ b/components/cart/CartItem/CartItem.tsx @@ -5,7 +5,7 @@ import Link from 'next/link' import s from './CartItem.module.css' import { Trash, Plus, Minus } from '@components/icons' import { useUI } from '@components/ui/context' -import type { LineItem } from '@framework/types' +import type { LineItem } from '@commerce/types/cart' import usePrice from '@framework/product/use-price' import useUpdateItem from '@framework/cart/use-update-item' import useRemoveItem from '@framework/cart/use-remove-item' @@ -35,7 +35,7 @@ const CartItem = ({ const updateItem = useUpdateItem({ item }) const removeItem = useRemoveItem() - const [quantity, setQuantity] = useState(item.quantity) + const [quantity, setQuantity] = useState(item.quantity) const [removing, setRemoving] = useState(false) const updateQuantity = async (val: number) => { @@ -43,10 +43,10 @@ const CartItem = ({ } const handleQuantity = (e: ChangeEvent) => { - const val = Number(e.target.value) + const val = !e.target.value ? '' : Number(e.target.value) - if (Number.isInteger(val) && val >= 0) { - setQuantity(Number(e.target.value)) + if (!val || (Number.isInteger(val) && val >= 0)) { + setQuantity(val) } } const handleBlur = () => { diff --git a/components/common/Footer/Footer.tsx b/components/common/Footer/Footer.tsx index 5fb9ede584..4190815ec2 100644 --- a/components/common/Footer/Footer.tsx +++ b/components/common/Footer/Footer.tsx @@ -2,7 +2,7 @@ import { FC } from 'react' import cn from 'classnames' import Link from 'next/link' import { useRouter } from 'next/router' -import type { Page } from '@framework/common/get-all-pages' +import type { Page } from '@commerce/types/page' import getSlug from '@lib/get-slug' import { Github, Vercel } from '@components/icons' import { Logo, Container } from '@components/ui' diff --git a/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx b/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx index 423048f752..8d8ea1adff 100644 --- a/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx +++ b/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx @@ -1,6 +1,6 @@ import { FC } from 'react' import Link from 'next/link' -import type { Product } from '@commerce/types' +import type { Product } from '@commerce/types/product' import { Grid } from '@components/ui' import { ProductCard } from '@components/product' import s from './HomeAllProductsGrid.module.css' diff --git a/components/common/Layout/Layout.tsx b/components/common/Layout/Layout.tsx index 1e1a22967e..c5de5739a3 100644 --- a/components/common/Layout/Layout.tsx +++ b/components/common/Layout/Layout.tsx @@ -1,16 +1,17 @@ -import cn from 'classnames' -import dynamic from 'next/dynamic' -import s from './Layout.module.css' -import { useRouter } from 'next/router' import React, { FC } from 'react' +import { useRouter } from 'next/router' +import dynamic from 'next/dynamic' +import cn from 'classnames' +import type { Page } from '@commerce/types/page' +import type { Category } from '@commerce/types/site' +import { CommerceProvider } from '@framework' +import { useAcceptCookies } from '@lib/hooks/useAcceptCookies' import { useUI } from '@components/ui/context' import { Navbar, Footer } from '@components/common' -import { useAcceptCookies } from '@lib/hooks/useAcceptCookies' import { Sidebar, Button, Modal, LoadingDots } from '@components/ui' import CartSidebarView from '@components/cart/CartSidebarView' -import type { Page, Category } from '@commerce/types' import LoginView from '@components/auth/LoginView' -import { CommerceProvider } from '@framework' +import s from './Layout.module.css' const Loading = () => (
diff --git a/components/common/Navbar/Navbar.tsx b/components/common/Navbar/Navbar.tsx index aa49993f35..16088485f1 100644 --- a/components/common/Navbar/Navbar.tsx +++ b/components/common/Navbar/Navbar.tsx @@ -27,13 +27,11 @@ const Navbar: FC = ({ links }) => ( All - {links - ? links.map((l) => ( - - {l.label} - - )) - : null} + {links?.map((l) => ( + + {l.label} + + ))}
diff --git a/components/common/UserNav/UserNav.tsx b/components/common/UserNav/UserNav.tsx index 4d00970a92..83422a8cfd 100644 --- a/components/common/UserNav/UserNav.tsx +++ b/components/common/UserNav/UserNav.tsx @@ -1,7 +1,7 @@ import { FC } from 'react' import Link from 'next/link' import cn from 'classnames' -import type { LineItem } from '@framework/types' +import type { LineItem } from '@commerce/types/cart' import useCart from '@framework/cart/use-cart' import useCustomer from '@framework/customer/use-customer' import { Avatar } from '@components/common' diff --git a/components/product/ProductCard/ProductCard.tsx b/components/product/ProductCard/ProductCard.tsx index 45a19d2ddf..c2e2103671 100644 --- a/components/product/ProductCard/ProductCard.tsx +++ b/components/product/ProductCard/ProductCard.tsx @@ -1,7 +1,7 @@ import { FC } from 'react' import cn from 'classnames' import Link from 'next/link' -import type { Product } from '@commerce/types' +import type { Product } from '@commerce/types/product' import s from './ProductCard.module.css' import Image, { ImageProps } from 'next/image' import WishlistButton from '@components/wishlist/WishlistButton' diff --git a/components/product/ProductView/ProductView.tsx b/components/product/ProductView/ProductView.tsx index 3b09fa39a5..a9385b2f0b 100644 --- a/components/product/ProductView/ProductView.tsx +++ b/components/product/ProductView/ProductView.tsx @@ -5,7 +5,7 @@ import { FC, useEffect, useState } from 'react' import s from './ProductView.module.css' import { Swatch, ProductSlider } from '@components/product' import { Button, Container, Text, useUI } from '@components/ui' -import type { Product } from '@commerce/types' +import type { Product } from '@commerce/types/product' import usePrice from '@framework/product/use-price' import { useAddItem } from '@framework/cart' import { getVariant, SelectedOptions } from '../helpers' @@ -18,6 +18,8 @@ interface Props { } const ProductView: FC = ({ product }) => { + // TODO: fix this missing argument issue + /* @ts-ignore */ const addItem = useAddItem() const { price } = usePrice({ amount: product.price.value, @@ -146,8 +148,11 @@ const ProductView: FC = ({ product }) => { className={s.button} onClick={addToCart} loading={loading} + disabled={variant?.availableForSale === false} > - Add to Cart + {variant?.availableForSale === false + ? 'Not Available' + : 'Add To Cart'} diff --git a/components/product/Swatch/Swatch.tsx b/components/product/Swatch/Swatch.tsx index 7e8de3e4ac..bb8abf3d14 100644 --- a/components/product/Swatch/Swatch.tsx +++ b/components/product/Swatch/Swatch.tsx @@ -44,14 +44,15 @@ const Swatch: FC & SwatchProps> = ({ className={swatchClassName} style={color ? { backgroundColor: color } : {}} aria-label="Variant Swatch" + {...(label && color && { title: label })} {...props} > - {variant === 'color' && active && ( + {color && active && ( )} - {variant !== 'color' ? label : null} + {!color ? label : null} ) } diff --git a/components/product/helpers.ts b/components/product/helpers.ts index a0ceb7aa59..19ec2a1710 100644 --- a/components/product/helpers.ts +++ b/components/product/helpers.ts @@ -1,4 +1,4 @@ -import type { Product } from '@commerce/types' +import type { Product } from '@commerce/types/product' export type SelectedOptions = Record export function getVariant(product: Product, opts: SelectedOptions) { diff --git a/components/wishlist/WishlistButton/WishlistButton.tsx b/components/wishlist/WishlistButton/WishlistButton.tsx index 6dc59b9000..5841de11ee 100644 --- a/components/wishlist/WishlistButton/WishlistButton.tsx +++ b/components/wishlist/WishlistButton/WishlistButton.tsx @@ -6,7 +6,7 @@ import useAddItem from '@framework/wishlist/use-add-item' import useCustomer from '@framework/customer/use-customer' import useWishlist from '@framework/wishlist/use-wishlist' import useRemoveItem from '@framework/wishlist/use-remove-item' -import type { Product, ProductVariant } from '@commerce/types' +import type { Product, ProductVariant } from '@commerce/types/product' type Props = { productId: Product['id'] diff --git a/components/wishlist/WishlistCard/WishlistCard.tsx b/components/wishlist/WishlistCard/WishlistCard.tsx index a303041323..dfc1165c2a 100644 --- a/components/wishlist/WishlistCard/WishlistCard.tsx +++ b/components/wishlist/WishlistCard/WishlistCard.tsx @@ -7,7 +7,7 @@ import { Trash } from '@components/icons' import { Button, Text } from '@components/ui' import { useUI } from '@components/ui/context' -import type { Product } from '@commerce/types' +import type { Product } from '@commerce/types/product' import usePrice from '@framework/product/use-price' import useAddItem from '@framework/cart/use-add-item' import useRemoveItem from '@framework/wishlist/use-remove-item' @@ -20,14 +20,17 @@ const placeholderImg = '/product-img-placeholder.svg' const WishlistCard: FC = ({ product }) => { const { price } = usePrice({ - amount: product.prices?.price?.value, - baseAmount: product.prices?.retailPrice?.value, - currencyCode: product.prices?.price?.currencyCode!, + amount: product.price?.value, + baseAmount: product.price?.retailPrice, + currencyCode: product.price?.currencyCode!, }) // @ts-ignore Wishlist is not always enabled const removeItem = useRemoveItem({ wishlist: { includeProducts: true } }) const [loading, setLoading] = useState(false) const [removing, setRemoving] = useState(false) + + // TODO: fix this missing argument issue + /* @ts-ignore */ const addItem = useAddItem() const { openSidebar } = useUI() diff --git a/framework/bigcommerce/api/cart/index.ts b/framework/bigcommerce/api/cart/index.ts deleted file mode 100644 index 4ee6688950..0000000000 --- a/framework/bigcommerce/api/cart/index.ts +++ /dev/null @@ -1,78 +0,0 @@ -import isAllowedMethod from '../utils/is-allowed-method' -import createApiHandler, { - BigcommerceApiHandler, - BigcommerceHandler, -} from '../utils/create-api-handler' -import { BigcommerceApiError } from '../utils/errors' -import getCart from './handlers/get-cart' -import addItem from './handlers/add-item' -import updateItem from './handlers/update-item' -import removeItem from './handlers/remove-item' -import type { - BigcommerceCart, - GetCartHandlerBody, - AddCartItemHandlerBody, - UpdateCartItemHandlerBody, - RemoveCartItemHandlerBody, -} from '../../types' - -export type CartHandlers = { - getCart: BigcommerceHandler - addItem: BigcommerceHandler - updateItem: BigcommerceHandler - removeItem: BigcommerceHandler -} - -const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] - -// TODO: a complete implementation should have schema validation for `req.body` -const cartApi: BigcommerceApiHandler = async ( - req, - res, - config, - handlers -) => { - if (!isAllowedMethod(req, res, METHODS)) return - - const { cookies } = req - const cartId = cookies[config.cartCookie] - - try { - // Return current cart info - if (req.method === 'GET') { - const body = { cartId } - return await handlers['getCart']({ req, res, config, body }) - } - - // Create or add an item to the cart - if (req.method === 'POST') { - const body = { ...req.body, cartId } - return await handlers['addItem']({ req, res, config, body }) - } - - // Update item in cart - if (req.method === 'PUT') { - const body = { ...req.body, cartId } - return await handlers['updateItem']({ req, res, config, body }) - } - - // Remove an item from the cart - if (req.method === 'DELETE') { - const body = { ...req.body, cartId } - return await handlers['removeItem']({ req, res, config, body }) - } - } catch (error) { - console.error(error) - - const message = - error instanceof BigcommerceApiError - ? 'An unexpected error ocurred with the Bigcommerce API' - : 'An unexpected error ocurred' - - res.status(500).json({ data: null, errors: [{ message }] }) - } -} - -export const handlers = { getCart, addItem, updateItem, removeItem } - -export default createApiHandler(cartApi, handlers, {}) diff --git a/framework/bigcommerce/api/catalog/products.ts b/framework/bigcommerce/api/catalog/products.ts deleted file mode 100644 index d52b2de7e3..0000000000 --- a/framework/bigcommerce/api/catalog/products.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Product } from '@commerce/types' -import isAllowedMethod from '../utils/is-allowed-method' -import createApiHandler, { - BigcommerceApiHandler, - BigcommerceHandler, -} from '../utils/create-api-handler' -import { BigcommerceApiError } from '../utils/errors' -import getProducts from './handlers/get-products' - -export type SearchProductsData = { - products: Product[] - found: boolean -} - -export type ProductsHandlers = { - getProducts: BigcommerceHandler< - SearchProductsData, - { search?: string; category?: string; brand?: string; sort?: string } - > -} - -const METHODS = ['GET'] - -// TODO(lf): a complete implementation should have schema validation for `req.body` -const productsApi: BigcommerceApiHandler< - SearchProductsData, - ProductsHandlers -> = async (req, res, config, handlers) => { - if (!isAllowedMethod(req, res, METHODS)) return - - try { - const body = req.query - return await handlers['getProducts']({ req, res, config, body }) - } catch (error) { - console.error(error) - - const message = - error instanceof BigcommerceApiError - ? 'An unexpected error ocurred with the Bigcommerce API' - : 'An unexpected error ocurred' - - res.status(500).json({ data: null, errors: [{ message }] }) - } -} - -export const handlers = { getProducts } - -export default createApiHandler(productsApi, handlers, {}) diff --git a/framework/bigcommerce/api/checkout.ts b/framework/bigcommerce/api/checkout.ts deleted file mode 100644 index 530f4c40a0..0000000000 --- a/framework/bigcommerce/api/checkout.ts +++ /dev/null @@ -1,77 +0,0 @@ -import isAllowedMethod from './utils/is-allowed-method' -import createApiHandler, { - BigcommerceApiHandler, -} from './utils/create-api-handler' -import { BigcommerceApiError } from './utils/errors' - -const METHODS = ['GET'] -const fullCheckout = true - -// TODO: a complete implementation should have schema validation for `req.body` -const checkoutApi: BigcommerceApiHandler = async (req, res, config) => { - if (!isAllowedMethod(req, res, METHODS)) return - - const { cookies } = req - const cartId = cookies[config.cartCookie] - - try { - if (!cartId) { - res.redirect('/cart') - return - } - - const { data } = await config.storeApiFetch( - `/v3/carts/${cartId}/redirect_urls`, - { - method: 'POST', - } - ) - - if (fullCheckout) { - res.redirect(data.checkout_url) - return - } - - // TODO: make the embedded checkout work too! - const html = ` - - - - - - Checkout - - - - -
- - - ` - - res.status(200) - res.setHeader('Content-Type', 'text/html') - res.write(html) - res.end() - } catch (error) { - console.error(error) - - const message = - error instanceof BigcommerceApiError - ? 'An unexpected error ocurred with the Bigcommerce API' - : 'An unexpected error ocurred' - - res.status(500).json({ data: null, errors: [{ message }] }) - } -} - -export default createApiHandler(checkoutApi, {}, {}) diff --git a/framework/bigcommerce/api/customers/index.ts b/framework/bigcommerce/api/customers/index.ts deleted file mode 100644 index 5af4d1d1d7..0000000000 --- a/framework/bigcommerce/api/customers/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import createApiHandler, { - BigcommerceApiHandler, - BigcommerceHandler, -} from '../utils/create-api-handler' -import isAllowedMethod from '../utils/is-allowed-method' -import { BigcommerceApiError } from '../utils/errors' -import getLoggedInCustomer, { - Customer, -} from './handlers/get-logged-in-customer' - -export type { Customer } - -export type CustomerData = { - customer: Customer -} - -export type CustomersHandlers = { - getLoggedInCustomer: BigcommerceHandler -} - -const METHODS = ['GET'] - -const customersApi: BigcommerceApiHandler< - CustomerData, - CustomersHandlers -> = async (req, res, config, handlers) => { - if (!isAllowedMethod(req, res, METHODS)) return - - try { - const body = null - return await handlers['getLoggedInCustomer']({ req, res, config, body }) - } catch (error) { - console.error(error) - - const message = - error instanceof BigcommerceApiError - ? 'An unexpected error ocurred with the Bigcommerce API' - : 'An unexpected error ocurred' - - res.status(500).json({ data: null, errors: [{ message }] }) - } -} - -const handlers = { getLoggedInCustomer } - -export default createApiHandler(customersApi, handlers, {}) diff --git a/framework/bigcommerce/api/customers/login.ts b/framework/bigcommerce/api/customers/login.ts deleted file mode 100644 index e8f24a92dc..0000000000 --- a/framework/bigcommerce/api/customers/login.ts +++ /dev/null @@ -1,45 +0,0 @@ -import createApiHandler, { - BigcommerceApiHandler, - BigcommerceHandler, -} from '../utils/create-api-handler' -import isAllowedMethod from '../utils/is-allowed-method' -import { BigcommerceApiError } from '../utils/errors' -import login from './handlers/login' - -export type LoginBody = { - email: string - password: string -} - -export type LoginHandlers = { - login: BigcommerceHandler> -} - -const METHODS = ['POST'] - -const loginApi: BigcommerceApiHandler = async ( - req, - res, - config, - handlers -) => { - if (!isAllowedMethod(req, res, METHODS)) return - - try { - const body = req.body ?? {} - return await handlers['login']({ req, res, config, body }) - } catch (error) { - console.error(error) - - const message = - error instanceof BigcommerceApiError - ? 'An unexpected error ocurred with the Bigcommerce API' - : 'An unexpected error ocurred' - - res.status(500).json({ data: null, errors: [{ message }] }) - } -} - -const handlers = { login } - -export default createApiHandler(loginApi, handlers, {}) diff --git a/framework/bigcommerce/api/customers/logout.ts b/framework/bigcommerce/api/customers/logout.ts deleted file mode 100644 index 0a49652451..0000000000 --- a/framework/bigcommerce/api/customers/logout.ts +++ /dev/null @@ -1,42 +0,0 @@ -import createApiHandler, { - BigcommerceApiHandler, - BigcommerceHandler, -} from '../utils/create-api-handler' -import isAllowedMethod from '../utils/is-allowed-method' -import { BigcommerceApiError } from '../utils/errors' -import logout from './handlers/logout' - -export type LogoutHandlers = { - logout: BigcommerceHandler -} - -const METHODS = ['GET'] - -const logoutApi: BigcommerceApiHandler = async ( - req, - res, - config, - handlers -) => { - if (!isAllowedMethod(req, res, METHODS)) return - - try { - const redirectTo = req.query.redirect_to - const body = typeof redirectTo === 'string' ? { redirectTo } : {} - - return await handlers['logout']({ req, res, config, body }) - } catch (error) { - console.error(error) - - const message = - error instanceof BigcommerceApiError - ? 'An unexpected error ocurred with the Bigcommerce API' - : 'An unexpected error ocurred' - - res.status(500).json({ data: null, errors: [{ message }] }) - } -} - -const handlers = { logout } - -export default createApiHandler(logoutApi, handlers, {}) diff --git a/framework/bigcommerce/api/customers/signup.ts b/framework/bigcommerce/api/customers/signup.ts deleted file mode 100644 index aa26f78cff..0000000000 --- a/framework/bigcommerce/api/customers/signup.ts +++ /dev/null @@ -1,50 +0,0 @@ -import createApiHandler, { - BigcommerceApiHandler, - BigcommerceHandler, -} from '../utils/create-api-handler' -import isAllowedMethod from '../utils/is-allowed-method' -import { BigcommerceApiError } from '../utils/errors' -import signup from './handlers/signup' - -export type SignupBody = { - firstName: string - lastName: string - email: string - password: string -} - -export type SignupHandlers = { - signup: BigcommerceHandler> -} - -const METHODS = ['POST'] - -const signupApi: BigcommerceApiHandler = async ( - req, - res, - config, - handlers -) => { - if (!isAllowedMethod(req, res, METHODS)) return - - const { cookies } = req - const cartId = cookies[config.cartCookie] - - try { - const body = { ...req.body, cartId } - return await handlers['signup']({ req, res, config, body }) - } catch (error) { - console.error(error) - - const message = - error instanceof BigcommerceApiError - ? 'An unexpected error ocurred with the Bigcommerce API' - : 'An unexpected error ocurred' - - res.status(500).json({ data: null, errors: [{ message }] }) - } -} - -const handlers = { signup } - -export default createApiHandler(signupApi, handlers, {}) diff --git a/framework/bigcommerce/api/cart/handlers/add-item.ts b/framework/bigcommerce/api/endpoints/cart/add-item.ts similarity index 82% rename from framework/bigcommerce/api/cart/handlers/add-item.ts rename to framework/bigcommerce/api/endpoints/cart/add-item.ts index c47e72cdb0..52ef1223d9 100644 --- a/framework/bigcommerce/api/cart/handlers/add-item.ts +++ b/framework/bigcommerce/api/endpoints/cart/add-item.ts @@ -1,8 +1,9 @@ +import { normalizeCart } from '../../../lib/normalize' import { parseCartItem } from '../../utils/parse-item' import getCartCookie from '../../utils/get-cart-cookie' -import type { CartHandlers } from '..' +import type { CartEndpoint } from '.' -const addItem: CartHandlers['addItem'] = async ({ +const addItem: CartEndpoint['handlers']['addItem'] = async ({ res, body: { cartId, item }, config, @@ -39,7 +40,7 @@ const addItem: CartHandlers['addItem'] = async ({ 'Set-Cookie', getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge) ) - res.status(200).json({ data }) + res.status(200).json({ data: normalizeCart(data) }) } export default addItem diff --git a/framework/bigcommerce/api/cart/handlers/get-cart.ts b/framework/bigcommerce/api/endpoints/cart/get-cart.ts similarity index 69% rename from framework/bigcommerce/api/cart/handlers/get-cart.ts rename to framework/bigcommerce/api/endpoints/cart/get-cart.ts index 890ac99972..d3bb309e2f 100644 --- a/framework/bigcommerce/api/cart/handlers/get-cart.ts +++ b/framework/bigcommerce/api/endpoints/cart/get-cart.ts @@ -1,10 +1,11 @@ -import type { BigcommerceCart } from '../../../types' +import { normalizeCart } from '../../../lib/normalize' import { BigcommerceApiError } from '../../utils/errors' import getCartCookie from '../../utils/get-cart-cookie' -import type { CartHandlers } from '../' +import type { BigcommerceCart } from '../../../types/cart' +import type { CartEndpoint } from '.' // Return current cart info -const getCart: CartHandlers['getCart'] = async ({ +const getCart: CartEndpoint['handlers']['getCart'] = async ({ res, body: { cartId }, config, @@ -26,7 +27,9 @@ const getCart: CartHandlers['getCart'] = async ({ } } - res.status(200).json({ data: result.data ?? null }) + res.status(200).json({ + data: result.data ? normalizeCart(result.data) : null, + }) } export default getCart diff --git a/framework/bigcommerce/api/endpoints/cart/index.ts b/framework/bigcommerce/api/endpoints/cart/index.ts new file mode 100644 index 0000000000..ae2414d701 --- /dev/null +++ b/framework/bigcommerce/api/endpoints/cart/index.ts @@ -0,0 +1,26 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import cartEndpoint from '@commerce/api/endpoints/cart' +import type { CartSchema } from '../../../types/cart' +import type { BigcommerceAPI } from '../..' +import getCart from './get-cart' +import addItem from './add-item' +import updateItem from './update-item' +import removeItem from './remove-item' + +export type CartAPI = GetAPISchema + +export type CartEndpoint = CartAPI['endpoint'] + +export const handlers: CartEndpoint['handlers'] = { + getCart, + addItem, + updateItem, + removeItem, +} + +const cartApi = createEndpoint({ + handler: cartEndpoint, + handlers, +}) + +export default cartApi diff --git a/framework/bigcommerce/api/cart/handlers/remove-item.ts b/framework/bigcommerce/api/endpoints/cart/remove-item.ts similarity index 77% rename from framework/bigcommerce/api/cart/handlers/remove-item.ts rename to framework/bigcommerce/api/endpoints/cart/remove-item.ts index c098489486..baf10c80ff 100644 --- a/framework/bigcommerce/api/cart/handlers/remove-item.ts +++ b/framework/bigcommerce/api/endpoints/cart/remove-item.ts @@ -1,7 +1,8 @@ +import { normalizeCart } from '../../../lib/normalize' import getCartCookie from '../../utils/get-cart-cookie' -import type { CartHandlers } from '..' +import type { CartEndpoint } from '.' -const removeItem: CartHandlers['removeItem'] = async ({ +const removeItem: CartEndpoint['handlers']['removeItem'] = async ({ res, body: { cartId, itemId }, config, @@ -27,7 +28,7 @@ const removeItem: CartHandlers['removeItem'] = async ({ : // Remove the cart cookie if the cart was removed (empty items) getCartCookie(config.cartCookie) ) - res.status(200).json({ data }) + res.status(200).json({ data: data && normalizeCart(data) }) } export default removeItem diff --git a/framework/bigcommerce/api/cart/handlers/update-item.ts b/framework/bigcommerce/api/endpoints/cart/update-item.ts similarity index 77% rename from framework/bigcommerce/api/cart/handlers/update-item.ts rename to framework/bigcommerce/api/endpoints/cart/update-item.ts index 27b74ca201..113553fadb 100644 --- a/framework/bigcommerce/api/cart/handlers/update-item.ts +++ b/framework/bigcommerce/api/endpoints/cart/update-item.ts @@ -1,8 +1,9 @@ +import { normalizeCart } from '../../../lib/normalize' import { parseCartItem } from '../../utils/parse-item' import getCartCookie from '../../utils/get-cart-cookie' -import type { CartHandlers } from '..' +import type { CartEndpoint } from '.' -const updateItem: CartHandlers['updateItem'] = async ({ +const updateItem: CartEndpoint['handlers']['updateItem'] = async ({ res, body: { cartId, itemId, item }, config, @@ -29,7 +30,7 @@ const updateItem: CartHandlers['updateItem'] = async ({ 'Set-Cookie', getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge) ) - res.status(200).json({ data }) + res.status(200).json({ data: normalizeCart(data) }) } export default updateItem diff --git a/framework/bigcommerce/api/catalog/handlers/get-products.ts b/framework/bigcommerce/api/endpoints/catalog/products/get-products.ts similarity index 68% rename from framework/bigcommerce/api/catalog/handlers/get-products.ts rename to framework/bigcommerce/api/endpoints/catalog/products/get-products.ts index bedd773b00..6dde39e28e 100644 --- a/framework/bigcommerce/api/catalog/handlers/get-products.ts +++ b/framework/bigcommerce/api/endpoints/catalog/products/get-products.ts @@ -1,6 +1,5 @@ -import { Product } from '@commerce/types' -import getAllProducts, { ProductEdge } from '../../../product/get-all-products' -import type { ProductsHandlers } from '../products' +import { Product } from '@commerce/types/product' +import { ProductsEndpoint } from '.' const SORT: { [key: string]: string | undefined } = { latest: 'id', @@ -11,10 +10,11 @@ const SORT: { [key: string]: string | undefined } = { const LIMIT = 12 // Return current cart info -const getProducts: ProductsHandlers['getProducts'] = async ({ +const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ res, - body: { search, category, brand, sort }, + body: { search, categoryId, brandId, sort }, config, + commerce, }) => { // Use a dummy base as we only care about the relative path const url = new URL('/v3/catalog/products', 'http://a') @@ -24,11 +24,11 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ if (search) url.searchParams.set('keyword', search) - if (category && Number.isInteger(Number(category))) - url.searchParams.set('categories:in', category) + if (categoryId && Number.isInteger(Number(categoryId))) + url.searchParams.set('categories:in', String(categoryId)) - if (brand && Number.isInteger(Number(brand))) - url.searchParams.set('brand_id', brand) + if (brandId && Number.isInteger(Number(brandId))) + url.searchParams.set('brand_id', String(brandId)) if (sort) { const [_sort, direction] = sort.split('-') @@ -47,18 +47,18 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ url.pathname + url.search ) - const entityIds = data.map((p) => p.id) - const found = entityIds.length > 0 + const ids = data.map((p) => String(p.id)) + const found = ids.length > 0 // We want the GraphQL version of each product - const graphqlData = await getAllProducts({ - variables: { first: LIMIT, entityIds }, + const graphqlData = await commerce.getAllProducts({ + variables: { first: LIMIT, ids }, config, }) // Put the products in an object that we can use to get them by id const productsById = graphqlData.products.reduce<{ - [k: number]: Product + [k: string]: Product }>((prods, p) => { prods[Number(p.id)] = p return prods @@ -68,7 +68,7 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ // Populate the products array with the graphql products, in the order // assigned by the list of entity ids - entityIds.forEach((id) => { + ids.forEach((id) => { const product = productsById[id] if (product) products.push(product) }) diff --git a/framework/bigcommerce/api/endpoints/catalog/products/index.ts b/framework/bigcommerce/api/endpoints/catalog/products/index.ts new file mode 100644 index 0000000000..555740f606 --- /dev/null +++ b/framework/bigcommerce/api/endpoints/catalog/products/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import productsEndpoint from '@commerce/api/endpoints/catalog/products' +import type { ProductsSchema } from '../../../../types/product' +import type { BigcommerceAPI } from '../../..' +import getProducts from './get-products' + +export type ProductsAPI = GetAPISchema + +export type ProductsEndpoint = ProductsAPI['endpoint'] + +export const handlers: ProductsEndpoint['handlers'] = { getProducts } + +const productsApi = createEndpoint({ + handler: productsEndpoint, + handlers, +}) + +export default productsApi diff --git a/framework/bigcommerce/api/endpoints/checkout/checkout.ts b/framework/bigcommerce/api/endpoints/checkout/checkout.ts new file mode 100644 index 0000000000..517a57950f --- /dev/null +++ b/framework/bigcommerce/api/endpoints/checkout/checkout.ts @@ -0,0 +1,62 @@ +import type { CheckoutEndpoint } from '.' + +const fullCheckout = true + +const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({ + req, + res, + config, +}) => { + const { cookies } = req + const cartId = cookies[config.cartCookie] + + if (!cartId) { + res.redirect('/cart') + return + } + + const { data } = await config.storeApiFetch( + `/v3/carts/${cartId}/redirect_urls`, + { + method: 'POST', + } + ) + + if (fullCheckout) { + res.redirect(data.checkout_url) + return + } + + // TODO: make the embedded checkout work too! + const html = ` + + + + + + Checkout + + + + +
+ + + ` + + res.status(200) + res.setHeader('Content-Type', 'text/html') + res.write(html) + res.end() +} + +export default checkout diff --git a/framework/bigcommerce/api/endpoints/checkout/index.ts b/framework/bigcommerce/api/endpoints/checkout/index.ts new file mode 100644 index 0000000000..eaba32e470 --- /dev/null +++ b/framework/bigcommerce/api/endpoints/checkout/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import checkoutEndpoint from '@commerce/api/endpoints/checkout' +import type { CheckoutSchema } from '../../../types/checkout' +import type { BigcommerceAPI } from '../..' +import checkout from './checkout' + +export type CheckoutAPI = GetAPISchema + +export type CheckoutEndpoint = CheckoutAPI['endpoint'] + +export const handlers: CheckoutEndpoint['handlers'] = { checkout } + +const checkoutApi = createEndpoint({ + handler: checkoutEndpoint, + handlers, +}) + +export default checkoutApi diff --git a/framework/bigcommerce/api/customers/handlers/get-logged-in-customer.ts b/framework/bigcommerce/api/endpoints/customer/get-logged-in-customer.ts similarity index 89% rename from framework/bigcommerce/api/customers/handlers/get-logged-in-customer.ts rename to framework/bigcommerce/api/endpoints/customer/get-logged-in-customer.ts index 698235dda8..cfcce95326 100644 --- a/framework/bigcommerce/api/customers/handlers/get-logged-in-customer.ts +++ b/framework/bigcommerce/api/endpoints/customer/get-logged-in-customer.ts @@ -1,5 +1,5 @@ import type { GetLoggedInCustomerQuery } from '../../../schema' -import type { CustomersHandlers } from '..' +import type { CustomerEndpoint } from '.' export const getLoggedInCustomerQuery = /* GraphQL */ ` query getLoggedInCustomer { @@ -24,7 +24,7 @@ export const getLoggedInCustomerQuery = /* GraphQL */ ` export type Customer = NonNullable -const getLoggedInCustomer: CustomersHandlers['getLoggedInCustomer'] = async ({ +const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] = async ({ req, res, config, diff --git a/framework/bigcommerce/api/endpoints/customer/index.ts b/framework/bigcommerce/api/endpoints/customer/index.ts new file mode 100644 index 0000000000..cb0f6787ad --- /dev/null +++ b/framework/bigcommerce/api/endpoints/customer/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import customerEndpoint from '@commerce/api/endpoints/customer' +import type { CustomerSchema } from '../../../types/customer' +import type { BigcommerceAPI } from '../..' +import getLoggedInCustomer from './get-logged-in-customer' + +export type CustomerAPI = GetAPISchema + +export type CustomerEndpoint = CustomerAPI['endpoint'] + +export const handlers: CustomerEndpoint['handlers'] = { getLoggedInCustomer } + +const customerApi = createEndpoint({ + handler: customerEndpoint, + handlers, +}) + +export default customerApi diff --git a/framework/bigcommerce/api/endpoints/login/index.ts b/framework/bigcommerce/api/endpoints/login/index.ts new file mode 100644 index 0000000000..2b454c7c25 --- /dev/null +++ b/framework/bigcommerce/api/endpoints/login/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import loginEndpoint from '@commerce/api/endpoints/login' +import type { LoginSchema } from '../../../types/login' +import type { BigcommerceAPI } from '../..' +import login from './login' + +export type LoginAPI = GetAPISchema + +export type LoginEndpoint = LoginAPI['endpoint'] + +export const handlers: LoginEndpoint['handlers'] = { login } + +const loginApi = createEndpoint({ + handler: loginEndpoint, + handlers, +}) + +export default loginApi diff --git a/framework/bigcommerce/api/customers/handlers/login.ts b/framework/bigcommerce/api/endpoints/login/login.ts similarity index 81% rename from framework/bigcommerce/api/customers/handlers/login.ts rename to framework/bigcommerce/api/endpoints/login/login.ts index 9e019f3a0a..f55c3b54f4 100644 --- a/framework/bigcommerce/api/customers/handlers/login.ts +++ b/framework/bigcommerce/api/endpoints/login/login.ts @@ -1,13 +1,13 @@ import { FetcherError } from '@commerce/utils/errors' -import login from '../../../auth/login' -import type { LoginHandlers } from '../login' +import type { LoginEndpoint } from '.' const invalidCredentials = /invalid credentials/i -const loginHandler: LoginHandlers['login'] = async ({ +const login: LoginEndpoint['handlers']['login'] = async ({ res, body: { email, password }, config, + commerce, }) => { // TODO: Add proper validations with something like Ajv if (!(email && password)) { @@ -21,7 +21,7 @@ const loginHandler: LoginHandlers['login'] = async ({ // and numeric characters. try { - await login({ variables: { email, password }, config, res }) + await commerce.login({ variables: { email, password }, config, res }) } catch (error) { // Check if the email and password didn't match an existing account if ( @@ -46,4 +46,4 @@ const loginHandler: LoginHandlers['login'] = async ({ res.status(200).json({ data: null }) } -export default loginHandler +export default login diff --git a/framework/bigcommerce/api/endpoints/logout/index.ts b/framework/bigcommerce/api/endpoints/logout/index.ts new file mode 100644 index 0000000000..0dbb23bab4 --- /dev/null +++ b/framework/bigcommerce/api/endpoints/logout/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import logoutEndpoint from '@commerce/api/endpoints/logout' +import type { LogoutSchema } from '../../../types/logout' +import type { BigcommerceAPI } from '../..' +import logout from './logout' + +export type LogoutAPI = GetAPISchema + +export type LogoutEndpoint = LogoutAPI['endpoint'] + +export const handlers: LogoutEndpoint['handlers'] = { logout } + +const logoutApi = createEndpoint({ + handler: logoutEndpoint, + handlers, +}) + +export default logoutApi diff --git a/framework/bigcommerce/api/customers/handlers/logout.ts b/framework/bigcommerce/api/endpoints/logout/logout.ts similarity index 74% rename from framework/bigcommerce/api/customers/handlers/logout.ts rename to framework/bigcommerce/api/endpoints/logout/logout.ts index 937ce09541..b90a8c3ceb 100644 --- a/framework/bigcommerce/api/customers/handlers/logout.ts +++ b/framework/bigcommerce/api/endpoints/logout/logout.ts @@ -1,7 +1,7 @@ import { serialize } from 'cookie' -import { LogoutHandlers } from '../logout' +import type { LogoutEndpoint } from '.' -const logoutHandler: LogoutHandlers['logout'] = async ({ +const logout: LogoutEndpoint['handlers']['logout'] = async ({ res, body: { redirectTo }, config, @@ -20,4 +20,4 @@ const logoutHandler: LogoutHandlers['logout'] = async ({ } } -export default logoutHandler +export default logout diff --git a/framework/bigcommerce/api/endpoints/signup/index.ts b/framework/bigcommerce/api/endpoints/signup/index.ts new file mode 100644 index 0000000000..6ce8be271c --- /dev/null +++ b/framework/bigcommerce/api/endpoints/signup/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import signupEndpoint from '@commerce/api/endpoints/signup' +import type { SignupSchema } from '../../../types/signup' +import type { BigcommerceAPI } from '../..' +import signup from './signup' + +export type SignupAPI = GetAPISchema + +export type SignupEndpoint = SignupAPI['endpoint'] + +export const handlers: SignupEndpoint['handlers'] = { signup } + +const singupApi = createEndpoint({ + handler: signupEndpoint, + handlers, +}) + +export default singupApi diff --git a/framework/bigcommerce/api/customers/handlers/signup.ts b/framework/bigcommerce/api/endpoints/signup/signup.ts similarity index 88% rename from framework/bigcommerce/api/customers/handlers/signup.ts rename to framework/bigcommerce/api/endpoints/signup/signup.ts index 1b24db0ccc..46c071be8d 100644 --- a/framework/bigcommerce/api/customers/handlers/signup.ts +++ b/framework/bigcommerce/api/endpoints/signup/signup.ts @@ -1,11 +1,11 @@ import { BigcommerceApiError } from '../../utils/errors' -import login from '../../../auth/login' -import { SignupHandlers } from '../signup' +import type { SignupEndpoint } from '.' -const signup: SignupHandlers['signup'] = async ({ +const signup: SignupEndpoint['handlers']['signup'] = async ({ res, body: { firstName, lastName, email, password }, config, + commerce, }) => { // TODO: Add proper validations with something like Ajv if (!(firstName && lastName && email && password)) { @@ -54,7 +54,7 @@ const signup: SignupHandlers['signup'] = async ({ } // Login the customer right after creating it - await login({ variables: { email, password }, res, config }) + await commerce.login({ variables: { email, password }, res, config }) res.status(200).json({ data: null }) } diff --git a/framework/bigcommerce/api/wishlist/handlers/add-item.ts b/framework/bigcommerce/api/endpoints/wishlist/add-item.ts similarity index 76% rename from framework/bigcommerce/api/wishlist/handlers/add-item.ts rename to framework/bigcommerce/api/endpoints/wishlist/add-item.ts index 00d7b06bdb..4c5970a5d6 100644 --- a/framework/bigcommerce/api/wishlist/handlers/add-item.ts +++ b/framework/bigcommerce/api/endpoints/wishlist/add-item.ts @@ -1,13 +1,14 @@ -import type { WishlistHandlers } from '..' -import getCustomerId from '../../../customer/get-customer-id' -import getCustomerWishlist from '../../../customer/get-customer-wishlist' +import getCustomerWishlist from '../../operations/get-customer-wishlist' import { parseWishlistItem } from '../../utils/parse-item' +import getCustomerId from './utils/get-customer-id' +import type { WishlistEndpoint } from '.' -// Returns the wishlist of the signed customer -const addItem: WishlistHandlers['addItem'] = async ({ +// Return wishlist info +const addItem: WishlistEndpoint['handlers']['addItem'] = async ({ res, body: { customerToken, item }, config, + commerce, }) => { if (!item) { return res.status(400).json({ @@ -26,7 +27,7 @@ const addItem: WishlistHandlers['addItem'] = async ({ }) } - const { wishlist } = await getCustomerWishlist({ + const { wishlist } = await commerce.getCustomerWishlist({ variables: { customerId }, config, }) diff --git a/framework/bigcommerce/api/wishlist/handlers/get-wishlist.ts b/framework/bigcommerce/api/endpoints/wishlist/get-wishlist.ts similarity index 64% rename from framework/bigcommerce/api/wishlist/handlers/get-wishlist.ts rename to framework/bigcommerce/api/endpoints/wishlist/get-wishlist.ts index 3737c033ab..21443119cb 100644 --- a/framework/bigcommerce/api/wishlist/handlers/get-wishlist.ts +++ b/framework/bigcommerce/api/endpoints/wishlist/get-wishlist.ts @@ -1,12 +1,14 @@ -import getCustomerId from '../../../customer/get-customer-id' -import getCustomerWishlist from '../../../customer/get-customer-wishlist' -import type { Wishlist, WishlistHandlers } from '..' +import type { Wishlist } from '../../../types/wishlist' +import type { WishlistEndpoint } from '.' +import getCustomerId from './utils/get-customer-id' +import getCustomerWishlist from '../../operations/get-customer-wishlist' // Return wishlist info -const getWishlist: WishlistHandlers['getWishlist'] = async ({ +const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({ res, body: { customerToken, includeProducts }, config, + commerce, }) => { let result: { data?: Wishlist } = {} @@ -22,7 +24,7 @@ const getWishlist: WishlistHandlers['getWishlist'] = async ({ }) } - const { wishlist } = await getCustomerWishlist({ + const { wishlist } = await commerce.getCustomerWishlist({ variables: { customerId }, includeProducts, config, diff --git a/framework/bigcommerce/api/endpoints/wishlist/index.ts b/framework/bigcommerce/api/endpoints/wishlist/index.ts new file mode 100644 index 0000000000..31af234cec --- /dev/null +++ b/framework/bigcommerce/api/endpoints/wishlist/index.ts @@ -0,0 +1,24 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import wishlistEndpoint from '@commerce/api/endpoints/wishlist' +import type { WishlistSchema } from '../../../types/wishlist' +import type { BigcommerceAPI } from '../..' +import getWishlist from './get-wishlist' +import addItem from './add-item' +import removeItem from './remove-item' + +export type WishlistAPI = GetAPISchema + +export type WishlistEndpoint = WishlistAPI['endpoint'] + +export const handlers: WishlistEndpoint['handlers'] = { + getWishlist, + addItem, + removeItem, +} + +const wishlistApi = createEndpoint({ + handler: wishlistEndpoint, + handlers, +}) + +export default wishlistApi diff --git a/framework/bigcommerce/api/wishlist/handlers/remove-item.ts b/framework/bigcommerce/api/endpoints/wishlist/remove-item.ts similarity index 63% rename from framework/bigcommerce/api/wishlist/handlers/remove-item.ts rename to framework/bigcommerce/api/endpoints/wishlist/remove-item.ts index a9cfd9db58..22ac31cf93 100644 --- a/framework/bigcommerce/api/wishlist/handlers/remove-item.ts +++ b/framework/bigcommerce/api/endpoints/wishlist/remove-item.ts @@ -1,20 +1,20 @@ -import getCustomerId from '../../../customer/get-customer-id' -import getCustomerWishlist, { - Wishlist, -} from '../../../customer/get-customer-wishlist' -import type { WishlistHandlers } from '..' +import type { Wishlist } from '../../../types/wishlist' +import getCustomerWishlist from '../../operations/get-customer-wishlist' +import getCustomerId from './utils/get-customer-id' +import type { WishlistEndpoint } from '.' -// Return current wishlist info -const removeItem: WishlistHandlers['removeItem'] = async ({ +// Return wishlist info +const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({ res, body: { customerToken, itemId }, config, + commerce, }) => { const customerId = customerToken && (await getCustomerId({ customerToken, config })) const { wishlist } = (customerId && - (await getCustomerWishlist({ + (await commerce.getCustomerWishlist({ variables: { customerId }, config, }))) || diff --git a/framework/bigcommerce/customer/get-customer-id.ts b/framework/bigcommerce/api/endpoints/wishlist/utils/get-customer-id.ts similarity index 65% rename from framework/bigcommerce/customer/get-customer-id.ts rename to framework/bigcommerce/api/endpoints/wishlist/utils/get-customer-id.ts index 65ce5a6a85..603f8be2d2 100644 --- a/framework/bigcommerce/customer/get-customer-id.ts +++ b/framework/bigcommerce/api/endpoints/wishlist/utils/get-customer-id.ts @@ -1,5 +1,5 @@ -import { GetCustomerIdQuery } from '../schema' -import { BigcommerceConfig, getConfig } from '../api' +import type { GetCustomerIdQuery } from '../../../../schema' +import type { BigcommerceConfig } from '../../..' export const getCustomerIdQuery = /* GraphQL */ ` query getCustomerId { @@ -14,10 +14,8 @@ async function getCustomerId({ config, }: { customerToken: string - config?: BigcommerceConfig -}): Promise { - config = getConfig(config) - + config: BigcommerceConfig +}): Promise { const { data } = await config.fetch( getCustomerIdQuery, undefined, @@ -28,7 +26,7 @@ async function getCustomerId({ } ) - return data?.customer?.entityId + return String(data?.customer?.entityId) } export default getCustomerId diff --git a/framework/bigcommerce/api/index.ts b/framework/bigcommerce/api/index.ts index 0216fe61ba..300f1087b1 100644 --- a/framework/bigcommerce/api/index.ts +++ b/framework/bigcommerce/api/index.ts @@ -1,8 +1,29 @@ import type { RequestInit } from '@vercel/fetch' -import type { CommerceAPIConfig } from '@commerce/api' +import { + CommerceAPI, + CommerceAPIConfig, + getCommerceApi as commerceApi, +} from '@commerce/api' import fetchGraphqlApi from './utils/fetch-graphql-api' import fetchStoreApi from './utils/fetch-store-api' +import type { CartAPI } from './endpoints/cart' +import type { CustomerAPI } from './endpoints/customer' +import type { LoginAPI } from './endpoints/login' +import type { LogoutAPI } from './endpoints/logout' +import type { SignupAPI } from './endpoints/signup' +import type { ProductsAPI } from './endpoints/catalog/products' +import type { WishlistAPI } from './endpoints/wishlist' + +import login from './operations/login' +import getAllPages from './operations/get-all-pages' +import getPage from './operations/get-page' +import getSiteInfo from './operations/get-site-info' +import getCustomerWishlist from './operations/get-customer-wishlist' +import getAllProductPaths from './operations/get-all-product-paths' +import getAllProducts from './operations/get-all-products' +import getProduct from './operations/get-product' + export interface BigcommerceConfig extends CommerceAPIConfig { // Indicates if the returned metadata with translations should be applied to the // data or returned as it is @@ -39,34 +60,12 @@ if (!(STORE_API_URL && STORE_API_TOKEN && STORE_API_CLIENT_ID)) { ) } -export class Config { - private config: BigcommerceConfig - - constructor(config: Omit) { - this.config = { - ...config, - // The customerCookie is not customizable for now, BC sets the cookie and it's - // not important to rename it - customerCookie: 'SHOP_TOKEN', - } - } - - getConfig(userConfig: Partial = {}) { - return Object.entries(userConfig).reduce( - (cfg, [key, value]) => Object.assign(cfg, { [key]: value }), - { ...this.config } - ) - } - - setConfig(newConfig: Partial) { - Object.assign(this.config, newConfig) - } -} - const ONE_DAY = 60 * 60 * 24 -const config = new Config({ + +const config: BigcommerceConfig = { commerceUrl: API_URL, apiToken: API_TOKEN, + customerCookie: 'SHOP_TOKEN', cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId', cartCookieMaxAge: ONE_DAY * 30, fetch: fetchGraphqlApi, @@ -77,12 +76,36 @@ const config = new Config({ storeApiClientId: STORE_API_CLIENT_ID, storeChannelId: STORE_CHANNEL_ID, storeApiFetch: fetchStoreApi, -}) +} -export function getConfig(userConfig?: Partial) { - return config.getConfig(userConfig) +const operations = { + login, + getAllPages, + getPage, + getSiteInfo, + getCustomerWishlist, + getAllProductPaths, + getAllProducts, + getProduct, } -export function setConfig(newConfig: Partial) { - return config.setConfig(newConfig) +export const provider = { config, operations } + +export type Provider = typeof provider + +export type APIs = + | CartAPI + | CustomerAPI + | LoginAPI + | LogoutAPI + | SignupAPI + | ProductsAPI + | WishlistAPI + +export type BigcommerceAPI

= CommerceAPI

+ +export function getCommerceApi

( + customProvider: P = provider as any +): BigcommerceAPI

{ + return commerceApi(customProvider) } diff --git a/framework/bigcommerce/api/operations/get-all-pages.ts b/framework/bigcommerce/api/operations/get-all-pages.ts new file mode 100644 index 0000000000..3a9b64b1f2 --- /dev/null +++ b/framework/bigcommerce/api/operations/get-all-pages.ts @@ -0,0 +1,46 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { Page, GetAllPagesOperation } from '../../types/page' +import type { RecursivePartial, RecursiveRequired } from '../utils/types' +import { BigcommerceConfig, Provider } from '..' + +export default function getAllPagesOperation({ + commerce, +}: OperationContext) { + async function getAllPages(opts?: { + config?: Partial + preview?: boolean + }): Promise + + async function getAllPages( + opts: { + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getAllPages({ + config, + preview, + }: { + url?: string + config?: Partial + preview?: boolean + } = {}): Promise { + const cfg = commerce.getConfig(config) + // RecursivePartial forces the method to check for every prop in the data, which is + // required in case there's a custom `url` + const { data } = await cfg.storeApiFetch< + RecursivePartial<{ data: Page[] }> + >('/v3/content/pages') + const pages = (data as RecursiveRequired) ?? [] + + return { + pages: preview ? pages : pages.filter((p) => p.is_visible), + } + } + + return getAllPages +} diff --git a/framework/bigcommerce/api/operations/get-all-product-paths.ts b/framework/bigcommerce/api/operations/get-all-product-paths.ts new file mode 100644 index 0000000000..da7b457ebd --- /dev/null +++ b/framework/bigcommerce/api/operations/get-all-product-paths.ts @@ -0,0 +1,66 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { GetAllProductPathsQuery } from '../../schema' +import type { GetAllProductPathsOperation } from '../../types/product' +import type { RecursivePartial, RecursiveRequired } from '../utils/types' +import filterEdges from '../utils/filter-edges' +import { BigcommerceConfig, Provider } from '..' + +export const getAllProductPathsQuery = /* GraphQL */ ` + query getAllProductPaths($first: Int = 100) { + site { + products(first: $first) { + edges { + node { + path + } + } + } + } + } +` + +export default function getAllProductPathsOperation({ + commerce, +}: OperationContext) { + async function getAllProductPaths< + T extends GetAllProductPathsOperation + >(opts?: { + variables?: T['variables'] + config?: BigcommerceConfig + }): Promise + + async function getAllProductPaths( + opts: { + variables?: T['variables'] + config?: BigcommerceConfig + } & OperationOptions + ): Promise + + async function getAllProductPaths({ + query = getAllProductPathsQuery, + variables, + config, + }: { + query?: string + variables?: T['variables'] + config?: BigcommerceConfig + } = {}): Promise { + config = commerce.getConfig(config) + // RecursivePartial forces the method to check for every prop in the data, which is + // required in case there's a custom `query` + const { data } = await config.fetch< + RecursivePartial + >(query, { variables }) + const products = data.site?.products?.edges + + return { + products: filterEdges(products as RecursiveRequired).map( + ({ node }) => node + ), + } + } + return getAllProductPaths +} diff --git a/framework/bigcommerce/api/operations/get-all-products.ts b/framework/bigcommerce/api/operations/get-all-products.ts new file mode 100644 index 0000000000..c2652f5bfd --- /dev/null +++ b/framework/bigcommerce/api/operations/get-all-products.ts @@ -0,0 +1,135 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { + GetAllProductsQuery, + GetAllProductsQueryVariables, +} from '../../schema' +import type { GetAllProductsOperation } from '../../types/product' +import type { RecursivePartial, RecursiveRequired } from '../utils/types' +import filterEdges from '../utils/filter-edges' +import setProductLocaleMeta from '../utils/set-product-locale-meta' +import { productConnectionFragment } from '../fragments/product' +import { BigcommerceConfig, Provider } from '..' +import { normalizeProduct } from '../../lib/normalize' + +export const getAllProductsQuery = /* GraphQL */ ` + query getAllProducts( + $hasLocale: Boolean = false + $locale: String = "null" + $entityIds: [Int!] + $first: Int = 10 + $products: Boolean = false + $featuredProducts: Boolean = false + $bestSellingProducts: Boolean = false + $newestProducts: Boolean = false + ) { + site { + products(first: $first, entityIds: $entityIds) @include(if: $products) { + ...productConnnection + } + featuredProducts(first: $first) @include(if: $featuredProducts) { + ...productConnnection + } + bestSellingProducts(first: $first) @include(if: $bestSellingProducts) { + ...productConnnection + } + newestProducts(first: $first) @include(if: $newestProducts) { + ...productConnnection + } + } + } + + ${productConnectionFragment} +` + +export type ProductEdge = NonNullable< + NonNullable[0] +> + +export type ProductNode = ProductEdge['node'] + +export type GetAllProductsResult< + T extends Record = { + products: ProductEdge[] + } +> = T + +function getProductsType( + relevance?: GetAllProductsOperation['variables']['relevance'] +) { + switch (relevance) { + case 'featured': + return 'featuredProducts' + case 'best_selling': + return 'bestSellingProducts' + case 'newest': + return 'newestProducts' + default: + return 'products' + } +} + +export default function getAllProductsOperation({ + commerce, +}: OperationContext) { + async function getAllProducts(opts?: { + variables?: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getAllProducts( + opts: { + variables?: T['variables'] + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getAllProducts({ + query = getAllProductsQuery, + variables: vars = {}, + config: cfg, + }: { + query?: string + variables?: T['variables'] + config?: Partial + preview?: boolean + } = {}): Promise { + const config = commerce.getConfig(cfg) + const { locale } = config + const field = getProductsType(vars.relevance) + const variables: GetAllProductsQueryVariables = { + locale, + hasLocale: !!locale, + } + + variables[field] = true + + if (vars.first) variables.first = vars.first + if (vars.ids) variables.entityIds = vars.ids.map((id) => Number(id)) + + // RecursivePartial forces the method to check for every prop in the data, which is + // required in case there's a custom `query` + const { data } = await config.fetch>( + query, + { variables } + ) + const edges = data.site?.[field]?.edges + const products = filterEdges(edges as RecursiveRequired) + + if (locale && config.applyLocale) { + products.forEach((product: RecursivePartial) => { + if (product.node) setProductLocaleMeta(product.node) + }) + } + + return { + products: products.map(({ node }) => normalizeProduct(node as any)), + } + } + + return getAllProducts +} diff --git a/framework/bigcommerce/api/operations/get-customer-wishlist.ts b/framework/bigcommerce/api/operations/get-customer-wishlist.ts new file mode 100644 index 0000000000..fc9487ffe4 --- /dev/null +++ b/framework/bigcommerce/api/operations/get-customer-wishlist.ts @@ -0,0 +1,81 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { + GetCustomerWishlistOperation, + Wishlist, +} from '../../types/wishlist' +import type { RecursivePartial, RecursiveRequired } from '../utils/types' +import { BigcommerceConfig, Provider } from '..' +import getAllProducts, { ProductEdge } from './get-all-products' + +export default function getCustomerWishlistOperation({ + commerce, +}: OperationContext) { + async function getCustomerWishlist< + T extends GetCustomerWishlistOperation + >(opts: { + variables: T['variables'] + config?: BigcommerceConfig + includeProducts?: boolean + }): Promise + + async function getCustomerWishlist( + opts: { + variables: T['variables'] + config?: BigcommerceConfig + includeProducts?: boolean + } & OperationOptions + ): Promise + + async function getCustomerWishlist({ + config, + variables, + includeProducts, + }: { + url?: string + variables: T['variables'] + config?: BigcommerceConfig + includeProducts?: boolean + }): Promise { + config = commerce.getConfig(config) + + const { data = [] } = await config.storeApiFetch< + RecursivePartial<{ data: Wishlist[] }> + >(`/v3/wishlists?customer_id=${variables.customerId}`) + const wishlist = data[0] + + if (includeProducts && wishlist?.items?.length) { + const ids = wishlist.items + ?.map((item) => (item?.product_id ? String(item?.product_id) : null)) + .filter((id): id is string => !!id) + + if (ids?.length) { + const graphqlData = await commerce.getAllProducts({ + variables: { first: 100, ids }, + config, + }) + // Put the products in an object that we can use to get them by id + const productsById = graphqlData.products.reduce<{ + [k: number]: ProductEdge + }>((prods, p) => { + prods[Number(p.id)] = p as any + return prods + }, {}) + // Populate the wishlist items with the graphql products + wishlist.items.forEach((item) => { + const product = item && productsById[item.product_id!] + if (item && product) { + // @ts-ignore Fix this type when the wishlist type is properly defined + item.product = product + } + }) + } + } + + return { wishlist: wishlist as RecursiveRequired } + } + + return getCustomerWishlist +} diff --git a/framework/bigcommerce/api/operations/get-page.ts b/framework/bigcommerce/api/operations/get-page.ts new file mode 100644 index 0000000000..6a1fea9d0e --- /dev/null +++ b/framework/bigcommerce/api/operations/get-page.ts @@ -0,0 +1,54 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { GetPageOperation, Page } from '../../types/page' +import type { RecursivePartial, RecursiveRequired } from '../utils/types' +import type { BigcommerceConfig, Provider } from '..' +import { normalizePage } from '../../lib/normalize' + +export default function getPageOperation({ + commerce, +}: OperationContext) { + async function getPage(opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getPage( + opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getPage({ + url, + variables, + config, + preview, + }: { + url?: string + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise { + const cfg = commerce.getConfig(config) + // RecursivePartial forces the method to check for every prop in the data, which is + // required in case there's a custom `url` + const { data } = await cfg.storeApiFetch< + RecursivePartial<{ data: Page[] }> + >(url || `/v3/content/pages?id=${variables.id}&include=body`) + const firstPage = data?.[0] + const page = firstPage as RecursiveRequired + + if (preview || page?.is_visible) { + return { page: normalizePage(page as any) } + } + return {} + } + + return getPage +} diff --git a/framework/bigcommerce/api/operations/get-product.ts b/framework/bigcommerce/api/operations/get-product.ts new file mode 100644 index 0000000000..fb356e952d --- /dev/null +++ b/framework/bigcommerce/api/operations/get-product.ts @@ -0,0 +1,119 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { GetProductOperation } from '../../types/product' +import type { GetProductQuery, GetProductQueryVariables } from '../../schema' +import setProductLocaleMeta from '../utils/set-product-locale-meta' +import { productInfoFragment } from '../fragments/product' +import { BigcommerceConfig, Provider } from '..' +import { normalizeProduct } from '../../lib/normalize' + +export const getProductQuery = /* GraphQL */ ` + query getProduct( + $hasLocale: Boolean = false + $locale: String = "null" + $path: String! + ) { + site { + route(path: $path) { + node { + __typename + ... on Product { + ...productInfo + variants { + edges { + node { + entityId + defaultImage { + urlOriginal + altText + isDefault + } + prices { + ...productPrices + } + inventory { + aggregated { + availableToSell + warningLevel + } + isInStock + } + productOptions { + edges { + node { + __typename + entityId + displayName + ...multipleChoiceOption + } + } + } + } + } + } + } + } + } + } + } + + ${productInfoFragment} +` + +// TODO: See if this type is useful for defining the Product type +// export type ProductNode = Extract< +// GetProductQuery['site']['route']['node'], +// { __typename: 'Product' } +// > + +export default function getAllProductPathsOperation({ + commerce, +}: OperationContext) { + async function getProduct(opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getProduct( + opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getProduct({ + query = getProductQuery, + variables: { slug, ...vars }, + config: cfg, + }: { + query?: string + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise { + const config = commerce.getConfig(cfg) + const { locale } = config + const variables: GetProductQueryVariables = { + locale, + hasLocale: !!locale, + path: slug ? `/${slug}/` : vars.path!, + } + const { data } = await config.fetch(query, { variables }) + const product = data.site?.route?.node + + if (product?.__typename === 'Product') { + if (locale && config.applyLocale) { + setProductLocaleMeta(product) + } + + return { product: normalizeProduct(product as any) } + } + + return {} + } + return getProduct +} diff --git a/framework/bigcommerce/api/operations/get-site-info.ts b/framework/bigcommerce/api/operations/get-site-info.ts new file mode 100644 index 0000000000..0dd94dd9d9 --- /dev/null +++ b/framework/bigcommerce/api/operations/get-site-info.ts @@ -0,0 +1,87 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { GetSiteInfoOperation } from '../../types/site' +import type { GetSiteInfoQuery } from '../../schema' +import filterEdges from '../utils/filter-edges' +import type { BigcommerceConfig, Provider } from '..' +import { categoryTreeItemFragment } from '../fragments/category-tree' +import { normalizeCategory } from '../../lib/normalize' + +// Get 3 levels of categories +export const getSiteInfoQuery = /* GraphQL */ ` + query getSiteInfo { + site { + categoryTree { + ...categoryTreeItem + children { + ...categoryTreeItem + children { + ...categoryTreeItem + } + } + } + brands { + pageInfo { + startCursor + endCursor + } + edges { + cursor + node { + entityId + name + defaultImage { + urlOriginal + altText + } + pageTitle + metaDesc + metaKeywords + searchKeywords + path + } + } + } + } + } + ${categoryTreeItemFragment} +` + +export default function getSiteInfoOperation({ + commerce, +}: OperationContext) { + async function getSiteInfo(opts?: { + config?: Partial + preview?: boolean + }): Promise + + async function getSiteInfo( + opts: { + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getSiteInfo({ + query = getSiteInfoQuery, + config, + }: { + query?: string + config?: Partial + preview?: boolean + } = {}): Promise { + const cfg = commerce.getConfig(config) + const { data } = await cfg.fetch(query) + const categories = data.site.categoryTree.map(normalizeCategory) + const brands = data.site?.brands?.edges + + return { + categories: categories ?? [], + brands: filterEdges(brands), + } + } + + return getSiteInfo +} diff --git a/framework/bigcommerce/api/operations/login.ts b/framework/bigcommerce/api/operations/login.ts new file mode 100644 index 0000000000..021ba3c65b --- /dev/null +++ b/framework/bigcommerce/api/operations/login.ts @@ -0,0 +1,79 @@ +import type { ServerResponse } from 'http' +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { LoginOperation } from '../../types/login' +import type { LoginMutation } from '../../schema' +import type { RecursivePartial } from '../utils/types' +import concatHeader from '../utils/concat-cookie' +import type { BigcommerceConfig, Provider } from '..' + +export const loginMutation = /* GraphQL */ ` + mutation login($email: String!, $password: String!) { + login(email: $email, password: $password) { + result + } + } +` + +export default function loginOperation({ + commerce, +}: OperationContext) { + async function login(opts: { + variables: T['variables'] + config?: BigcommerceConfig + res: ServerResponse + }): Promise + + async function login( + opts: { + variables: T['variables'] + config?: BigcommerceConfig + res: ServerResponse + } & OperationOptions + ): Promise + + async function login({ + query = loginMutation, + variables, + res: response, + config, + }: { + query?: string + variables: T['variables'] + res: ServerResponse + config?: BigcommerceConfig + }): Promise { + config = commerce.getConfig(config) + + const { data, res } = await config.fetch>( + query, + { variables } + ) + // Bigcommerce returns a Set-Cookie header with the auth cookie + let cookie = res.headers.get('Set-Cookie') + + if (cookie && typeof cookie === 'string') { + // In development, don't set a secure cookie or the browser will ignore it + if (process.env.NODE_ENV !== 'production') { + cookie = cookie.replace('; Secure', '') + // SameSite=none can't be set unless the cookie is Secure + // bc seems to sometimes send back SameSite=None rather than none so make + // this case insensitive + cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax') + } + + response.setHeader( + 'Set-Cookie', + concatHeader(response.getHeader('Set-Cookie'), cookie)! + ) + } + + return { + result: data.login?.result, + } + } + + return login +} diff --git a/framework/bigcommerce/api/utils/create-api-handler.ts b/framework/bigcommerce/api/utils/create-api-handler.ts deleted file mode 100644 index 315ec464b9..0000000000 --- a/framework/bigcommerce/api/utils/create-api-handler.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' -import { BigcommerceConfig, getConfig } from '..' - -export type BigcommerceApiHandler< - T = any, - H extends BigcommerceHandlers = {}, - Options extends {} = {} -> = ( - req: NextApiRequest, - res: NextApiResponse>, - config: BigcommerceConfig, - handlers: H, - // Custom configs that may be used by a particular handler - options: Options -) => void | Promise - -export type BigcommerceHandler = (options: { - req: NextApiRequest - res: NextApiResponse> - config: BigcommerceConfig - body: Body -}) => void | Promise - -export type BigcommerceHandlers = { - [k: string]: BigcommerceHandler -} - -export type BigcommerceApiResponse = { - data: T | null - errors?: { message: string; code?: string }[] -} - -export default function createApiHandler< - T = any, - H extends BigcommerceHandlers = {}, - Options extends {} = {} ->( - handler: BigcommerceApiHandler, - handlers: H, - defaultOptions: Options -) { - return function getApiHandler({ - config, - operations, - options, - }: { - config?: BigcommerceConfig - operations?: Partial - options?: Options extends {} ? Partial : never - } = {}): NextApiHandler { - const ops = { ...operations, ...handlers } - const opts = { ...defaultOptions, ...options } - - return function apiHandler(req, res) { - return handler(req, res, getConfig(config), ops, opts) - } - } -} diff --git a/framework/bigcommerce/api/utils/fetch-graphql-api.ts b/framework/bigcommerce/api/utils/fetch-graphql-api.ts index a449b81e00..7dc39f9872 100644 --- a/framework/bigcommerce/api/utils/fetch-graphql-api.ts +++ b/framework/bigcommerce/api/utils/fetch-graphql-api.ts @@ -1,6 +1,6 @@ import { FetcherError } from '@commerce/utils/errors' import type { GraphQLFetcher } from '@commerce/api' -import { getConfig } from '..' +import { provider } from '..' import fetch from './fetch' const fetchGraphqlApi: GraphQLFetcher = async ( @@ -9,7 +9,7 @@ const fetchGraphqlApi: GraphQLFetcher = async ( fetchOptions ) => { // log.warn(query) - const config = getConfig() + const { config } = provider const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { ...fetchOptions, method: 'POST', diff --git a/framework/bigcommerce/api/utils/fetch-store-api.ts b/framework/bigcommerce/api/utils/fetch-store-api.ts index 7e59b9f06b..a00b3777a4 100644 --- a/framework/bigcommerce/api/utils/fetch-store-api.ts +++ b/framework/bigcommerce/api/utils/fetch-store-api.ts @@ -1,5 +1,5 @@ import type { RequestInit, Response } from '@vercel/fetch' -import { getConfig } from '..' +import { provider } from '..' import { BigcommerceApiError, BigcommerceNetworkError } from './errors' import fetch from './fetch' @@ -7,7 +7,7 @@ export default async function fetchStoreApi( endpoint: string, options?: RequestInit ): Promise { - const config = getConfig() + const { config } = provider let res: Response try { diff --git a/framework/bigcommerce/api/utils/is-allowed-method.ts b/framework/bigcommerce/api/utils/is-allowed-method.ts deleted file mode 100644 index 78bbba568d..0000000000 --- a/framework/bigcommerce/api/utils/is-allowed-method.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next' - -export default function isAllowedMethod( - req: NextApiRequest, - res: NextApiResponse, - allowedMethods: string[] -) { - const methods = allowedMethods.includes('OPTIONS') - ? allowedMethods - : [...allowedMethods, 'OPTIONS'] - - if (!req.method || !methods.includes(req.method)) { - res.status(405) - res.setHeader('Allow', methods.join(', ')) - res.end() - return false - } - - if (req.method === 'OPTIONS') { - res.status(200) - res.setHeader('Allow', methods.join(', ')) - res.setHeader('Content-Length', '0') - res.end() - return false - } - - return true -} diff --git a/framework/bigcommerce/api/utils/parse-item.ts b/framework/bigcommerce/api/utils/parse-item.ts index 7c8cd47280..14c9ac53d6 100644 --- a/framework/bigcommerce/api/utils/parse-item.ts +++ b/framework/bigcommerce/api/utils/parse-item.ts @@ -1,5 +1,5 @@ -import type { ItemBody as WishlistItemBody } from '../wishlist' -import type { CartItemBody, OptionSelections } from '../../types' +import type { WishlistItemBody } from '../../types/wishlist' +import type { CartItemBody, OptionSelections } from '../../types/cart' type BCWishlistItemBody = { product_id: number diff --git a/framework/bigcommerce/api/utils/set-product-locale-meta.ts b/framework/bigcommerce/api/utils/set-product-locale-meta.ts index 974a197bdd..767286477e 100644 --- a/framework/bigcommerce/api/utils/set-product-locale-meta.ts +++ b/framework/bigcommerce/api/utils/set-product-locale-meta.ts @@ -1,4 +1,4 @@ -import type { ProductNode } from '../../product/get-all-products' +import type { ProductNode } from '../operations/get-all-products' import type { RecursivePartial } from './types' export default function setProductLocaleMeta( diff --git a/framework/bigcommerce/api/wishlist/index.ts b/framework/bigcommerce/api/wishlist/index.ts deleted file mode 100644 index 7c700689ce..0000000000 --- a/framework/bigcommerce/api/wishlist/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -import isAllowedMethod from '../utils/is-allowed-method' -import createApiHandler, { - BigcommerceApiHandler, - BigcommerceHandler, -} from '../utils/create-api-handler' -import { BigcommerceApiError } from '../utils/errors' -import type { - Wishlist, - WishlistItem, -} from '../../customer/get-customer-wishlist' -import getWishlist from './handlers/get-wishlist' -import addItem from './handlers/add-item' -import removeItem from './handlers/remove-item' -import type { Product, ProductVariant, Customer } from '@commerce/types' - -export type { Wishlist, WishlistItem } - -export type ItemBody = { - productId: Product['id'] - variantId: ProductVariant['id'] -} - -export type AddItemBody = { item: ItemBody } - -export type RemoveItemBody = { itemId: Product['id'] } - -export type WishlistBody = { - customer_id: Customer['entityId'] - is_public: number - name: string - items: any[] -} - -export type AddWishlistBody = { wishlist: WishlistBody } - -export type WishlistHandlers = { - getWishlist: BigcommerceHandler< - Wishlist, - { customerToken?: string; includeProducts?: boolean } - > - addItem: BigcommerceHandler< - Wishlist, - { customerToken?: string } & Partial - > - removeItem: BigcommerceHandler< - Wishlist, - { customerToken?: string } & Partial - > -} - -const METHODS = ['GET', 'POST', 'DELETE'] - -// TODO: a complete implementation should have schema validation for `req.body` -const wishlistApi: BigcommerceApiHandler = async ( - req, - res, - config, - handlers -) => { - if (!isAllowedMethod(req, res, METHODS)) return - - const { cookies } = req - const customerToken = cookies[config.customerCookie] - - try { - // Return current wishlist info - if (req.method === 'GET') { - const body = { - customerToken, - includeProducts: req.query.products === '1', - } - return await handlers['getWishlist']({ req, res, config, body }) - } - - // Add an item to the wishlist - if (req.method === 'POST') { - const body = { ...req.body, customerToken } - return await handlers['addItem']({ req, res, config, body }) - } - - // Remove an item from the wishlist - if (req.method === 'DELETE') { - const body = { ...req.body, customerToken } - return await handlers['removeItem']({ req, res, config, body }) - } - } catch (error) { - console.error(error) - - const message = - error instanceof BigcommerceApiError - ? 'An unexpected error ocurred with the Bigcommerce API' - : 'An unexpected error ocurred' - - res.status(500).json({ data: null, errors: [{ message }] }) - } -} - -export const handlers = { - getWishlist, - addItem, - removeItem, -} - -export default createApiHandler(wishlistApi, handlers, {}) diff --git a/framework/bigcommerce/auth/login.ts b/framework/bigcommerce/auth/login.ts deleted file mode 100644 index 3fef298797..0000000000 --- a/framework/bigcommerce/auth/login.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { ServerResponse } from 'http' -import type { LoginMutation, LoginMutationVariables } from '../schema' -import type { RecursivePartial } from '../api/utils/types' -import concatHeader from '../api/utils/concat-cookie' -import { BigcommerceConfig, getConfig } from '../api' - -export const loginMutation = /* GraphQL */ ` - mutation login($email: String!, $password: String!) { - login(email: $email, password: $password) { - result - } - } -` - -export type LoginResult = T - -export type LoginVariables = LoginMutationVariables - -async function login(opts: { - variables: LoginVariables - config?: BigcommerceConfig - res: ServerResponse -}): Promise - -async function login(opts: { - query: string - variables: V - res: ServerResponse - config?: BigcommerceConfig -}): Promise> - -async function login({ - query = loginMutation, - variables, - res: response, - config, -}: { - query?: string - variables: LoginVariables - res: ServerResponse - config?: BigcommerceConfig -}): Promise { - config = getConfig(config) - - const { data, res } = await config.fetch>( - query, - { variables } - ) - // Bigcommerce returns a Set-Cookie header with the auth cookie - let cookie = res.headers.get('Set-Cookie') - - if (cookie && typeof cookie === 'string') { - // In development, don't set a secure cookie or the browser will ignore it - if (process.env.NODE_ENV !== 'production') { - cookie = cookie.replace('; Secure', '') - // SameSite=none can't be set unless the cookie is Secure - // bc seems to sometimes send back SameSite=None rather than none so make - // this case insensitive - cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax') - } - - response.setHeader( - 'Set-Cookie', - concatHeader(response.getHeader('Set-Cookie'), cookie)! - ) - } - - return { - result: data.login?.result, - } -} - -export default login diff --git a/framework/bigcommerce/auth/use-login.tsx b/framework/bigcommerce/auth/use-login.tsx index 1be96a58cb..3ebacc9b74 100644 --- a/framework/bigcommerce/auth/use-login.tsx +++ b/framework/bigcommerce/auth/use-login.tsx @@ -2,14 +2,14 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' import { CommerceError } from '@commerce/utils/errors' import useLogin, { UseLogin } from '@commerce/auth/use-login' -import type { LoginBody } from '../api/customers/login' +import type { LoginHook } from '../types/login' import useCustomer from '../customer/use-customer' export default useLogin as UseLogin -export const handler: MutationHook = { +export const handler: MutationHook = { fetchOptions: { - url: '/api/bigcommerce/customers/login', + url: '/api/login', method: 'POST', }, async fetcher({ input: { email, password }, options, fetch }) { diff --git a/framework/bigcommerce/auth/use-logout.tsx b/framework/bigcommerce/auth/use-logout.tsx index 71015a1c1a..e75563e049 100644 --- a/framework/bigcommerce/auth/use-logout.tsx +++ b/framework/bigcommerce/auth/use-logout.tsx @@ -1,13 +1,14 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' import useLogout, { UseLogout } from '@commerce/auth/use-logout' +import type { LogoutHook } from '../types/logout' import useCustomer from '../customer/use-customer' export default useLogout as UseLogout -export const handler: MutationHook = { +export const handler: MutationHook = { fetchOptions: { - url: '/api/bigcommerce/customers/logout', + url: '/api/logout', method: 'GET', }, useHook: ({ fetch }) => () => { diff --git a/framework/bigcommerce/auth/use-signup.tsx b/framework/bigcommerce/auth/use-signup.tsx index 28f7024ef8..da06fd3ebd 100644 --- a/framework/bigcommerce/auth/use-signup.tsx +++ b/framework/bigcommerce/auth/use-signup.tsx @@ -2,14 +2,14 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' import { CommerceError } from '@commerce/utils/errors' import useSignup, { UseSignup } from '@commerce/auth/use-signup' -import type { SignupBody } from '../api/customers/signup' +import type { SignupHook } from '../types/signup' import useCustomer from '../customer/use-customer' export default useSignup as UseSignup -export const handler: MutationHook = { +export const handler: MutationHook = { fetchOptions: { - url: '/api/bigcommerce/customers/signup', + url: '/api/signup', method: 'POST', }, async fetcher({ diff --git a/framework/bigcommerce/cart/use-add-item.tsx b/framework/bigcommerce/cart/use-add-item.tsx index d74c235674..1ac6ac6f88 100644 --- a/framework/bigcommerce/cart/use-add-item.tsx +++ b/framework/bigcommerce/cart/use-add-item.tsx @@ -2,20 +2,14 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' import { CommerceError } from '@commerce/utils/errors' import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item' -import { normalizeCart } from '../lib/normalize' -import type { - Cart, - BigcommerceCart, - CartItemBody, - AddCartItemBody, -} from '../types' +import type { AddItemHook } from '@commerce/types/cart' import useCart from './use-cart' export default useAddItem as UseAddItem -export const handler: MutationHook = { +export const handler: MutationHook = { fetchOptions: { - url: '/api/bigcommerce/cart', + url: '/api/cart', method: 'POST', }, async fetcher({ input: item, options, fetch }) { @@ -28,12 +22,12 @@ export const handler: MutationHook = { }) } - const data = await fetch({ + const data = await fetch({ ...options, body: { item }, }) - return normalizeCart(data) + return data }, useHook: ({ fetch }) => () => { const { mutate } = useCart() diff --git a/framework/bigcommerce/cart/use-cart.tsx b/framework/bigcommerce/cart/use-cart.tsx index 2098e7431f..4ba1724d9d 100644 --- a/framework/bigcommerce/cart/use-cart.tsx +++ b/framework/bigcommerce/cart/use-cart.tsx @@ -1,25 +1,15 @@ import { useMemo } from 'react' import { SWRHook } from '@commerce/utils/types' -import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart' -import { normalizeCart } from '../lib/normalize' -import type { Cart } from '../types' +import useCart, { UseCart } from '@commerce/cart/use-cart' +import type { GetCartHook } from '@commerce/types/cart' export default useCart as UseCart -export const handler: SWRHook< - Cart | null, - {}, - FetchCartInput, - { isEmpty?: boolean } -> = { +export const handler: SWRHook = { fetchOptions: { - url: '/api/bigcommerce/cart', + url: '/api/cart', method: 'GET', }, - async fetcher({ input: { cartId }, options, fetch }) { - const data = cartId ? await fetch(options) : null - return data && normalizeCart(data) - }, useHook: ({ useData }) => (input) => { const response = useData({ swrOptions: { revalidateOnFocus: false, ...input?.swrOptions }, diff --git a/framework/bigcommerce/cart/use-remove-item.tsx b/framework/bigcommerce/cart/use-remove-item.tsx index 186780d6a2..1376f29ce4 100644 --- a/framework/bigcommerce/cart/use-remove-item.tsx +++ b/framework/bigcommerce/cart/use-remove-item.tsx @@ -4,48 +4,33 @@ import type { HookFetcherContext, } from '@commerce/utils/types' import { ValidationError } from '@commerce/utils/errors' -import useRemoveItem, { - RemoveItemInput as RemoveItemInputBase, - UseRemoveItem, -} from '@commerce/cart/use-remove-item' -import { normalizeCart } from '../lib/normalize' -import type { - RemoveCartItemBody, - Cart, - BigcommerceCart, - LineItem, -} from '../types' +import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item' +import type { Cart, LineItem, RemoveItemHook } from '@commerce/types/cart' import useCart from './use-cart' export type RemoveItemFn = T extends LineItem - ? (input?: RemoveItemInput) => Promise - : (input: RemoveItemInput) => Promise + ? (input?: RemoveItemActionInput) => Promise + : (input: RemoveItemActionInput) => Promise -export type RemoveItemInput = T extends LineItem - ? Partial - : RemoveItemInputBase +export type RemoveItemActionInput = T extends LineItem + ? Partial + : RemoveItemHook['actionInput'] export default useRemoveItem as UseRemoveItem export const handler = { fetchOptions: { - url: '/api/bigcommerce/cart', + url: '/api/cart', method: 'DELETE', }, async fetcher({ input: { itemId }, options, fetch, - }: HookFetcherContext) { - const data = await fetch({ - ...options, - body: { itemId }, - }) - return normalizeCart(data) + }: HookFetcherContext) { + return await fetch({ ...options, body: { itemId } }) }, - useHook: ({ - fetch, - }: MutationHookContext) => < + useHook: ({ fetch }: MutationHookContext) => < T extends LineItem | undefined = undefined >( ctx: { item?: T } = {} diff --git a/framework/bigcommerce/cart/use-update-item.tsx b/framework/bigcommerce/cart/use-update-item.tsx index f1840f806a..0f9f5754d3 100644 --- a/framework/bigcommerce/cart/use-update-item.tsx +++ b/framework/bigcommerce/cart/use-update-item.tsx @@ -5,36 +5,27 @@ import type { HookFetcherContext, } from '@commerce/utils/types' import { ValidationError } from '@commerce/utils/errors' -import useUpdateItem, { - UpdateItemInput as UpdateItemInputBase, - UseUpdateItem, -} from '@commerce/cart/use-update-item' -import { normalizeCart } from '../lib/normalize' -import type { - UpdateCartItemBody, - Cart, - BigcommerceCart, - LineItem, -} from '../types' +import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item' +import type { LineItem, UpdateItemHook } from '@commerce/types/cart' import { handler as removeItemHandler } from './use-remove-item' import useCart from './use-cart' -export type UpdateItemInput = T extends LineItem - ? Partial> - : UpdateItemInputBase +export type UpdateItemActionInput = T extends LineItem + ? Partial + : UpdateItemHook['actionInput'] export default useUpdateItem as UseUpdateItem export const handler = { fetchOptions: { - url: '/api/bigcommerce/cart', + url: '/api/cart', method: 'PUT', }, async fetcher({ input: { itemId, item }, options, fetch, - }: HookFetcherContext) { + }: HookFetcherContext) { if (Number.isInteger(item.quantity)) { // Also allow the update hook to remove an item if the quantity is lower than 1 if (item.quantity! < 1) { @@ -50,16 +41,12 @@ export const handler = { }) } - const data = await fetch({ + return await fetch({ ...options, body: { itemId, item }, }) - - return normalizeCart(data) }, - useHook: ({ - fetch, - }: MutationHookContext) => < + useHook: ({ fetch }: MutationHookContext) => < T extends LineItem | undefined = undefined >( ctx: { @@ -71,7 +58,7 @@ export const handler = { const { mutate } = useCart() as any return useCallback( - debounce(async (input: UpdateItemInput) => { + debounce(async (input: UpdateItemActionInput) => { const itemId = input.id ?? item?.id const productId = input.productId ?? item?.productId const variantId = input.productId ?? item?.variantId diff --git a/framework/bigcommerce/common/get-all-pages.ts b/framework/bigcommerce/common/get-all-pages.ts deleted file mode 100644 index dc5eb15a56..0000000000 --- a/framework/bigcommerce/common/get-all-pages.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' -import { BigcommerceConfig, getConfig } from '../api' -import { definitions } from '../api/definitions/store-content' - -export type Page = definitions['page_Full'] - -export type GetAllPagesResult< - T extends { pages: any[] } = { pages: Page[] } -> = T - -async function getAllPages(opts?: { - config?: BigcommerceConfig - preview?: boolean -}): Promise - -async function getAllPages(opts: { - url: string - config?: BigcommerceConfig - preview?: boolean -}): Promise> - -async function getAllPages({ - config, - preview, -}: { - url?: string - config?: BigcommerceConfig - preview?: boolean -} = {}): Promise { - config = getConfig(config) - // RecursivePartial forces the method to check for every prop in the data, which is - // required in case there's a custom `url` - const { data } = await config.storeApiFetch< - RecursivePartial<{ data: Page[] }> - >('/v3/content/pages') - const pages = (data as RecursiveRequired) ?? [] - - return { - pages: preview ? pages : pages.filter((p) => p.is_visible), - } -} - -export default getAllPages diff --git a/framework/bigcommerce/common/get-page.ts b/framework/bigcommerce/common/get-page.ts deleted file mode 100644 index 932032efb3..0000000000 --- a/framework/bigcommerce/common/get-page.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' -import { BigcommerceConfig, getConfig } from '../api' -import { definitions } from '../api/definitions/store-content' - -export type Page = definitions['page_Full'] - -export type GetPageResult = T - -export type PageVariables = { - id: number -} - -async function getPage(opts: { - url?: string - variables: PageVariables - config?: BigcommerceConfig - preview?: boolean -}): Promise - -async function getPage(opts: { - url: string - variables: V - config?: BigcommerceConfig - preview?: boolean -}): Promise> - -async function getPage({ - url, - variables, - config, - preview, -}: { - url?: string - variables: PageVariables - config?: BigcommerceConfig - preview?: boolean -}): Promise { - config = getConfig(config) - // RecursivePartial forces the method to check for every prop in the data, which is - // required in case there's a custom `url` - const { data } = await config.storeApiFetch< - RecursivePartial<{ data: Page[] }> - >(url || `/v3/content/pages?id=${variables.id}&include=body`) - const firstPage = data?.[0] - const page = firstPage as RecursiveRequired - - if (preview || page?.is_visible) { - return { page } - } - return {} -} - -export default getPage diff --git a/framework/bigcommerce/common/get-site-info.ts b/framework/bigcommerce/common/get-site-info.ts deleted file mode 100644 index a39608d386..0000000000 --- a/framework/bigcommerce/common/get-site-info.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../schema' -import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' -import filterEdges from '../api/utils/filter-edges' -import { BigcommerceConfig, getConfig } from '../api' -import { categoryTreeItemFragment } from '../api/fragments/category-tree' -import { Category } from '@commerce/types' -import getSlug from '@lib/get-slug' - -// Get 3 levels of categories -export const getSiteInfoQuery = /* GraphQL */ ` - query getSiteInfo { - site { - categoryTree { - ...categoryTreeItem - children { - ...categoryTreeItem - children { - ...categoryTreeItem - } - } - } - brands { - pageInfo { - startCursor - endCursor - } - edges { - cursor - node { - entityId - name - defaultImage { - urlOriginal - altText - } - pageTitle - metaDesc - metaKeywords - searchKeywords - path - } - } - } - } - } - ${categoryTreeItemFragment} -` - -export type CategoriesTree = NonNullable< - GetSiteInfoQuery['site']['categoryTree'] -> - -export type BrandEdge = NonNullable< - NonNullable[0] -> - -export type Brands = BrandEdge[] - -export type GetSiteInfoResult< - T extends { categories: any[]; brands: any[] } = { - categories: Category[] - brands: Brands - } -> = T - -async function getSiteInfo(opts?: { - variables?: GetSiteInfoQueryVariables - config?: BigcommerceConfig - preview?: boolean -}): Promise - -async function getSiteInfo< - T extends { categories: Category[]; brands: any[] }, - V = any ->(opts: { - query: string - variables?: V - config?: BigcommerceConfig - preview?: boolean -}): Promise> - -async function getSiteInfo({ - query = getSiteInfoQuery, - variables, - config, -}: { - query?: string - variables?: GetSiteInfoQueryVariables - config?: BigcommerceConfig - preview?: boolean -} = {}): Promise { - config = getConfig(config) - // RecursivePartial forces the method to check for every prop in the data, which is - // required in case there's a custom `query` - const { data } = await config.fetch>( - query, - { variables } - ) - - let categories = data!.site!.categoryTree?.map( - ({ entityId, name, path }: any) => ({ - id: `${entityId}`, - name, - slug: getSlug(path), - path, - }) - ) - - const brands = data.site?.brands?.edges - - return { - categories: categories ?? [], - brands: filterEdges(brands as RecursiveRequired), - } -} - -export default getSiteInfo diff --git a/framework/bigcommerce/customer/get-customer-wishlist.ts b/framework/bigcommerce/customer/get-customer-wishlist.ts deleted file mode 100644 index 97e5654a9d..0000000000 --- a/framework/bigcommerce/customer/get-customer-wishlist.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' -import { definitions } from '../api/definitions/wishlist' -import { BigcommerceConfig, getConfig } from '../api' -import getAllProducts, { ProductEdge } from '../product/get-all-products' - -export type Wishlist = Omit & { - items?: WishlistItem[] -} - -export type WishlistItem = NonNullable< - definitions['wishlist_Full']['items'] ->[0] & { - product?: ProductEdge['node'] -} - -export type GetCustomerWishlistResult< - T extends { wishlist?: any } = { wishlist?: Wishlist } -> = T - -export type GetCustomerWishlistVariables = { - customerId: number -} - -async function getCustomerWishlist(opts: { - variables: GetCustomerWishlistVariables - config?: BigcommerceConfig - includeProducts?: boolean -}): Promise - -async function getCustomerWishlist< - T extends { wishlist?: any }, - V = any ->(opts: { - url: string - variables: V - config?: BigcommerceConfig - includeProducts?: boolean -}): Promise> - -async function getCustomerWishlist({ - config, - variables, - includeProducts, -}: { - url?: string - variables: GetCustomerWishlistVariables - config?: BigcommerceConfig - includeProducts?: boolean -}): Promise { - config = getConfig(config) - - const { data = [] } = await config.storeApiFetch< - RecursivePartial<{ data: Wishlist[] }> - >(`/v3/wishlists?customer_id=${variables.customerId}`) - const wishlist = data[0] - - if (includeProducts && wishlist?.items?.length) { - const entityIds = wishlist.items - ?.map((item) => item?.product_id) - .filter((id): id is number => !!id) - - if (entityIds?.length) { - const graphqlData = await getAllProducts({ - variables: { first: 100, entityIds }, - config, - }) - // Put the products in an object that we can use to get them by id - const productsById = graphqlData.products.reduce<{ - [k: number]: ProductEdge - }>((prods, p) => { - prods[Number(p.id)] = p as any - return prods - }, {}) - // Populate the wishlist items with the graphql products - wishlist.items.forEach((item) => { - const product = item && productsById[item.product_id!] - if (item && product) { - // @ts-ignore Fix this type when the wishlist type is properly defined - item.product = product - } - }) - } - } - - return { wishlist: wishlist as RecursiveRequired } -} - -export default getCustomerWishlist diff --git a/framework/bigcommerce/customer/use-customer.tsx b/framework/bigcommerce/customer/use-customer.tsx index 0930078240..238b1229b4 100644 --- a/framework/bigcommerce/customer/use-customer.tsx +++ b/framework/bigcommerce/customer/use-customer.tsx @@ -1,16 +1,16 @@ import { SWRHook } from '@commerce/utils/types' import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' -import type { Customer, CustomerData } from '../api/customers' +import type { CustomerHook } from '../types/customer' export default useCustomer as UseCustomer -export const handler: SWRHook = { +export const handler: SWRHook = { fetchOptions: { - url: '/api/bigcommerce/customers', + url: '/api/customer', method: 'GET', }, async fetcher({ options, fetch }) { - const data = await fetch(options) + const data = await fetch(options) return data?.customer ?? null }, useHook: ({ useData }) => (input) => { diff --git a/framework/bigcommerce/lib/get-slug.ts b/framework/bigcommerce/lib/get-slug.ts new file mode 100644 index 0000000000..329c5a27e4 --- /dev/null +++ b/framework/bigcommerce/lib/get-slug.ts @@ -0,0 +1,5 @@ +// Remove trailing and leading slash, usually included in nodes +// returned by the BigCommerce API +const getSlug = (path: string) => path.replace(/^\/|\/$/g, '') + +export default getSlug diff --git a/framework/bigcommerce/lib/normalize.ts b/framework/bigcommerce/lib/normalize.ts index cc7606099b..cd1c3ce5aa 100644 --- a/framework/bigcommerce/lib/normalize.ts +++ b/framework/bigcommerce/lib/normalize.ts @@ -1,6 +1,10 @@ -import type { Product } from '@commerce/types' -import type { Cart, BigcommerceCart, LineItem } from '../types' +import type { Product } from '../types/product' +import type { Cart, BigcommerceCart, LineItem } from '../types/cart' +import type { Page } from '../types/page' +import type { BCCategory, Category } from '../types/site' +import { definitions } from '../api/definitions/store-content' import update from './immutability' +import getSlug from './get-slug' function normalizeProductOption(productOption: any) { const { @@ -69,6 +73,16 @@ export function normalizeProduct(productNode: any): Product { }) } +export function normalizePage(page: definitions['page_Full']): Page { + return { + id: String(page.id), + name: page.name, + is_visible: page.is_visible, + sort_order: page.sort_order, + body: page.body, + } +} + export function normalizeCart(data: BigcommerceCart): Cart { return { id: data.id, @@ -111,3 +125,12 @@ function normalizeLineItem(item: any): LineItem { })), } } + +export function normalizeCategory(category: BCCategory): Category { + return { + id: `${category.entityId}`, + name: category.name, + slug: getSlug(category.path), + path: category.path, + } +} diff --git a/framework/bigcommerce/product/get-all-product-paths.ts b/framework/bigcommerce/product/get-all-product-paths.ts deleted file mode 100644 index c1b23b38d2..0000000000 --- a/framework/bigcommerce/product/get-all-product-paths.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { - GetAllProductPathsQuery, - GetAllProductPathsQueryVariables, -} from '../schema' -import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' -import filterEdges from '../api/utils/filter-edges' -import { BigcommerceConfig, getConfig } from '../api' - -export const getAllProductPathsQuery = /* GraphQL */ ` - query getAllProductPaths($first: Int = 100) { - site { - products(first: $first) { - edges { - node { - path - } - } - } - } - } -` - -export type ProductPath = NonNullable< - NonNullable[0] -> - -export type ProductPaths = ProductPath[] - -export type { GetAllProductPathsQueryVariables } - -export type GetAllProductPathsResult< - T extends { products: any[] } = { products: ProductPaths } -> = T - -async function getAllProductPaths(opts?: { - variables?: GetAllProductPathsQueryVariables - config?: BigcommerceConfig -}): Promise - -async function getAllProductPaths< - T extends { products: any[] }, - V = any ->(opts: { - query: string - variables?: V - config?: BigcommerceConfig -}): Promise> - -async function getAllProductPaths({ - query = getAllProductPathsQuery, - variables, - config, -}: { - query?: string - variables?: GetAllProductPathsQueryVariables - config?: BigcommerceConfig -} = {}): Promise { - config = getConfig(config) - // RecursivePartial forces the method to check for every prop in the data, which is - // required in case there's a custom `query` - const { data } = await config.fetch< - RecursivePartial - >(query, { variables }) - const products = data.site?.products?.edges - - return { - products: filterEdges(products as RecursiveRequired), - } -} - -export default getAllProductPaths diff --git a/framework/bigcommerce/product/get-all-products.ts b/framework/bigcommerce/product/get-all-products.ts deleted file mode 100644 index 4c563bc62d..0000000000 --- a/framework/bigcommerce/product/get-all-products.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { - GetAllProductsQuery, - GetAllProductsQueryVariables, -} from '../schema' -import type { Product } from '@commerce/types' -import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' -import filterEdges from '../api/utils/filter-edges' -import setProductLocaleMeta from '../api/utils/set-product-locale-meta' -import { productConnectionFragment } from '../api/fragments/product' -import { BigcommerceConfig, getConfig } from '../api' -import { normalizeProduct } from '../lib/normalize' - -export const getAllProductsQuery = /* GraphQL */ ` - query getAllProducts( - $hasLocale: Boolean = false - $locale: String = "null" - $entityIds: [Int!] - $first: Int = 10 - $products: Boolean = false - $featuredProducts: Boolean = false - $bestSellingProducts: Boolean = false - $newestProducts: Boolean = false - ) { - site { - products(first: $first, entityIds: $entityIds) @include(if: $products) { - ...productConnnection - } - featuredProducts(first: $first) @include(if: $featuredProducts) { - ...productConnnection - } - bestSellingProducts(first: $first) @include(if: $bestSellingProducts) { - ...productConnnection - } - newestProducts(first: $first) @include(if: $newestProducts) { - ...productConnnection - } - } - } - - ${productConnectionFragment} -` - -export type ProductEdge = NonNullable< - NonNullable[0] -> - -export type ProductNode = ProductEdge['node'] - -export type GetAllProductsResult< - T extends Record = { - products: ProductEdge[] - } -> = T - -const FIELDS = [ - 'products', - 'featuredProducts', - 'bestSellingProducts', - 'newestProducts', -] - -export type ProductTypes = - | 'products' - | 'featuredProducts' - | 'bestSellingProducts' - | 'newestProducts' - -export type ProductVariables = { field?: ProductTypes } & Omit< - GetAllProductsQueryVariables, - ProductTypes | 'hasLocale' -> - -async function getAllProducts(opts?: { - variables?: ProductVariables - config?: BigcommerceConfig - preview?: boolean -}): Promise<{ products: Product[] }> - -async function getAllProducts< - T extends Record, - V = any ->(opts: { - query: string - variables?: V - config?: BigcommerceConfig - preview?: boolean -}): Promise> - -async function getAllProducts({ - query = getAllProductsQuery, - variables: { field = 'products', ...vars } = {}, - config, -}: { - query?: string - variables?: ProductVariables - config?: BigcommerceConfig - preview?: boolean - // TODO: fix the product type here -} = {}): Promise<{ products: Product[] | any[] }> { - config = getConfig(config) - - const locale = vars.locale || config.locale - const variables: GetAllProductsQueryVariables = { - ...vars, - locale, - hasLocale: !!locale, - } - - if (!FIELDS.includes(field)) { - throw new Error( - `The field variable has to match one of ${FIELDS.join(', ')}` - ) - } - - variables[field] = true - - // RecursivePartial forces the method to check for every prop in the data, which is - // required in case there's a custom `query` - const { data } = await config.fetch>( - query, - { variables } - ) - const edges = data.site?.[field]?.edges - const products = filterEdges(edges as RecursiveRequired) - - if (locale && config.applyLocale) { - products.forEach((product: RecursivePartial) => { - if (product.node) setProductLocaleMeta(product.node) - }) - } - - return { products: products.map(({ node }) => normalizeProduct(node as any)) } -} - -export default getAllProducts diff --git a/framework/bigcommerce/product/get-product.ts b/framework/bigcommerce/product/get-product.ts deleted file mode 100644 index b52568b623..0000000000 --- a/framework/bigcommerce/product/get-product.ts +++ /dev/null @@ -1,121 +0,0 @@ -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 '../lib/normalize' -import type { Product } from '@commerce/types' - -export const getProductQuery = /* GraphQL */ ` - query getProduct( - $hasLocale: Boolean = false - $locale: String = "null" - $path: String! - ) { - site { - route(path: $path) { - node { - __typename - ... on Product { - ...productInfo - variants { - edges { - node { - entityId - defaultImage { - urlOriginal - altText - isDefault - } - prices { - ...productPrices - } - inventory { - aggregated { - availableToSell - warningLevel - } - isInStock - } - productOptions { - edges { - node { - __typename - entityId - displayName - ...multipleChoiceOption - } - } - } - } - } - } - } - } - } - } - } - - ${productInfoFragment} -` - -export type ProductNode = Extract< - GetProductQuery['site']['route']['node'], - { __typename: 'Product' } -> - -export type GetProductResult< - T extends { product?: any } = { product?: ProductNode } -> = T - -export type ProductVariables = { locale?: string } & ( - | { path: string; slug?: never } - | { path?: never; slug: string } -) - -async function getProduct(opts: { - variables: ProductVariables - config?: BigcommerceConfig - preview?: boolean -}): Promise - -async function getProduct(opts: { - query: string - variables: V - config?: BigcommerceConfig - preview?: boolean -}): Promise> - -async function getProduct({ - query = getProductQuery, - variables: { slug, ...vars }, - config, -}: { - query?: string - variables: ProductVariables - config?: BigcommerceConfig - preview?: boolean -}): Promise { - config = getConfig(config) - - const locale = vars.locale || config.locale - const variables: GetProductQueryVariables = { - ...vars, - locale, - hasLocale: !!locale, - path: slug ? `/${slug}/` : vars.path!, - } - const { data } = await config.fetch(query, { variables }) - const product = data.site?.route?.node - - if (product?.__typename === 'Product') { - if (locale && config.applyLocale) { - setProductLocaleMeta(product) - } - - return { product: normalizeProduct(product as any) } - } - - return {} -} - -export default getProduct diff --git a/framework/bigcommerce/product/index.ts b/framework/bigcommerce/product/index.ts index b290c189f0..426a3edcd5 100644 --- a/framework/bigcommerce/product/index.ts +++ b/framework/bigcommerce/product/index.ts @@ -1,4 +1,2 @@ export { default as usePrice } from './use-price' export { default as useSearch } from './use-search' -export { default as getProduct } from './get-product' -export { default as getAllProducts } from './get-all-products' diff --git a/framework/bigcommerce/product/use-search.tsx b/framework/bigcommerce/product/use-search.tsx index ff0ed848ce..bea01753b2 100644 --- a/framework/bigcommerce/product/use-search.tsx +++ b/framework/bigcommerce/product/use-search.tsx @@ -1,6 +1,6 @@ import { SWRHook } from '@commerce/utils/types' import useSearch, { UseSearch } from '@commerce/product/use-search' -import type { SearchProductsData } from '../api/catalog/products' +import type { SearchProductsHook } from '../types/product' export default useSearch as UseSearch @@ -9,15 +9,12 @@ export type SearchProductsInput = { categoryId?: number | string brandId?: number sort?: string + locale?: string } -export const handler: SWRHook< - SearchProductsData, - SearchProductsInput, - SearchProductsInput -> = { +export const handler: SWRHook = { fetchOptions: { - url: '/api/bigcommerce/catalog/products', + url: '/api/catalog/products', method: 'GET', }, fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) { @@ -26,9 +23,9 @@ export const handler: SWRHook< if (search) url.searchParams.set('search', search) if (Number.isInteger(categoryId)) - url.searchParams.set('category', String(categoryId)) + url.searchParams.set('categoryId', String(categoryId)) if (Number.isInteger(brandId)) - url.searchParams.set('brand', String(brandId)) + url.searchParams.set('brandId', String(brandId)) if (sort) url.searchParams.set('sort', sort) return fetch({ diff --git a/framework/bigcommerce/types.ts b/framework/bigcommerce/types/cart.ts similarity index 52% rename from framework/bigcommerce/types.ts rename to framework/bigcommerce/types/cart.ts index beeab0223e..83076ea09f 100644 --- a/framework/bigcommerce/types.ts +++ b/framework/bigcommerce/types/cart.ts @@ -1,4 +1,6 @@ -import * as Core from '@commerce/types' +import * as Core from '@commerce/types/cart' + +export * from '@commerce/types/cart' // TODO: this type should match: // https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses @@ -23,16 +25,14 @@ export type BigcommerceCart = { // TODO: add missing fields } -export type Cart = Core.Cart & { - lineItems: LineItem[] -} - -export type LineItem = Core.LineItem - /** - * Cart mutations + * Extend core cart types */ +export type Cart = Core.Cart & { + lineItems: Core.LineItem[] +} + export type OptionSelections = { option_id: number option_value: number | string @@ -43,16 +43,24 @@ export type CartItemBody = Core.CartItemBody & { optionSelections?: OptionSelections } -export type GetCartHandlerBody = Core.GetCartHandlerBody - -export type AddCartItemBody = Core.AddCartItemBody +export type CartTypes = { + cart: Cart + item: Core.LineItem + itemBody: CartItemBody +} -export type AddCartItemHandlerBody = Core.AddCartItemHandlerBody +export type CartHooks = Core.CartHooks -export type UpdateCartItemBody = Core.UpdateCartItemBody +export type GetCartHook = CartHooks['getCart'] +export type AddItemHook = CartHooks['addItem'] +export type UpdateItemHook = CartHooks['updateItem'] +export type RemoveItemHook = CartHooks['removeItem'] -export type UpdateCartItemHandlerBody = Core.UpdateCartItemHandlerBody +export type CartSchema = Core.CartSchema -export type RemoveCartItemBody = Core.RemoveCartItemBody +export type CartHandlers = Core.CartHandlers -export type RemoveCartItemHandlerBody = Core.RemoveCartItemHandlerBody +export type GetCartHandler = CartHandlers['getCart'] +export type AddItemHandler = CartHandlers['addItem'] +export type UpdateItemHandler = CartHandlers['updateItem'] +export type RemoveItemHandler = CartHandlers['removeItem'] diff --git a/framework/bigcommerce/types/checkout.ts b/framework/bigcommerce/types/checkout.ts new file mode 100644 index 0000000000..4e2412ef6c --- /dev/null +++ b/framework/bigcommerce/types/checkout.ts @@ -0,0 +1 @@ +export * from '@commerce/types/checkout' diff --git a/framework/bigcommerce/types/common.ts b/framework/bigcommerce/types/common.ts new file mode 100644 index 0000000000..b52c33a4de --- /dev/null +++ b/framework/bigcommerce/types/common.ts @@ -0,0 +1 @@ +export * from '@commerce/types/common' diff --git a/framework/bigcommerce/types/customer.ts b/framework/bigcommerce/types/customer.ts new file mode 100644 index 0000000000..427bc0b03c --- /dev/null +++ b/framework/bigcommerce/types/customer.ts @@ -0,0 +1,5 @@ +import * as Core from '@commerce/types/customer' + +export * from '@commerce/types/customer' + +export type CustomerSchema = Core.CustomerSchema diff --git a/framework/bigcommerce/types/index.ts b/framework/bigcommerce/types/index.ts new file mode 100644 index 0000000000..7ab0b7f64f --- /dev/null +++ b/framework/bigcommerce/types/index.ts @@ -0,0 +1,25 @@ +import * as Cart from './cart' +import * as Checkout from './checkout' +import * as Common from './common' +import * as Customer from './customer' +import * as Login from './login' +import * as Logout from './logout' +import * as Page from './page' +import * as Product from './product' +import * as Signup from './signup' +import * as Site from './site' +import * as Wishlist from './wishlist' + +export type { + Cart, + Checkout, + Common, + Customer, + Login, + Logout, + Page, + Product, + Signup, + Site, + Wishlist, +} diff --git a/framework/bigcommerce/types/login.ts b/framework/bigcommerce/types/login.ts new file mode 100644 index 0000000000..24d5077ffd --- /dev/null +++ b/framework/bigcommerce/types/login.ts @@ -0,0 +1,8 @@ +import * as Core from '@commerce/types/login' +import type { LoginMutationVariables } from '../schema' + +export * from '@commerce/types/login' + +export type LoginOperation = Core.LoginOperation & { + variables: LoginMutationVariables +} diff --git a/framework/bigcommerce/types/logout.ts b/framework/bigcommerce/types/logout.ts new file mode 100644 index 0000000000..9f0a466afb --- /dev/null +++ b/framework/bigcommerce/types/logout.ts @@ -0,0 +1 @@ +export * from '@commerce/types/logout' diff --git a/framework/bigcommerce/types/page.ts b/framework/bigcommerce/types/page.ts new file mode 100644 index 0000000000..2bccfade2b --- /dev/null +++ b/framework/bigcommerce/types/page.ts @@ -0,0 +1,11 @@ +import * as Core from '@commerce/types/page' +export * from '@commerce/types/page' + +export type Page = Core.Page + +export type PageTypes = { + page: Page +} + +export type GetAllPagesOperation = Core.GetAllPagesOperation +export type GetPageOperation = Core.GetPageOperation diff --git a/framework/bigcommerce/types/product.ts b/framework/bigcommerce/types/product.ts new file mode 100644 index 0000000000..c776d58fa3 --- /dev/null +++ b/framework/bigcommerce/types/product.ts @@ -0,0 +1 @@ +export * from '@commerce/types/product' diff --git a/framework/bigcommerce/types/signup.ts b/framework/bigcommerce/types/signup.ts new file mode 100644 index 0000000000..58543c6f65 --- /dev/null +++ b/framework/bigcommerce/types/signup.ts @@ -0,0 +1 @@ +export * from '@commerce/types/signup' diff --git a/framework/bigcommerce/types/site.ts b/framework/bigcommerce/types/site.ts new file mode 100644 index 0000000000..12dd7038c8 --- /dev/null +++ b/framework/bigcommerce/types/site.ts @@ -0,0 +1,19 @@ +import * as Core from '@commerce/types/site' +import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../schema' + +export * from '@commerce/types/site' + +export type BCCategory = NonNullable< + GetSiteInfoQuery['site']['categoryTree'] +>[0] + +export type Brand = NonNullable< + NonNullable[0] +> + +export type SiteTypes = { + category: Core.Category + brand: Brand +} + +export type GetSiteInfoOperation = Core.GetSiteInfoOperation diff --git a/framework/bigcommerce/types/wishlist.ts b/framework/bigcommerce/types/wishlist.ts new file mode 100644 index 0000000000..1e148b88c7 --- /dev/null +++ b/framework/bigcommerce/types/wishlist.ts @@ -0,0 +1,23 @@ +import * as Core from '@commerce/types/wishlist' +import { definitions } from '../api/definitions/wishlist' +import type { ProductEdge } from '../api/operations/get-all-products' + +export * from '@commerce/types/wishlist' + +export type WishlistItem = NonNullable< + definitions['wishlist_Full']['items'] +>[0] & { + product?: ProductEdge['node'] +} + +export type Wishlist = Omit & { + items?: WishlistItem[] +} + +export type WishlistTypes = { + wishlist: Wishlist + itemBody: Core.WishlistItemBody +} + +export type WishlistSchema = Core.WishlistSchema +export type GetCustomerWishlistOperation = Core.GetCustomerWishlistOperation diff --git a/framework/bigcommerce/wishlist/use-add-item.tsx b/framework/bigcommerce/wishlist/use-add-item.tsx index 402e7da8b3..1bf086731b 100644 --- a/framework/bigcommerce/wishlist/use-add-item.tsx +++ b/framework/bigcommerce/wishlist/use-add-item.tsx @@ -2,15 +2,15 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' import { CommerceError } from '@commerce/utils/errors' import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item' -import type { ItemBody, AddItemBody } from '../api/wishlist' +import type { AddItemHook } from '../types/wishlist' import useCustomer from '../customer/use-customer' import useWishlist from './use-wishlist' export default useAddItem as UseAddItem -export const handler: MutationHook = { +export const handler: MutationHook = { fetchOptions: { - url: '/api/bigcommerce/wishlist', + url: '/api/wishlist', method: 'POST', }, useHook: ({ fetch }) => () => { diff --git a/framework/bigcommerce/wishlist/use-remove-item.tsx b/framework/bigcommerce/wishlist/use-remove-item.tsx index 622f321db9..9d25c14391 100644 --- a/framework/bigcommerce/wishlist/use-remove-item.tsx +++ b/framework/bigcommerce/wishlist/use-remove-item.tsx @@ -2,23 +2,17 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' import { CommerceError } from '@commerce/utils/errors' import useRemoveItem, { - RemoveItemInput, UseRemoveItem, } from '@commerce/wishlist/use-remove-item' -import type { RemoveItemBody, Wishlist } from '../api/wishlist' +import type { RemoveItemHook } from '../types/wishlist' import useCustomer from '../customer/use-customer' -import useWishlist, { UseWishlistInput } from './use-wishlist' +import useWishlist from './use-wishlist' export default useRemoveItem as UseRemoveItem -export const handler: MutationHook< - Wishlist | null, - { wishlist?: UseWishlistInput }, - RemoveItemInput, - RemoveItemBody -> = { +export const handler: MutationHook = { fetchOptions: { - url: '/api/bigcommerce/wishlist', + url: '/api/wishlist', method: 'DELETE', }, useHook: ({ fetch }) => ({ wishlist } = {}) => { diff --git a/framework/bigcommerce/wishlist/use-wishlist.tsx b/framework/bigcommerce/wishlist/use-wishlist.tsx index 4850d1cd98..b8fc946e34 100644 --- a/framework/bigcommerce/wishlist/use-wishlist.tsx +++ b/framework/bigcommerce/wishlist/use-wishlist.tsx @@ -1,21 +1,14 @@ import { useMemo } from 'react' import { SWRHook } from '@commerce/utils/types' import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist' -import type { Wishlist } from '../api/wishlist' +import type { GetWishlistHook } from '../types/wishlist' import useCustomer from '../customer/use-customer' -export type UseWishlistInput = { includeProducts?: boolean } - export default useWishlist as UseWishlist -export const handler: SWRHook< - Wishlist | null, - UseWishlistInput, - { customerId?: number } & UseWishlistInput, - { isEmpty?: boolean } -> = { +export const handler: SWRHook = { fetchOptions: { - url: '/api/bigcommerce/wishlist', + url: '/api/wishlist', method: 'GET', }, async fetcher({ input: { customerId, includeProducts }, options, fetch }) { diff --git a/framework/commerce/api/endpoints/cart.ts b/framework/commerce/api/endpoints/cart.ts new file mode 100644 index 0000000000..ca39e7da33 --- /dev/null +++ b/framework/commerce/api/endpoints/cart.ts @@ -0,0 +1,62 @@ +import type { CartSchema } from '../../types/cart' +import { CommerceAPIError } from '../utils/errors' +import isAllowedOperation from '../utils/is-allowed-operation' +import type { GetAPISchema } from '..' + +const cartEndpoint: GetAPISchema< + any, + CartSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers, config } = ctx + + if ( + !isAllowedOperation(req, res, { + GET: handlers['getCart'], + POST: handlers['addItem'], + PUT: handlers['updateItem'], + DELETE: handlers['removeItem'], + }) + ) { + return + } + + const { cookies } = req + const cartId = cookies[config.cartCookie] + + try { + // Return current cart info + if (req.method === 'GET') { + const body = { cartId } + return await handlers['getCart']({ ...ctx, body }) + } + + // Create or add an item to the cart + if (req.method === 'POST') { + const body = { ...req.body, cartId } + return await handlers['addItem']({ ...ctx, body }) + } + + // Update item in cart + if (req.method === 'PUT') { + const body = { ...req.body, cartId } + return await handlers['updateItem']({ ...ctx, body }) + } + + // Remove an item from the cart + if (req.method === 'DELETE') { + const body = { ...req.body, cartId } + return await handlers['removeItem']({ ...ctx, body }) + } + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default cartEndpoint diff --git a/framework/commerce/api/endpoints/catalog/products.ts b/framework/commerce/api/endpoints/catalog/products.ts new file mode 100644 index 0000000000..d2a4794bec --- /dev/null +++ b/framework/commerce/api/endpoints/catalog/products.ts @@ -0,0 +1,31 @@ +import type { ProductsSchema } from '../../../types/product' +import { CommerceAPIError } from '../../utils/errors' +import isAllowedOperation from '../../utils/is-allowed-operation' +import type { GetAPISchema } from '../..' + +const productsEndpoint: GetAPISchema< + any, + ProductsSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers } = ctx + + if (!isAllowedOperation(req, res, { GET: handlers['getProducts'] })) { + return + } + + try { + const body = req.query + return await handlers['getProducts']({ ...ctx, body }) + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default productsEndpoint diff --git a/framework/commerce/api/endpoints/checkout.ts b/framework/commerce/api/endpoints/checkout.ts new file mode 100644 index 0000000000..b39239a6a6 --- /dev/null +++ b/framework/commerce/api/endpoints/checkout.ts @@ -0,0 +1,35 @@ +import type { CheckoutSchema } from '../../types/checkout' +import { CommerceAPIError } from '../utils/errors' +import isAllowedOperation from '../utils/is-allowed-operation' +import type { GetAPISchema } from '..' + +const checkoutEndpoint: GetAPISchema< + any, + CheckoutSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers } = ctx + + if ( + !isAllowedOperation(req, res, { + GET: handlers['checkout'], + }) + ) { + return + } + + try { + const body = null + return await handlers['checkout']({ ...ctx, body }) + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default checkoutEndpoint diff --git a/framework/commerce/api/endpoints/customer.ts b/framework/commerce/api/endpoints/customer.ts new file mode 100644 index 0000000000..6372c494f1 --- /dev/null +++ b/framework/commerce/api/endpoints/customer.ts @@ -0,0 +1,35 @@ +import type { CustomerSchema } from '../../types/customer' +import { CommerceAPIError } from '../utils/errors' +import isAllowedOperation from '../utils/is-allowed-operation' +import type { GetAPISchema } from '..' + +const customerEndpoint: GetAPISchema< + any, + CustomerSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers } = ctx + + if ( + !isAllowedOperation(req, res, { + GET: handlers['getLoggedInCustomer'], + }) + ) { + return + } + + try { + const body = null + return await handlers['getLoggedInCustomer']({ ...ctx, body }) + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default customerEndpoint diff --git a/framework/commerce/api/endpoints/login.ts b/framework/commerce/api/endpoints/login.ts new file mode 100644 index 0000000000..bc071b7517 --- /dev/null +++ b/framework/commerce/api/endpoints/login.ts @@ -0,0 +1,35 @@ +import type { LoginSchema } from '../../types/login' +import { CommerceAPIError } from '../utils/errors' +import isAllowedOperation from '../utils/is-allowed-operation' +import type { GetAPISchema } from '..' + +const loginEndpoint: GetAPISchema< + any, + LoginSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers } = ctx + + if ( + !isAllowedOperation(req, res, { + POST: handlers['login'], + }) + ) { + return + } + + try { + const body = req.body ?? {} + return await handlers['login']({ ...ctx, body }) + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default loginEndpoint diff --git a/framework/commerce/api/endpoints/logout.ts b/framework/commerce/api/endpoints/logout.ts new file mode 100644 index 0000000000..8da11acb0f --- /dev/null +++ b/framework/commerce/api/endpoints/logout.ts @@ -0,0 +1,37 @@ +import type { LogoutSchema } from '../../types/logout' +import { CommerceAPIError } from '../utils/errors' +import isAllowedOperation from '../utils/is-allowed-operation' +import type { GetAPISchema } from '..' + +const logoutEndpoint: GetAPISchema< + any, + LogoutSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers } = ctx + + if ( + !isAllowedOperation(req, res, { + GET: handlers['logout'], + }) + ) { + return + } + + try { + const redirectTo = req.query.redirect_to + const body = typeof redirectTo === 'string' ? { redirectTo } : {} + + return await handlers['logout']({ ...ctx, body }) + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default logoutEndpoint diff --git a/framework/commerce/api/endpoints/signup.ts b/framework/commerce/api/endpoints/signup.ts new file mode 100644 index 0000000000..aa73ae7398 --- /dev/null +++ b/framework/commerce/api/endpoints/signup.ts @@ -0,0 +1,38 @@ +import type { SignupSchema } from '../../types/signup' +import { CommerceAPIError } from '../utils/errors' +import isAllowedOperation from '../utils/is-allowed-operation' +import type { GetAPISchema } from '..' + +const signupEndpoint: GetAPISchema< + any, + SignupSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers, config } = ctx + + if ( + !isAllowedOperation(req, res, { + POST: handlers['signup'], + }) + ) { + return + } + + const { cookies } = req + const cartId = cookies[config.cartCookie] + + try { + const body = { ...req.body, cartId } + return await handlers['signup']({ ...ctx, body }) + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default signupEndpoint diff --git a/framework/commerce/api/endpoints/wishlist.ts b/framework/commerce/api/endpoints/wishlist.ts new file mode 100644 index 0000000000..233ac52945 --- /dev/null +++ b/framework/commerce/api/endpoints/wishlist.ts @@ -0,0 +1,58 @@ +import type { WishlistSchema } from '../../types/wishlist' +import { CommerceAPIError } from '../utils/errors' +import isAllowedOperation from '../utils/is-allowed-operation' +import type { GetAPISchema } from '..' + +const wishlistEndpoint: GetAPISchema< + any, + WishlistSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers, config } = ctx + + if ( + !isAllowedOperation(req, res, { + GET: handlers['getWishlist'], + POST: handlers['addItem'], + DELETE: handlers['removeItem'], + }) + ) { + return + } + + const { cookies } = req + const customerToken = cookies[config.customerCookie] + + try { + // Return current wishlist info + if (req.method === 'GET') { + const body = { + customerToken, + includeProducts: req.query.products === '1', + } + return await handlers['getWishlist']({ ...ctx, body }) + } + + // Add an item to the wishlist + if (req.method === 'POST') { + const body = { ...req.body, customerToken } + return await handlers['addItem']({ ...ctx, body }) + } + + // Remove an item from the wishlist + if (req.method === 'DELETE') { + const body = { ...req.body, customerToken } + return await handlers['removeItem']({ ...ctx, body }) + } + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default wishlistEndpoint diff --git a/framework/commerce/api/index.ts b/framework/commerce/api/index.ts index 77b2eeb7e9..8b44bea332 100644 --- a/framework/commerce/api/index.ts +++ b/framework/commerce/api/index.ts @@ -1,7 +1,154 @@ +import type { NextApiHandler } from 'next' import type { RequestInit, Response } from '@vercel/fetch' +import type { APIEndpoint, APIHandler } from './utils/types' +import type { CartSchema } from '../types/cart' +import type { CustomerSchema } from '../types/customer' +import type { LoginSchema } from '../types/login' +import type { LogoutSchema } from '../types/logout' +import type { SignupSchema } from '../types/signup' +import type { ProductsSchema } from '../types/product' +import type { WishlistSchema } from '../types/wishlist' +import type { CheckoutSchema } from '../types/checkout' +import { + defaultOperations, + OPERATIONS, + AllOperations, + APIOperations, +} from './operations' + +export type APISchemas = + | CartSchema + | CustomerSchema + | LoginSchema + | LogoutSchema + | SignupSchema + | ProductsSchema + | WishlistSchema + | CheckoutSchema + +export type GetAPISchema< + C extends CommerceAPI, + S extends APISchemas = APISchemas +> = { + schema: S + endpoint: EndpointContext +} + +export type EndpointContext< + C extends CommerceAPI, + E extends EndpointSchemaBase +> = { + handler: Endpoint + handlers: EndpointHandlers +} + +export type EndpointSchemaBase = { + options: {} + handlers: { + [k: string]: { data?: any; body?: any } + } +} + +export type Endpoint< + C extends CommerceAPI, + E extends EndpointSchemaBase +> = APIEndpoint, any, E['options']> + +export type EndpointHandlers< + C extends CommerceAPI, + E extends EndpointSchemaBase +> = { + [H in keyof E['handlers']]: APIHandler< + C, + EndpointHandlers, + E['handlers'][H]['data'], + E['handlers'][H]['body'], + E['options'] + > +} + +export type APIProvider = { + config: CommerceAPIConfig + operations: APIOperations +} + +export type CommerceAPI< + P extends APIProvider = APIProvider +> = CommerceAPICore

& AllOperations

+ +export class CommerceAPICore

{ + constructor(readonly provider: P) {} + + getConfig(userConfig: Partial = {}): P['config'] { + return Object.entries(userConfig).reduce( + (cfg, [key, value]) => Object.assign(cfg, { [key]: value }), + { ...this.provider.config } + ) + } + + setConfig(newConfig: Partial) { + Object.assign(this.provider.config, newConfig) + } +} + +export function getCommerceApi

( + customProvider: P +): CommerceAPI

{ + const commerce = Object.assign( + new CommerceAPICore(customProvider), + defaultOperations as AllOperations

+ ) + const ops = customProvider.operations + + OPERATIONS.forEach((k) => { + const op = ops[k] + if (op) { + commerce[k] = op({ commerce }) as AllOperations

[typeof k] + } + }) + + return commerce +} + +export function getEndpoint< + P extends APIProvider, + T extends GetAPISchema +>( + commerce: CommerceAPI

, + context: T['endpoint'] & { + config?: P['config'] + options?: T['schema']['endpoint']['options'] + } +): NextApiHandler { + const cfg = commerce.getConfig(context.config) + + return function apiHandler(req, res) { + return context.handler({ + req, + res, + commerce, + config: cfg, + handlers: context.handlers, + options: context.options ?? {}, + }) + } +} + +export const createEndpoint = >( + endpoint: API['endpoint'] +) =>

( + commerce: CommerceAPI

, + context?: Partial & { + config?: P['config'] + options?: API['schema']['endpoint']['options'] + } +): NextApiHandler => { + return getEndpoint(commerce, { ...endpoint, ...context }) +} export interface CommerceAPIConfig { locale?: string + locales?: string[] commerceUrl: string apiToken: string cartCookie: string diff --git a/framework/commerce/api/operations.ts b/framework/commerce/api/operations.ts new file mode 100644 index 0000000000..2910a2d82b --- /dev/null +++ b/framework/commerce/api/operations.ts @@ -0,0 +1,177 @@ +import type { ServerResponse } from 'http' +import type { LoginOperation } from '../types/login' +import type { GetAllPagesOperation, GetPageOperation } from '../types/page' +import type { GetSiteInfoOperation } from '../types/site' +import type { GetCustomerWishlistOperation } from '../types/wishlist' +import type { + GetAllProductPathsOperation, + GetAllProductsOperation, + GetProductOperation, +} from '../types/product' +import type { APIProvider, CommerceAPI } from '.' + +const noop = () => { + throw new Error('Not implemented') +} + +export const OPERATIONS = [ + 'login', + 'getAllPages', + 'getPage', + 'getSiteInfo', + 'getCustomerWishlist', + 'getAllProductPaths', + 'getAllProducts', + 'getProduct', +] as const + +export const defaultOperations = OPERATIONS.reduce((ops, k) => { + ops[k] = noop + return ops +}, {} as { [K in AllowedOperations]: typeof noop }) + +export type AllowedOperations = typeof OPERATIONS[number] + +export type Operations

= { + login: { + (opts: { + variables: T['variables'] + config?: P['config'] + res: ServerResponse + }): Promise + + ( + opts: { + variables: T['variables'] + config?: P['config'] + res: ServerResponse + } & OperationOptions + ): Promise + } + + getAllPages: { + (opts?: { + config?: P['config'] + preview?: boolean + }): Promise + + ( + opts: { + config?: P['config'] + preview?: boolean + } & OperationOptions + ): Promise + } + + getPage: { + (opts: { + variables: T['variables'] + config?: P['config'] + preview?: boolean + }): Promise + + ( + opts: { + variables: T['variables'] + config?: P['config'] + preview?: boolean + } & OperationOptions + ): Promise + } + + getSiteInfo: { + (opts: { + config?: P['config'] + preview?: boolean + }): Promise + + ( + opts: { + config?: P['config'] + preview?: boolean + } & OperationOptions + ): Promise + } + + getCustomerWishlist: { + (opts: { + variables: T['variables'] + config?: P['config'] + includeProducts?: boolean + }): Promise + + ( + opts: { + variables: T['variables'] + config?: P['config'] + includeProducts?: boolean + } & OperationOptions + ): Promise + } + + getAllProductPaths: { + (opts: { + variables?: T['variables'] + config?: P['config'] + }): Promise + + ( + opts: { + variables?: T['variables'] + config?: P['config'] + } & OperationOptions + ): Promise + } + + getAllProducts: { + (opts: { + variables?: T['variables'] + config?: P['config'] + preview?: boolean + }): Promise + + ( + opts: { + variables?: T['variables'] + config?: P['config'] + preview?: boolean + } & OperationOptions + ): Promise + } + + getProduct: { + (opts: { + variables: T['variables'] + config?: P['config'] + preview?: boolean + }): Promise + + ( + opts: { + variables: T['variables'] + config?: P['config'] + preview?: boolean + } & OperationOptions + ): Promise + } +} + +export type APIOperations

= { + [K in keyof Operations

]?: (ctx: OperationContext

) => Operations

[K] +} + +export type AllOperations

= { + [K in keyof APIOperations

]-?: P['operations'][K] extends ( + ...args: any + ) => any + ? ReturnType + : typeof noop +} + +export type OperationContext

= { + commerce: CommerceAPI

+} + +export type OperationOptions = + | { query: string; url?: never } + | { query?: never; url: string } diff --git a/framework/commerce/api/utils/errors.ts b/framework/commerce/api/utils/errors.ts new file mode 100644 index 0000000000..6f9ecce0c3 --- /dev/null +++ b/framework/commerce/api/utils/errors.ts @@ -0,0 +1,22 @@ +import type { Response } from '@vercel/fetch' + +export class CommerceAPIError extends Error { + status: number + res: Response + data: any + + constructor(msg: string, res: Response, data?: any) { + super(msg) + this.name = 'CommerceApiError' + this.status = res.status + this.res = res + this.data = data + } +} + +export class CommerceNetworkError extends Error { + constructor(msg: string) { + super(msg) + this.name = 'CommerceNetworkError' + } +} diff --git a/framework/shopify/api/utils/is-allowed-method.ts b/framework/commerce/api/utils/is-allowed-method.ts similarity index 85% rename from framework/shopify/api/utils/is-allowed-method.ts rename to framework/commerce/api/utils/is-allowed-method.ts index 78bbba568d..51c37e2210 100644 --- a/framework/shopify/api/utils/is-allowed-method.ts +++ b/framework/commerce/api/utils/is-allowed-method.ts @@ -1,9 +1,11 @@ import type { NextApiRequest, NextApiResponse } from 'next' +export type HTTP_METHODS = 'OPTIONS' | 'GET' | 'POST' | 'PUT' | 'DELETE' + export default function isAllowedMethod( req: NextApiRequest, res: NextApiResponse, - allowedMethods: string[] + allowedMethods: HTTP_METHODS[] ) { const methods = allowedMethods.includes('OPTIONS') ? allowedMethods diff --git a/framework/commerce/api/utils/is-allowed-operation.ts b/framework/commerce/api/utils/is-allowed-operation.ts new file mode 100644 index 0000000000..f507781bfa --- /dev/null +++ b/framework/commerce/api/utils/is-allowed-operation.ts @@ -0,0 +1,19 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import isAllowedMethod, { HTTP_METHODS } from './is-allowed-method' +import { APIHandler } from './types' + +export default function isAllowedOperation( + req: NextApiRequest, + res: NextApiResponse, + allowedOperations: { [k in HTTP_METHODS]?: APIHandler } +) { + const methods = Object.keys(allowedOperations) as HTTP_METHODS[] + const allowedMethods = methods.reduce((arr, method) => { + if (allowedOperations[method]) { + arr.push(method) + } + return arr + }, []) + + return isAllowedMethod(req, res, allowedMethods) +} diff --git a/framework/commerce/api/utils/types.ts b/framework/commerce/api/utils/types.ts new file mode 100644 index 0000000000..27a95df405 --- /dev/null +++ b/framework/commerce/api/utils/types.ts @@ -0,0 +1,49 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import type { CommerceAPI } from '..' + +export type ErrorData = { message: string; code?: string } + +export type APIResponse = + | { data: Data; errors?: ErrorData[] } + // If `data` doesn't include `null`, then `null` is only allowed on errors + | (Data extends null + ? { data: null; errors?: ErrorData[] } + : { data: null; errors: ErrorData[] }) + +export type APIHandlerContext< + C extends CommerceAPI, + H extends APIHandlers = {}, + Data = any, + Options extends {} = {} +> = { + req: NextApiRequest + res: NextApiResponse> + commerce: C + config: C['provider']['config'] + handlers: H + /** + * Custom configs that may be used by a particular handler + */ + options: Options +} + +export type APIHandler< + C extends CommerceAPI, + H extends APIHandlers = {}, + Data = any, + Body = any, + Options extends {} = {} +> = ( + context: APIHandlerContext & { body: Body } +) => void | Promise + +export type APIHandlers = { + [k: string]: APIHandler +} + +export type APIEndpoint< + C extends CommerceAPI = CommerceAPI, + H extends APIHandlers = {}, + Data = any, + Options extends {} = {} +> = (context: APIHandlerContext) => void | Promise diff --git a/framework/commerce/auth/use-login.tsx b/framework/commerce/auth/use-login.tsx index cc4cf6a73b..67fb429dc5 100644 --- a/framework/commerce/auth/use-login.tsx +++ b/framework/commerce/auth/use-login.tsx @@ -1,13 +1,14 @@ import { useHook, useMutationHook } from '../utils/use-hook' import { mutationFetcher } from '../utils/default-fetcher' import type { MutationHook, HookFetcherFn } from '../utils/types' +import type { LoginHook } from '../types/login' import type { Provider } from '..' export type UseLogin< - H extends MutationHook = MutationHook + H extends MutationHook> = MutationHook > = ReturnType -export const fetcher: HookFetcherFn = mutationFetcher +export const fetcher: HookFetcherFn = mutationFetcher const fn = (provider: Provider) => provider.auth?.useLogin! diff --git a/framework/commerce/auth/use-logout.tsx b/framework/commerce/auth/use-logout.tsx index d0f7e3ae03..6ca16decf7 100644 --- a/framework/commerce/auth/use-logout.tsx +++ b/framework/commerce/auth/use-logout.tsx @@ -1,13 +1,14 @@ import { useHook, useMutationHook } from '../utils/use-hook' import { mutationFetcher } from '../utils/default-fetcher' import type { HookFetcherFn, MutationHook } from '../utils/types' +import type { LogoutHook } from '../types/logout' import type { Provider } from '..' export type UseLogout< - H extends MutationHook = MutationHook + H extends MutationHook> = MutationHook > = ReturnType -export const fetcher: HookFetcherFn = mutationFetcher +export const fetcher: HookFetcherFn = mutationFetcher const fn = (provider: Provider) => provider.auth?.useLogout! diff --git a/framework/commerce/auth/use-signup.tsx b/framework/commerce/auth/use-signup.tsx index 72e2422098..2f846fad6b 100644 --- a/framework/commerce/auth/use-signup.tsx +++ b/framework/commerce/auth/use-signup.tsx @@ -1,13 +1,14 @@ import { useHook, useMutationHook } from '../utils/use-hook' import { mutationFetcher } from '../utils/default-fetcher' import type { HookFetcherFn, MutationHook } from '../utils/types' +import type { SignupHook } from '../types/signup' import type { Provider } from '..' export type UseSignup< - H extends MutationHook = MutationHook + H extends MutationHook> = MutationHook > = ReturnType -export const fetcher: HookFetcherFn = mutationFetcher +export const fetcher: HookFetcherFn = mutationFetcher const fn = (provider: Provider) => provider.auth?.useSignup! diff --git a/framework/commerce/cart/use-add-item.tsx b/framework/commerce/cart/use-add-item.tsx index 324464656c..f4072c7633 100644 --- a/framework/commerce/cart/use-add-item.tsx +++ b/framework/commerce/cart/use-add-item.tsx @@ -1,17 +1,14 @@ import { useHook, useMutationHook } from '../utils/use-hook' import { mutationFetcher } from '../utils/default-fetcher' import type { HookFetcherFn, MutationHook } from '../utils/types' -import type { Cart, CartItemBody, AddCartItemBody } from '../types' +import type { AddItemHook } from '../types/cart' import type { Provider } from '..' export type UseAddItem< - H extends MutationHook = MutationHook + H extends MutationHook> = MutationHook > = ReturnType -export const fetcher: HookFetcherFn< - Cart, - AddCartItemBody -> = mutationFetcher +export const fetcher: HookFetcherFn = mutationFetcher const fn = (provider: Provider) => provider.cart?.useAddItem! diff --git a/framework/commerce/cart/use-cart.tsx b/framework/commerce/cart/use-cart.tsx index fbed715c89..cfce59e36c 100644 --- a/framework/commerce/cart/use-cart.tsx +++ b/framework/commerce/cart/use-cart.tsx @@ -1,28 +1,19 @@ import Cookies from 'js-cookie' import { useHook, useSWRHook } from '../utils/use-hook' -import type { HookFetcherFn, SWRHook } from '../utils/types' -import type { Cart } from '../types' +import type { SWRHook, HookFetcherFn } from '../utils/types' +import type { GetCartHook } from '../types/cart' import { Provider, useCommerce } from '..' -export type FetchCartInput = { - cartId?: Cart['id'] -} - export type UseCart< - H extends SWRHook = SWRHook< - Cart | null, - {}, - FetchCartInput, - { isEmpty?: boolean } - > + H extends SWRHook> = SWRHook > = ReturnType -export const fetcher: HookFetcherFn = async ({ +export const fetcher: HookFetcherFn = async ({ options, input: { cartId }, fetch, }) => { - return cartId ? await fetch({ ...options }) : null + return cartId ? await fetch(options) : null } const fn = (provider: Provider) => provider.cart?.useCart! diff --git a/framework/commerce/cart/use-remove-item.tsx b/framework/commerce/cart/use-remove-item.tsx index a9d1b37d25..f2bb43ffbc 100644 --- a/framework/commerce/cart/use-remove-item.tsx +++ b/framework/commerce/cart/use-remove-item.tsx @@ -1,29 +1,14 @@ import { useHook, useMutationHook } from '../utils/use-hook' import { mutationFetcher } from '../utils/default-fetcher' import type { HookFetcherFn, MutationHook } from '../utils/types' -import type { Cart, LineItem, RemoveCartItemBody } from '../types' +import type { RemoveItemHook } from '../types/cart' import type { Provider } from '..' -/** - * Input expected by the action returned by the `useRemoveItem` hook - */ -export type RemoveItemInput = { - id: string -} - export type UseRemoveItem< - H extends MutationHook = MutationHook< - Cart | null, - { item?: LineItem }, - RemoveItemInput, - RemoveCartItemBody - > + H extends MutationHook> = MutationHook > = ReturnType -export const fetcher: HookFetcherFn< - Cart | null, - RemoveCartItemBody -> = mutationFetcher +export const fetcher: HookFetcherFn = mutationFetcher const fn = (provider: Provider) => provider.cart?.useRemoveItem! diff --git a/framework/commerce/cart/use-update-item.tsx b/framework/commerce/cart/use-update-item.tsx index f8d0f1a407..2527732eb7 100644 --- a/framework/commerce/cart/use-update-item.tsx +++ b/framework/commerce/cart/use-update-item.tsx @@ -1,32 +1,14 @@ import { useHook, useMutationHook } from '../utils/use-hook' import { mutationFetcher } from '../utils/default-fetcher' import type { HookFetcherFn, MutationHook } from '../utils/types' -import type { Cart, CartItemBody, LineItem, UpdateCartItemBody } from '../types' +import type { UpdateItemHook } from '../types/cart' import type { Provider } from '..' -/** - * Input expected by the action returned by the `useUpdateItem` hook - */ -export type UpdateItemInput = T & { - id: string -} - export type UseUpdateItem< - H extends MutationHook = MutationHook< - Cart | null, - { - item?: LineItem - wait?: number - }, - UpdateItemInput, - UpdateCartItemBody - > + H extends MutationHook> = MutationHook > = ReturnType -export const fetcher: HookFetcherFn< - Cart | null, - UpdateCartItemBody -> = mutationFetcher +export const fetcher: HookFetcherFn = mutationFetcher const fn = (provider: Provider) => provider.cart?.useUpdateItem! diff --git a/framework/commerce/config.js b/framework/commerce/config.js index dc016d47a0..48f0d526b1 100644 --- a/framework/commerce/config.js +++ b/framework/commerce/config.js @@ -56,6 +56,19 @@ function withCommerceConfig(nextConfig = {}) { tsconfig.compilerOptions.paths['@framework'] = [`framework/${name}`] tsconfig.compilerOptions.paths['@framework/*'] = [`framework/${name}/*`] + // When running for production it may be useful to exclude the other providers + // from TS checking + if (process.env.VERCEL) { + const exclude = tsconfig.exclude.filter( + (item) => !item.startsWith('framework/') + ) + + tsconfig.exclude = PROVIDERS.reduce((exclude, current) => { + if (current !== name) exclude.push(`framework/${current}`) + return exclude + }, exclude) + } + fs.writeFileSync( tsconfigPath, prettier.format(JSON.stringify(tsconfig), { parser: 'json' }) diff --git a/framework/commerce/customer/use-customer.tsx b/framework/commerce/customer/use-customer.tsx index 5d6416a4b5..bbeeb3269c 100644 --- a/framework/commerce/customer/use-customer.tsx +++ b/framework/commerce/customer/use-customer.tsx @@ -1,14 +1,14 @@ import { useHook, useSWRHook } from '../utils/use-hook' import { SWRFetcher } from '../utils/default-fetcher' +import type { CustomerHook } from '../types/customer' import type { HookFetcherFn, SWRHook } from '../utils/types' -import type { Customer } from '../types' -import { Provider } from '..' +import type { Provider } from '..' export type UseCustomer< - H extends SWRHook = SWRHook + H extends SWRHook> = SWRHook > = ReturnType -export const fetcher: HookFetcherFn = SWRFetcher +export const fetcher: HookFetcherFn = SWRFetcher const fn = (provider: Provider) => provider.customer?.useCustomer! diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx index 07bf74a22a..7ecb44dc15 100644 --- a/framework/commerce/index.tsx +++ b/framework/commerce/index.tsx @@ -6,35 +6,44 @@ import { useMemo, useRef, } from 'react' -import { Fetcher, SWRHook, MutationHook } from './utils/types' -import type { FetchCartInput } from './cart/use-cart' -import type { Cart, Wishlist, Customer, SearchProductsData } from './types' + +import type { + Customer, + Wishlist, + Cart, + Product, + Signup, + Login, + Logout, +} from '@commerce/types' + +import type { Fetcher, SWRHook, MutationHook } from './utils/types' const Commerce = createContext | {}>({}) export type Provider = CommerceConfig & { fetcher: Fetcher cart?: { - useCart?: SWRHook - useAddItem?: MutationHook - useUpdateItem?: MutationHook - useRemoveItem?: MutationHook + useCart?: SWRHook + useAddItem?: MutationHook + useUpdateItem?: MutationHook + useRemoveItem?: MutationHook } wishlist?: { - useWishlist?: SWRHook - useAddItem?: MutationHook - useRemoveItem?: MutationHook + useWishlist?: SWRHook + useAddItem?: MutationHook + useRemoveItem?: MutationHook } customer?: { - useCustomer?: SWRHook + useCustomer?: SWRHook } products?: { - useSearch?: SWRHook + useSearch?: SWRHook } auth?: { - useSignup?: MutationHook - useLogin?: MutationHook - useLogout?: MutationHook + useSignup?: MutationHook + useLogin?: MutationHook + useLogout?: MutationHook } } diff --git a/framework/commerce/new-provider.md b/framework/commerce/new-provider.md index 4051c0f010..511704af6b 100644 --- a/framework/commerce/new-provider.md +++ b/framework/commerce/new-provider.md @@ -149,7 +149,7 @@ export const handler: SWRHook< { isEmpty?: boolean } > = { fetchOptions: { - url: '/api/bigcommerce/cart', + url: '/api/cart', method: 'GET', }, async fetcher({ input: { cartId }, options, fetch }) { @@ -197,7 +197,7 @@ export default useAddItem as UseAddItem export const handler: MutationHook = { fetchOptions: { - url: '/api/bigcommerce/cart', + url: '/api/cart', method: 'POST', }, async fetcher({ input: item, options, fetch }) { diff --git a/framework/commerce/product/use-search.tsx b/framework/commerce/product/use-search.tsx index d2b7820457..342b49e6e0 100644 --- a/framework/commerce/product/use-search.tsx +++ b/framework/commerce/product/use-search.tsx @@ -1,14 +1,14 @@ import { useHook, useSWRHook } from '../utils/use-hook' import { SWRFetcher } from '../utils/default-fetcher' import type { HookFetcherFn, SWRHook } from '../utils/types' -import type { SearchProductsData } from '../types' -import { Provider } from '..' +import type { SearchProductsHook } from '../types/product' +import type { Provider } from '..' export type UseSearch< - H extends SWRHook = SWRHook + H extends SWRHook> = SWRHook > = ReturnType -export const fetcher: HookFetcherFn = SWRFetcher +export const fetcher: HookFetcherFn = SWRFetcher const fn = (provider: Provider) => provider.products?.useSearch! diff --git a/framework/commerce/types.ts b/framework/commerce/types.ts deleted file mode 100644 index 62ce4de0a9..0000000000 --- a/framework/commerce/types.ts +++ /dev/null @@ -1,213 +0,0 @@ -import type { Wishlist as BCWishlist } from '../bigcommerce/api/wishlist' -import type { Customer as BCCustomer } from '../bigcommerce/api/customers' -import type { SearchProductsData as BCSearchProductsData } from '../bigcommerce/api/catalog/products' - -export type Discount = { - // The value of the discount, can be an amount or percentage - value: number -} - -export type LineItem = { - id: string - variantId: string - productId: string - name: string - quantity: number - discounts: Discount[] - // A human-friendly unique string automatically generated from the product’s name - path: string - variant: ProductVariant -} - -export type Measurement = { - value: number - unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES' -} - -export type Image = { - url: string - altText?: string - width?: number - height?: number -} - -export type ProductVariant = { - id: string - // The SKU (stock keeping unit) associated with the product variant. - sku: string - // The product variant’s title, or the product's name. - name: string - // Whether a customer needs to provide a shipping address when placing - // an order for the product variant. - requiresShipping: boolean - // The product variant’s price after all discounts are applied. - price: number - // Product variant’s price, as quoted by the manufacturer/distributor. - listPrice: number - // Image associated with the product variant. Falls back to the product image - // if no image is available. - image?: Image - // Indicates whether this product variant is in stock. - isInStock?: boolean - // Indicates if the product variant is available for sale. - availableForSale?: boolean - // The variant's weight. If a weight was not explicitly specified on the - // variant this will be the product's weight. - weight?: Measurement - // The variant's height. If a height was not explicitly specified on the - // variant, this will be the product's height. - height?: Measurement - // The variant's width. If a width was not explicitly specified on the - // variant, this will be the product's width. - width?: Measurement - // The variant's depth. If a depth was not explicitly specified on the - // variant, this will be the product's depth. - depth?: Measurement -} - -// Shopping cart, a.k.a Checkout -export type Cart = { - id: string - // ID of the customer to which the cart belongs. - customerId?: string - // The email assigned to this cart - email?: string - // The date and time when the cart was created. - createdAt: string - // The currency used for this cart - currency: { code: string } - // Specifies if taxes are included in the line items. - taxesIncluded: boolean - lineItems: LineItem[] - // The sum of all the prices of all the items in the cart. - // Duties, taxes, shipping and discounts excluded. - lineItemsSubtotalPrice: number - // Price of the cart before duties, shipping and taxes. - subtotalPrice: number - // The sum of all the prices of all the items in the cart. - // Duties, taxes and discounts included. - totalPrice: number - // Discounts that have been applied on the cart. - discounts?: Discount[] -} - -// TODO: Properly define this type -export interface Wishlist extends BCWishlist {} - -// TODO: Properly define this type -export interface Customer extends BCCustomer {} - -// TODO: Properly define this type -export interface SearchProductsData extends BCSearchProductsData {} - -/** - * Cart mutations - */ - -// Base cart item body used for cart mutations -export type CartItemBody = { - variantId: string - productId?: string - quantity?: number -} - -// Body used by the `getCart` operation handler -export type GetCartHandlerBody = { - cartId?: string -} - -// Body used by the add item to cart operation -export type AddCartItemBody = { - item: T -} - -// Body expected by the add item to cart operation handler -export type AddCartItemHandlerBody = Partial< - AddCartItemBody -> & { - cartId?: string -} - -// Body used by the update cart item operation -export type UpdateCartItemBody = { - itemId: string - item: T -} - -// Body expected by the update cart item operation handler -export type UpdateCartItemHandlerBody = Partial< - UpdateCartItemBody -> & { - cartId?: string -} - -// Body used by the remove cart item operation -export type RemoveCartItemBody = { - itemId: string -} - -// Body expected by the remove cart item operation handler -export type RemoveCartItemHandlerBody = Partial & { - cartId?: string -} - -export type Category = { - id: string - name: string - slug: string - path: string -} - -export type Page = any - -/** - * Temporal types - */ - -interface Entity { - id: string | number - [prop: string]: any -} - -export interface Product extends Entity { - name: string - description: string - descriptionHtml?: string - slug?: string - path?: string - images: ProductImage[] - variants: ProductVariant2[] - price: ProductPrice - options: ProductOption[] - sku?: string -} - -interface ProductOption extends Entity { - displayName: string - values: ProductOptionValues[] -} - -interface ProductOptionValues { - label: string - hexColors?: string[] -} - -interface ProductImage { - url: string - alt?: string -} - -interface ProductVariant2 { - id: string | number - options: ProductOption[] -} - -interface ProductPrice { - value: number - currencyCode: 'USD' | 'ARS' | string | undefined - retailPrice?: number - salePrice?: number - listPrice?: number - extendedSalePrice?: number - extendedListPrice?: number -} diff --git a/framework/commerce/types/cart.ts b/framework/commerce/types/cart.ts new file mode 100644 index 0000000000..7826f9b2dc --- /dev/null +++ b/framework/commerce/types/cart.ts @@ -0,0 +1,179 @@ +import type { Discount, Measurement, Image } from './common' + +export type SelectedOption = { + // The option's id. + id?: string + // The product option’s name. + name: string + /// The product option’s value. + value: string +} + +export type LineItem = { + id: string + variantId: string + productId: string + name: string + quantity: number + discounts: Discount[] + // A human-friendly unique string automatically generated from the product’s name + path: string + variant: ProductVariant + options?: SelectedOption[] +} + +export type ProductVariant = { + id: string + // The SKU (stock keeping unit) associated with the product variant. + sku: string + // The product variant’s title, or the product's name. + name: string + // Whether a customer needs to provide a shipping address when placing + // an order for the product variant. + requiresShipping: boolean + // The product variant’s price after all discounts are applied. + price: number + // Product variant’s price, as quoted by the manufacturer/distributor. + listPrice: number + // Image associated with the product variant. Falls back to the product image + // if no image is available. + image?: Image + // Indicates whether this product variant is in stock. + isInStock?: boolean + // Indicates if the product variant is available for sale. + availableForSale?: boolean + // The variant's weight. If a weight was not explicitly specified on the + // variant this will be the product's weight. + weight?: Measurement + // The variant's height. If a height was not explicitly specified on the + // variant, this will be the product's height. + height?: Measurement + // The variant's width. If a width was not explicitly specified on the + // variant, this will be the product's width. + width?: Measurement + // The variant's depth. If a depth was not explicitly specified on the + // variant, this will be the product's depth. + depth?: Measurement +} + +// Shopping cart, a.k.a Checkout +export type Cart = { + id: string + // ID of the customer to which the cart belongs. + customerId?: string + // The email assigned to this cart + email?: string + // The date and time when the cart was created. + createdAt: string + // The currency used for this cart + currency: { code: string } + // Specifies if taxes are included in the line items. + taxesIncluded: boolean + lineItems: LineItem[] + // The sum of all the prices of all the items in the cart. + // Duties, taxes, shipping and discounts excluded. + lineItemsSubtotalPrice: number + // Price of the cart before duties, shipping and taxes. + subtotalPrice: number + // The sum of all the prices of all the items in the cart. + // Duties, taxes and discounts included. + totalPrice: number + // Discounts that have been applied on the cart. + discounts?: Discount[] +} + +/** + * Base cart item body used for cart mutations + */ +export type CartItemBody = { + variantId: string + productId?: string + quantity?: number +} + +/** + * Hooks schema + */ + +export type CartTypes = { + cart?: Cart + item: LineItem + itemBody: CartItemBody +} + +export type CartHooks = { + getCart: GetCartHook + addItem: AddItemHook + updateItem: UpdateItemHook + removeItem: RemoveItemHook +} + +export type GetCartHook = { + data: T['cart'] | null + input: {} + fetcherInput: { cartId?: string } + swrState: { isEmpty: boolean } +} + +export type AddItemHook = { + data: T['cart'] + input?: T['itemBody'] + fetcherInput: T['itemBody'] + body: { item: T['itemBody'] } + actionInput: T['itemBody'] +} + +export type UpdateItemHook = { + data: T['cart'] | null + input: { item?: T['item']; wait?: number } + fetcherInput: { itemId: string; item: T['itemBody'] } + body: { itemId: string; item: T['itemBody'] } + actionInput: T['itemBody'] & { id: string } +} + +export type RemoveItemHook = { + data: T['cart'] | null + input: { item?: T['item'] } + fetcherInput: { itemId: string } + body: { itemId: string } + actionInput: { id: string } +} + +/** + * API Schema + */ + +export type CartSchema = { + endpoint: { + options: {} + handlers: CartHandlers + } +} + +export type CartHandlers = { + getCart: GetCartHandler + addItem: AddItemHandler + updateItem: UpdateItemHandler + removeItem: RemoveItemHandler +} + +export type GetCartHandler = GetCartHook & { + body: { cartId?: string } +} + +export type AddItemHandler = AddItemHook & { + body: { cartId: string } +} + +export type UpdateItemHandler< + T extends CartTypes = CartTypes +> = UpdateItemHook & { + data: T['cart'] + body: { cartId: string } +} + +export type RemoveItemHandler< + T extends CartTypes = CartTypes +> = RemoveItemHook & { + body: { cartId: string } +} diff --git a/framework/commerce/types/checkout.ts b/framework/commerce/types/checkout.ts new file mode 100644 index 0000000000..9e3c7ecfaa --- /dev/null +++ b/framework/commerce/types/checkout.ts @@ -0,0 +1,10 @@ +export type CheckoutSchema = { + endpoint: { + options: {} + handlers: { + checkout: { + data: null + } + } + } +} diff --git a/framework/commerce/types/common.ts b/framework/commerce/types/common.ts new file mode 100644 index 0000000000..06908c464a --- /dev/null +++ b/framework/commerce/types/common.ts @@ -0,0 +1,16 @@ +export type Discount = { + // The value of the discount, can be an amount or percentage + value: number +} + +export type Measurement = { + value: number + unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES' +} + +export type Image = { + url: string + altText?: string + width?: number + height?: number +} diff --git a/framework/commerce/types/customer.ts b/framework/commerce/types/customer.ts new file mode 100644 index 0000000000..ba90acdf46 --- /dev/null +++ b/framework/commerce/types/customer.ts @@ -0,0 +1,22 @@ +// TODO: define this type +export type Customer = any + +export type CustomerTypes = { + customer: Customer +} + +export type CustomerHook = { + data: T['customer'] | null + fetchData: { customer: T['customer'] } | null +} + +export type CustomerSchema = { + endpoint: { + options: {} + handlers: { + getLoggedInCustomer: { + data: { customer: T['customer'] } | null + } + } + } +} diff --git a/framework/commerce/types/index.ts b/framework/commerce/types/index.ts new file mode 100644 index 0000000000..7ab0b7f64f --- /dev/null +++ b/framework/commerce/types/index.ts @@ -0,0 +1,25 @@ +import * as Cart from './cart' +import * as Checkout from './checkout' +import * as Common from './common' +import * as Customer from './customer' +import * as Login from './login' +import * as Logout from './logout' +import * as Page from './page' +import * as Product from './product' +import * as Signup from './signup' +import * as Site from './site' +import * as Wishlist from './wishlist' + +export type { + Cart, + Checkout, + Common, + Customer, + Login, + Logout, + Page, + Product, + Signup, + Site, + Wishlist, +} diff --git a/framework/commerce/types/login.ts b/framework/commerce/types/login.ts new file mode 100644 index 0000000000..b6ef228e09 --- /dev/null +++ b/framework/commerce/types/login.ts @@ -0,0 +1,29 @@ +export type LoginBody = { + email: string + password: string +} + +export type LoginTypes = { + body: LoginBody +} + +export type LoginHook = { + data: null + actionInput: LoginBody + fetcherInput: LoginBody + body: T['body'] +} + +export type LoginSchema = { + endpoint: { + options: {} + handlers: { + login: LoginHook + } + } +} + +export type LoginOperation = { + data: { result?: string } + variables: unknown +} diff --git a/framework/commerce/types/logout.ts b/framework/commerce/types/logout.ts new file mode 100644 index 0000000000..a7240052f8 --- /dev/null +++ b/framework/commerce/types/logout.ts @@ -0,0 +1,17 @@ +export type LogoutTypes = { + body: { redirectTo?: string } +} + +export type LogoutHook = { + data: null + body: T['body'] +} + +export type LogoutSchema = { + endpoint: { + options: {} + handlers: { + logout: LogoutHook + } + } +} diff --git a/framework/commerce/types/page.ts b/framework/commerce/types/page.ts new file mode 100644 index 0000000000..89f82c1a6e --- /dev/null +++ b/framework/commerce/types/page.ts @@ -0,0 +1,28 @@ +// TODO: define this type +export type Page = { + // ID of the Web page. + id: string + // Page name, as displayed on the storefront. + name: string + // Relative URL on the storefront for this page. + url?: string + // HTML or variable that populates this page’s `` element, in default/desktop view. Required in POST if page type is `raw`. + body: string + // If true, this page appears in the storefront’s navigation menu. + is_visible?: boolean + // Order in which this page should display on the storefront. (Lower integers specify earlier display.) + sort_order?: number +} + +export type PageTypes = { + page: Page +} + +export type GetAllPagesOperation = { + data: { pages: T['page'][] } +} + +export type GetPageOperation = { + data: { page?: T['page'] } + variables: { id: string } +} diff --git a/framework/commerce/types/product.ts b/framework/commerce/types/product.ts new file mode 100644 index 0000000000..a12e332b4b --- /dev/null +++ b/framework/commerce/types/product.ts @@ -0,0 +1,99 @@ +export type ProductImage = { + url: string + alt?: string +} + +export type ProductPrice = { + value: number + currencyCode?: 'USD' | 'ARS' | string + retailPrice?: number + salePrice?: number + listPrice?: number + extendedSalePrice?: number + extendedListPrice?: number +} + +export type ProductOption = { + __typename?: 'MultipleChoiceOption' + id: string + displayName: string + values: ProductOptionValues[] +} + +export type ProductOptionValues = { + label: string + hexColors?: string[] +} + +export type ProductVariant = { + id: string | number + options: ProductOption[] + availableForSale?: boolean +} + +export type Product = { + id: string + name: string + description: string + descriptionHtml?: string + sku?: string + slug?: string + path?: string + images: ProductImage[] + variants: ProductVariant[] + price: ProductPrice + options: ProductOption[] +} + +export type SearchProductsBody = { + search?: string + categoryId?: string | number + brandId?: string | number + sort?: string + locale?: string +} + +export type ProductTypes = { + product: Product + searchBody: SearchProductsBody +} + +export type SearchProductsHook = { + data: { + products: T['product'][] + found: boolean + } + body: T['searchBody'] + input: T['searchBody'] + fetcherInput: T['searchBody'] +} + +export type ProductsSchema = { + endpoint: { + options: {} + handlers: { + getProducts: SearchProductsHook + } + } +} + +export type GetAllProductPathsOperation< + T extends ProductTypes = ProductTypes +> = { + data: { products: Pick[] } + variables: { first?: number } +} + +export type GetAllProductsOperation = { + data: { products: T['product'][] } + variables: { + relevance?: 'featured' | 'best_selling' | 'newest' + ids?: string[] + first?: number + } +} + +export type GetProductOperation = { + data: { product?: T['product'] } + variables: { path: string; slug?: never } | { path?: never; slug: string } +} diff --git a/framework/commerce/types/signup.ts b/framework/commerce/types/signup.ts new file mode 100644 index 0000000000..4e23da6c05 --- /dev/null +++ b/framework/commerce/types/signup.ts @@ -0,0 +1,26 @@ +export type SignupBody = { + firstName: string + lastName: string + email: string + password: string +} + +export type SignupTypes = { + body: SignupBody +} + +export type SignupHook = { + data: null + body: T['body'] + actionInput: T['body'] + fetcherInput: T['body'] +} + +export type SignupSchema = { + endpoint: { + options: {} + handlers: { + signup: SignupHook + } + } +} diff --git a/framework/commerce/types/site.ts b/framework/commerce/types/site.ts new file mode 100644 index 0000000000..73c7dddd2c --- /dev/null +++ b/framework/commerce/types/site.ts @@ -0,0 +1,20 @@ +export type Category = { + id: string + name: string + slug: string + path: string +} + +export type Brand = any + +export type SiteTypes = { + category: Category + brand: Brand +} + +export type GetSiteInfoOperation = { + data: { + categories: T['category'][] + brands: T['brand'][] + } +} diff --git a/framework/commerce/types/wishlist.ts b/framework/commerce/types/wishlist.ts new file mode 100644 index 0000000000..b3759849c5 --- /dev/null +++ b/framework/commerce/types/wishlist.ts @@ -0,0 +1,60 @@ +// TODO: define this type +export type Wishlist = any + +export type WishlistItemBody = { + variantId: string | number + productId: string +} + +export type WishlistTypes = { + wishlist: Wishlist + itemBody: WishlistItemBody +} + +export type GetWishlistHook = { + data: T['wishlist'] | null + body: { includeProducts?: boolean } + input: { includeProducts?: boolean } + fetcherInput: { customerId: string; includeProducts?: boolean } + swrState: { isEmpty: boolean } +} + +export type AddItemHook = { + data: T['wishlist'] + body: { item: T['itemBody'] } + fetcherInput: { item: T['itemBody'] } + actionInput: T['itemBody'] +} + +export type RemoveItemHook = { + data: T['wishlist'] | null + body: { itemId: string } + fetcherInput: { itemId: string } + actionInput: { id: string } + input: { wishlist?: { includeProducts?: boolean } } +} + +export type WishlistSchema = { + endpoint: { + options: {} + handlers: { + getWishlist: GetWishlistHook & { + data: T['wishlist'] | null + body: { customerToken?: string } + } + addItem: AddItemHook & { + body: { customerToken?: string } + } + removeItem: RemoveItemHook & { + body: { customerToken?: string } + } + } + } +} + +export type GetCustomerWishlistOperation< + T extends WishlistTypes = WishlistTypes +> = { + data: { wishlist?: T['wishlist'] } + variables: { customerId: string } +} diff --git a/framework/commerce/utils/default-fetcher.ts b/framework/commerce/utils/default-fetcher.ts index 493a9b5f97..53312fc966 100644 --- a/framework/commerce/utils/default-fetcher.ts +++ b/framework/commerce/utils/default-fetcher.ts @@ -1,9 +1,9 @@ import type { HookFetcherFn } from './types' -export const SWRFetcher: HookFetcherFn = ({ options, fetch }) => +export const SWRFetcher: HookFetcherFn = ({ options, fetch }) => fetch(options) -export const mutationFetcher: HookFetcherFn = ({ +export const mutationFetcher: HookFetcherFn = ({ input, options, fetch, diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts index 852afb2083..751cea4a5c 100644 --- a/framework/commerce/utils/types.ts +++ b/framework/commerce/utils/types.ts @@ -36,14 +36,19 @@ export type HookFetcher = ( fetch: (options: FetcherOptions) => Promise ) => Data | Promise -export type HookFetcherFn = ( - context: HookFetcherContext -) => Data | Promise +export type HookFetcherFn = ( + context: HookFetcherContext +) => H['data'] | Promise -export type HookFetcherContext = { +export type HookFetcherContext = { options: HookFetcherOptions - input: Input - fetch: (options: FetcherOptions) => Promise + input: H['fetcherInput'] + fetch: < + T = H['fetchData'] extends {} | null ? H['fetchData'] : any, + B = H['body'] + >( + options: FetcherOptions + ) => Promise } export type HookFetcherOptions = { method?: string } & ( @@ -58,7 +63,7 @@ export type HookSWRInput = [string, HookInputValue][] export type HookFetchInput = { [k: string]: HookInputValue } export type HookFunction< - Input extends { [k: string]: unknown } | null, + Input extends { [k: string]: unknown } | undefined, T > = keyof Input extends never ? () => T @@ -66,62 +71,72 @@ export type HookFunction< ? (input?: Input) => T : (input: Input) => T -export type SWRHook< - // Data obj returned by the hook and fetch operation - Data, +export type HookSchemaBase = { + // Data obj returned by the hook + data: any // Input expected by the hook - Input extends { [k: string]: unknown } = {}, - // Input expected before doing a fetch operation - FetchInput extends HookFetchInput = {}, + input?: {} + // Input expected before doing a fetch operation (aka fetch handler) + fetcherInput?: {} + // Body object expected by the fetch operation + body?: {} + // Data returned by the fetch operation + fetchData?: any +} + +export type SWRHookSchemaBase = HookSchemaBase & { // Custom state added to the response object of SWR - State = {} -> = { + swrState?: {} +} + +export type MutationSchemaBase = HookSchemaBase & { + // Input expected by the action returned by the hook + actionInput?: {} +} + +/** + * Generates a SWR hook handler based on the schema of a hook + */ +export type SWRHook = { useHook( - context: SWRHookContext + context: SWRHookContext ): HookFunction< - Input & { swrOptions?: SwrOptions }, - ResponseState & State + H['input'] & { swrOptions?: SwrOptions }, + ResponseState & H['swrState'] > fetchOptions: HookFetcherOptions - fetcher?: HookFetcherFn + fetcher?: HookFetcherFn } -export type SWRHookContext< - Data, - FetchInput extends { [k: string]: unknown } = {} -> = { +export type SWRHookContext = { useData(context?: { input?: HookFetchInput | HookSWRInput - swrOptions?: SwrOptions - }): ResponseState + swrOptions?: SwrOptions + }): ResponseState } -export type MutationHook< - // Data obj returned by the hook and fetch operation - Data, - // Input expected by the hook - Input extends { [k: string]: unknown } = {}, - // Input expected by the action returned by the hook - ActionInput extends { [k: string]: unknown } = {}, - // Input expected before doing a fetch operation - FetchInput extends { [k: string]: unknown } = ActionInput -> = { +/** + * Generates a mutation hook handler based on the schema of a hook + */ +export type MutationHook = { useHook( - context: MutationHookContext - ): HookFunction>> + context: MutationHookContext + ): HookFunction< + H['input'], + HookFunction> + > fetchOptions: HookFetcherOptions - fetcher?: HookFetcherFn + fetcher?: HookFetcherFn } -export type MutationHookContext< - Data, - FetchInput extends { [k: string]: unknown } | null = {} -> = { - fetch: keyof FetchInput extends never - ? () => Data | Promise - : Partial extends FetchInput - ? (context?: { input?: FetchInput }) => Data | Promise - : (context: { input: FetchInput }) => Data | Promise +export type MutationHookContext = { + fetch: keyof H['fetcherInput'] extends never + ? () => H['data'] | Promise + : Partial extends H['fetcherInput'] + ? (context?: { + input?: H['fetcherInput'] + }) => H['data'] | Promise + : (context: { input: H['fetcherInput'] }) => H['data'] | Promise } export type SwrOptions = ConfigInterface< diff --git a/framework/commerce/utils/use-data.tsx b/framework/commerce/utils/use-data.tsx index 9224b612c6..4fc208babf 100644 --- a/framework/commerce/utils/use-data.tsx +++ b/framework/commerce/utils/use-data.tsx @@ -2,10 +2,11 @@ import useSWR, { responseInterface } from 'swr' import type { HookSWRInput, HookFetchInput, - Fetcher, - SwrOptions, HookFetcherOptions, HookFetcherFn, + Fetcher, + SwrOptions, + SWRHookSchemaBase, } from './types' import defineProperty from './define-property' import { CommerceError } from './errors' @@ -14,15 +15,15 @@ export type ResponseState = responseInterface & { isLoading: boolean } -export type UseData = ( +export type UseData = ( options: { fetchOptions: HookFetcherOptions - fetcher: HookFetcherFn + fetcher: HookFetcherFn }, input: HookFetchInput | HookSWRInput, fetcherFn: Fetcher, - swrOptions?: SwrOptions -) => ResponseState + swrOptions?: SwrOptions +) => ResponseState const useData: UseData = (options, input, fetcherFn, swrOptions) => { const hookInput = Array.isArray(input) ? input : Object.entries(input) diff --git a/framework/commerce/utils/use-hook.ts b/framework/commerce/utils/use-hook.ts index da3431e3cb..1bf0779c4d 100644 --- a/framework/commerce/utils/use-hook.ts +++ b/framework/commerce/utils/use-hook.ts @@ -10,14 +10,14 @@ export function useFetcher() { export function useHook< P extends Provider, - H extends MutationHook | SWRHook + H extends MutationHook | SWRHook >(fn: (provider: P) => H) { const { providerRef } = useCommerce

() const provider = providerRef.current return fn(provider) } -export function useSWRHook>( +export function useSWRHook>( hook: PickRequired ) { const fetcher = useFetcher() @@ -30,7 +30,7 @@ export function useSWRHook>( }) } -export function useMutationHook>( +export function useMutationHook>( hook: PickRequired ) { const fetcher = useFetcher() diff --git a/framework/commerce/wishlist/use-add-item.tsx b/framework/commerce/wishlist/use-add-item.tsx index 11c8cc241f..f464be1ca6 100644 --- a/framework/commerce/wishlist/use-add-item.tsx +++ b/framework/commerce/wishlist/use-add-item.tsx @@ -1,10 +1,11 @@ import { useHook, useMutationHook } from '../utils/use-hook' import { mutationFetcher } from '../utils/default-fetcher' import type { MutationHook } from '../utils/types' +import type { AddItemHook } from '../types/wishlist' import type { Provider } from '..' export type UseAddItem< - H extends MutationHook = MutationHook + H extends MutationHook> = MutationHook > = ReturnType export const fetcher = mutationFetcher diff --git a/framework/commerce/wishlist/use-remove-item.tsx b/framework/commerce/wishlist/use-remove-item.tsx index c8c34a5afd..4419c17af1 100644 --- a/framework/commerce/wishlist/use-remove-item.tsx +++ b/framework/commerce/wishlist/use-remove-item.tsx @@ -1,28 +1,20 @@ import { useHook, useMutationHook } from '../utils/use-hook' import { mutationFetcher } from '../utils/default-fetcher' import type { HookFetcherFn, MutationHook } from '../utils/types' +import type { RemoveItemHook } from '../types/wishlist' import type { Provider } from '..' -export type RemoveItemInput = { - id: string | number -} - export type UseRemoveItem< - H extends MutationHook = MutationHook< - any | null, - { wishlist?: any }, - RemoveItemInput, - {} - > + H extends MutationHook> = MutationHook > = ReturnType -export const fetcher: HookFetcherFn = mutationFetcher +export const fetcher: HookFetcherFn = mutationFetcher const fn = (provider: Provider) => provider.wishlist?.useRemoveItem! -const useRemoveItem: UseRemoveItem = (input) => { +const useRemoveItem: UseRemoveItem = (...args) => { const hook = useHook(fn) - return useMutationHook({ fetcher, ...hook })(input) + return useMutationHook({ fetcher, ...hook })(...args) } export default useRemoveItem diff --git a/framework/commerce/wishlist/use-wishlist.tsx b/framework/commerce/wishlist/use-wishlist.tsx index 7a93b20b1d..672203f79f 100644 --- a/framework/commerce/wishlist/use-wishlist.tsx +++ b/framework/commerce/wishlist/use-wishlist.tsx @@ -1,25 +1,20 @@ import { useHook, useSWRHook } from '../utils/use-hook' import { SWRFetcher } from '../utils/default-fetcher' import type { HookFetcherFn, SWRHook } from '../utils/types' -import type { Wishlist } from '../types' +import type { GetWishlistHook } from '../types/wishlist' import type { Provider } from '..' export type UseWishlist< - H extends SWRHook = SWRHook< - Wishlist | null, - { includeProducts?: boolean }, - { customerId?: number; includeProducts: boolean }, - { isEmpty?: boolean } - > + H extends SWRHook> = SWRHook > = ReturnType -export const fetcher: HookFetcherFn = SWRFetcher +export const fetcher: HookFetcherFn = SWRFetcher const fn = (provider: Provider) => provider.wishlist?.useWishlist! -const useWishlist: UseWishlist = (input) => { +const useWishlist: UseWishlist = (...args) => { const hook = useHook(fn) - return useSWRHook({ fetcher, ...hook })(input) + return useSWRHook({ fetcher, ...hook })(...args) } export default useWishlist diff --git a/framework/shopify/README.md b/framework/shopify/README.md index d67111a41d..d5c4aa9423 100644 --- a/framework/shopify/README.md +++ b/framework/shopify/README.md @@ -121,3 +121,15 @@ const pages = await getAllPages({ config, }) ``` + +## Code generation + +This provider makes use of GraphQL code generation. The [schema.graphql](./schema.graphql) and [schema.d.ts](./schema.d.ts) files contain the generated types & schema introspection results. + +When developing the provider, changes to any GraphQL operations should be followed by re-generation of the types and schema files: + +From the project root dir, run: + +```sh +yarn generate:shopify +``` diff --git a/framework/shopify/api/cart/index.ts b/framework/shopify/api/cart/index.ts deleted file mode 100644 index ea9b101e1c..0000000000 --- a/framework/shopify/api/cart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export default function () {} diff --git a/framework/shopify/api/catalog/index.ts b/framework/shopify/api/catalog/index.ts deleted file mode 100644 index ea9b101e1c..0000000000 --- a/framework/shopify/api/catalog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export default function () {} diff --git a/framework/shopify/api/catalog/products.ts b/framework/shopify/api/catalog/products.ts deleted file mode 100644 index ea9b101e1c..0000000000 --- a/framework/shopify/api/catalog/products.ts +++ /dev/null @@ -1 +0,0 @@ -export default function () {} diff --git a/framework/shopify/api/customer.ts b/framework/shopify/api/customer.ts deleted file mode 100644 index ea9b101e1c..0000000000 --- a/framework/shopify/api/customer.ts +++ /dev/null @@ -1 +0,0 @@ -export default function () {} diff --git a/framework/shopify/api/customers/index.ts b/framework/shopify/api/customers/index.ts deleted file mode 100644 index ea9b101e1c..0000000000 --- a/framework/shopify/api/customers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export default function () {} diff --git a/framework/shopify/api/customers/login.ts b/framework/shopify/api/customers/login.ts deleted file mode 100644 index ea9b101e1c..0000000000 --- a/framework/shopify/api/customers/login.ts +++ /dev/null @@ -1 +0,0 @@ -export default function () {} diff --git a/framework/shopify/api/customers/logout.ts b/framework/shopify/api/customers/logout.ts deleted file mode 100644 index ea9b101e1c..0000000000 --- a/framework/shopify/api/customers/logout.ts +++ /dev/null @@ -1 +0,0 @@ -export default function () {} diff --git a/framework/shopify/api/customers/signup.ts b/framework/shopify/api/customers/signup.ts deleted file mode 100644 index ea9b101e1c..0000000000 --- a/framework/shopify/api/customers/signup.ts +++ /dev/null @@ -1 +0,0 @@ -export default function () {} diff --git a/framework/shopify/api/endpoints/cart.ts b/framework/shopify/api/endpoints/cart.ts new file mode 100644 index 0000000000..d09c976c3b --- /dev/null +++ b/framework/shopify/api/endpoints/cart.ts @@ -0,0 +1 @@ +export default function (_commerce: any) {} diff --git a/framework/shopify/api/endpoints/catalog/products.ts b/framework/shopify/api/endpoints/catalog/products.ts new file mode 100644 index 0000000000..d09c976c3b --- /dev/null +++ b/framework/shopify/api/endpoints/catalog/products.ts @@ -0,0 +1 @@ +export default function (_commerce: any) {} diff --git a/framework/shopify/api/checkout/index.ts b/framework/shopify/api/endpoints/checkout/checkout.ts similarity index 56% rename from framework/shopify/api/checkout/index.ts rename to framework/shopify/api/endpoints/checkout/checkout.ts index 2440784668..0c340a129c 100644 --- a/framework/shopify/api/checkout/index.ts +++ b/framework/shopify/api/endpoints/checkout/checkout.ts @@ -1,24 +1,16 @@ -import isAllowedMethod from '../utils/is-allowed-method' -import createApiHandler, { - ShopifyApiHandler, -} from '../utils/create-api-handler' - import { SHOPIFY_CHECKOUT_ID_COOKIE, SHOPIFY_CHECKOUT_URL_COOKIE, SHOPIFY_CUSTOMER_TOKEN_COOKIE, -} from '../../const' - -import { getConfig } from '..' -import associateCustomerWithCheckoutMutation from '../../utils/mutations/associate-customer-with-checkout' - -const METHODS = ['GET'] - -const checkoutApi: ShopifyApiHandler = async (req, res, config) => { - if (!isAllowedMethod(req, res, METHODS)) return - - config = getConfig() - +} from '../../../const' +import associateCustomerWithCheckoutMutation from '../../../utils/mutations/associate-customer-with-checkout' +import type { CheckoutEndpoint } from '.' + +const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({ + req, + res, + config, +}) => { const { cookies } = req const checkoutUrl = cookies[SHOPIFY_CHECKOUT_URL_COOKIE] const customerCookie = cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE] @@ -43,4 +35,4 @@ const checkoutApi: ShopifyApiHandler = async (req, res, config) => { } } -export default createApiHandler(checkoutApi, {}, {}) +export default checkout diff --git a/framework/shopify/api/endpoints/checkout/index.ts b/framework/shopify/api/endpoints/checkout/index.ts new file mode 100644 index 0000000000..5d78f451bb --- /dev/null +++ b/framework/shopify/api/endpoints/checkout/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import checkoutEndpoint from '@commerce/api/endpoints/checkout' +import type { CheckoutSchema } from '../../../types/checkout' +import type { ShopifyAPI } from '../..' +import checkout from './checkout' + +export type CheckoutAPI = GetAPISchema + +export type CheckoutEndpoint = CheckoutAPI['endpoint'] + +export const handlers: CheckoutEndpoint['handlers'] = { checkout } + +const checkoutApi = createEndpoint({ + handler: checkoutEndpoint, + handlers, +}) + +export default checkoutApi diff --git a/framework/shopify/api/endpoints/customer.ts b/framework/shopify/api/endpoints/customer.ts new file mode 100644 index 0000000000..d09c976c3b --- /dev/null +++ b/framework/shopify/api/endpoints/customer.ts @@ -0,0 +1 @@ +export default function (_commerce: any) {} diff --git a/framework/shopify/api/endpoints/login.ts b/framework/shopify/api/endpoints/login.ts new file mode 100644 index 0000000000..d09c976c3b --- /dev/null +++ b/framework/shopify/api/endpoints/login.ts @@ -0,0 +1 @@ +export default function (_commerce: any) {} diff --git a/framework/shopify/api/endpoints/logout.ts b/framework/shopify/api/endpoints/logout.ts new file mode 100644 index 0000000000..d09c976c3b --- /dev/null +++ b/framework/shopify/api/endpoints/logout.ts @@ -0,0 +1 @@ +export default function (_commerce: any) {} diff --git a/framework/shopify/api/endpoints/signup.ts b/framework/shopify/api/endpoints/signup.ts new file mode 100644 index 0000000000..d09c976c3b --- /dev/null +++ b/framework/shopify/api/endpoints/signup.ts @@ -0,0 +1 @@ +export default function (_commerce: any) {} diff --git a/framework/shopify/api/endpoints/wishlist.ts b/framework/shopify/api/endpoints/wishlist.ts new file mode 100644 index 0000000000..d09c976c3b --- /dev/null +++ b/framework/shopify/api/endpoints/wishlist.ts @@ -0,0 +1 @@ +export default function (_commerce: any) {} diff --git a/framework/shopify/api/index.ts b/framework/shopify/api/index.ts index 387ed02fca..28c7d34b33 100644 --- a/framework/shopify/api/index.ts +++ b/framework/shopify/api/index.ts @@ -1,12 +1,20 @@ -import type { CommerceAPIConfig } from '@commerce/api' +import { + CommerceAPI, + CommerceAPIConfig, + getCommerceApi as commerceApi, +} from '@commerce/api' import { API_URL, API_TOKEN, - SHOPIFY_CHECKOUT_ID_COOKIE, SHOPIFY_CUSTOMER_TOKEN_COOKIE, + SHOPIFY_CHECKOUT_ID_COOKIE, } from '../const' +import fetchGraphqlApi from './utils/fetch-graphql-api' + +import * as operations from './operations' + if (!API_URL) { throw new Error( `The environment variable NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN is missing and it's required to access your store` @@ -18,44 +26,30 @@ if (!API_TOKEN) { `The environment variable NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN is missing and it's required to access your store` ) } - -import fetchGraphqlApi from './utils/fetch-graphql-api' - export interface ShopifyConfig extends CommerceAPIConfig {} -export class Config { - private config: ShopifyConfig +const ONE_DAY = 60 * 60 * 24 - constructor(config: ShopifyConfig) { - this.config = config - } - - getConfig(userConfig: Partial = {}) { - return Object.entries(userConfig).reduce( - (cfg, [key, value]) => Object.assign(cfg, { [key]: value }), - { ...this.config } - ) - } - - setConfig(newConfig: Partial) { - Object.assign(this.config, newConfig) - } -} - -const config = new Config({ - locale: 'en-US', +const config: ShopifyConfig = { commerceUrl: API_URL, - apiToken: API_TOKEN!, + apiToken: API_TOKEN, + customerCookie: SHOPIFY_CUSTOMER_TOKEN_COOKIE, cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE, - cartCookieMaxAge: 60 * 60 * 24 * 30, + cartCookieMaxAge: ONE_DAY * 30, fetch: fetchGraphqlApi, - customerCookie: SHOPIFY_CUSTOMER_TOKEN_COOKIE, -}) +} -export function getConfig(userConfig?: Partial) { - return config.getConfig(userConfig) +export const provider = { + config, + operations, } -export function setConfig(newConfig: Partial) { - return config.setConfig(newConfig) +export type Provider = typeof provider + +export type ShopifyAPI

= CommerceAPI

+ +export function getCommerceApi

( + customProvider: P = provider as any +): ShopifyAPI

{ + return commerceApi(customProvider) } diff --git a/framework/shopify/api/operations/get-all-pages.ts b/framework/shopify/api/operations/get-all-pages.ts new file mode 100644 index 0000000000..ab0af9ff76 --- /dev/null +++ b/framework/shopify/api/operations/get-all-pages.ts @@ -0,0 +1,67 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import { + GetAllPagesQuery, + GetAllPagesQueryVariables, + PageEdge, +} from '../../schema' +import { normalizePages } from '../../utils' +import type { ShopifyConfig, Provider } from '..' +import type { GetAllPagesOperation, Page } from '../../types/page' +import getAllPagesQuery from '../../utils/queries/get-all-pages-query' + +export default function getAllPagesOperation({ + commerce, +}: OperationContext) { + async function getAllPages(opts?: { + config?: Partial + preview?: boolean + }): Promise + + async function getAllPages( + opts: { + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getAllPages({ + query = getAllPagesQuery, + config, + variables, + }: { + url?: string + config?: Partial + variables?: GetAllPagesQueryVariables + preview?: boolean + query?: string + } = {}): Promise { + const { fetch, locale, locales = ['en-US'] } = commerce.getConfig(config) + + const { data } = await fetch( + query, + { + variables, + }, + { + ...(locale && { + headers: { + 'Accept-Language': locale, + }, + }), + } + ) + + return { + pages: locales.reduce( + (arr, locale) => + arr.concat(normalizePages(data.pages.edges as PageEdge[], locale)), + [] + ), + } + } + + return getAllPages +} diff --git a/framework/shopify/api/operations/get-all-product-paths.ts b/framework/shopify/api/operations/get-all-product-paths.ts new file mode 100644 index 0000000000..c84f8c90ac --- /dev/null +++ b/framework/shopify/api/operations/get-all-product-paths.ts @@ -0,0 +1,55 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import { GetAllProductPathsOperation } from '../../types/product' +import { + GetAllProductPathsQuery, + GetAllProductPathsQueryVariables, + ProductEdge, +} from '../../schema' +import type { ShopifyConfig, Provider } from '..' +import { getAllProductsQuery } from '../../utils' + +export default function getAllProductPathsOperation({ + commerce, +}: OperationContext) { + async function getAllProductPaths< + T extends GetAllProductPathsOperation + >(opts?: { + variables?: T['variables'] + config?: ShopifyConfig + }): Promise + + async function getAllProductPaths( + opts: { + variables?: T['variables'] + config?: ShopifyConfig + } & OperationOptions + ): Promise + + async function getAllProductPaths({ + query = getAllProductsQuery, + config, + variables, + }: { + query?: string + config?: ShopifyConfig + variables?: T['variables'] + } = {}): Promise { + config = commerce.getConfig(config) + + const { data } = await config.fetch< + GetAllProductPathsQuery, + GetAllProductPathsQueryVariables + >(query, { variables }) + + return { + products: data.products.edges.map(({ node: { handle } }) => ({ + path: `/${handle}`, + })), + } + } + + return getAllProductPaths +} diff --git a/framework/shopify/api/operations/get-all-products.ts b/framework/shopify/api/operations/get-all-products.ts new file mode 100644 index 0000000000..08d781d5c6 --- /dev/null +++ b/framework/shopify/api/operations/get-all-products.ts @@ -0,0 +1,67 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import { GetAllProductsOperation } from '../../types/product' +import { + GetAllProductsQuery, + GetAllProductsQueryVariables, + Product as ShopifyProduct, +} from '../../schema' +import type { ShopifyConfig, Provider } from '..' +import getAllProductsQuery from '../../utils/queries/get-all-products-query' +import { normalizeProduct } from '../../utils' + +export default function getAllProductsOperation({ + commerce, +}: OperationContext) { + async function getAllProducts(opts?: { + variables?: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getAllProducts( + opts: { + variables?: T['variables'] + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getAllProducts({ + query = getAllProductsQuery, + variables, + config, + }: { + query?: string + variables?: T['variables'] + config?: Partial + preview?: boolean + } = {}): Promise { + const { fetch, locale } = commerce.getConfig(config) + + const { data } = await fetch< + GetAllProductsQuery, + GetAllProductsQueryVariables + >( + query, + { variables }, + { + ...(locale && { + headers: { + 'Accept-Language': locale, + }, + }), + } + ) + + return { + products: data.products.edges.map(({ node }) => + normalizeProduct(node as ShopifyProduct) + ), + } + } + + return getAllProducts +} diff --git a/framework/shopify/api/operations/get-page.ts b/framework/shopify/api/operations/get-page.ts new file mode 100644 index 0000000000..67e135ebef --- /dev/null +++ b/framework/shopify/api/operations/get-page.ts @@ -0,0 +1,64 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import { normalizePage } from '../../utils' +import type { ShopifyConfig, Provider } from '..' +import { + GetPageQuery, + GetPageQueryVariables, + Page as ShopifyPage, +} from '../../schema' +import { GetPageOperation } from '../../types/page' +import getPageQuery from '../../utils/queries/get-page-query' + +export default function getPageOperation({ + commerce, +}: OperationContext) { + async function getPage(opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getPage( + opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getPage({ + query = getPageQuery, + variables, + config, + }: { + query?: string + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise { + const { fetch, locale = 'en-US' } = commerce.getConfig(config) + + const { + data: { node: page }, + } = await fetch( + query, + { + variables, + }, + { + ...(locale && { + headers: { + 'Accept-Language': locale, + }, + }), + } + ) + + return page ? { page: normalizePage(page as ShopifyPage, locale) } : {} + } + + return getPage +} diff --git a/framework/shopify/api/operations/get-product.ts b/framework/shopify/api/operations/get-product.ts new file mode 100644 index 0000000000..447b5c7929 --- /dev/null +++ b/framework/shopify/api/operations/get-product.ts @@ -0,0 +1,63 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import { GetProductOperation } from '../../types/product' +import { normalizeProduct, getProductQuery } from '../../utils' +import type { ShopifyConfig, Provider } from '..' +import { GetProductBySlugQuery, Product as ShopifyProduct } from '../../schema' + +export default function getProductOperation({ + commerce, +}: OperationContext) { + async function getProduct(opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getProduct( + opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getProduct({ + query = getProductQuery, + variables, + config: cfg, + }: { + query?: string + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise { + const { fetch, locale } = commerce.getConfig(cfg) + + const { + data: { productByHandle }, + } = await fetch( + query, + { + variables, + }, + { + ...(locale && { + headers: { + 'Accept-Language': locale, + }, + }), + } + ) + + return { + ...(productByHandle && { + product: normalizeProduct(productByHandle as ShopifyProduct), + }), + } + } + + return getProduct +} diff --git a/framework/shopify/api/operations/get-site-info.ts b/framework/shopify/api/operations/get-site-info.ts new file mode 100644 index 0000000000..27b63b0f91 --- /dev/null +++ b/framework/shopify/api/operations/get-site-info.ts @@ -0,0 +1,62 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import { GetSiteInfoQueryVariables } from '../../schema' +import type { ShopifyConfig, Provider } from '..' +import { GetSiteInfoOperation } from '../../types/site' + +import { getCategories, getBrands, getSiteInfoQuery } from '../../utils' + +export default function getSiteInfoOperation({ + commerce, +}: OperationContext) { + async function getSiteInfo(opts?: { + config?: Partial + preview?: boolean + }): Promise + + async function getSiteInfo( + opts: { + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getSiteInfo({ + query = getSiteInfoQuery, + config, + variables, + }: { + query?: string + config?: Partial + preview?: boolean + variables?: GetSiteInfoQueryVariables + } = {}): Promise { + const cfg = commerce.getConfig(config) + + const categories = await getCategories(cfg) + const brands = await getBrands(cfg) + /* + const { fetch, locale } = cfg + const { data } = await fetch( + query, + { variables }, + { + ...(locale && { + headers: { + 'Accept-Language': locale, + }, + }), + } + ) + */ + + return { + categories, + brands, + } + } + + return getSiteInfo +} diff --git a/framework/shopify/api/operations/index.ts b/framework/shopify/api/operations/index.ts new file mode 100644 index 0000000000..7872a20b63 --- /dev/null +++ b/framework/shopify/api/operations/index.ts @@ -0,0 +1,7 @@ +export { default as getAllPages } from './get-all-pages' +export { default as getPage } from './get-page' +export { default as getAllProducts } from './get-all-products' +export { default as getAllProductPaths } from './get-all-product-paths' +export { default as getProduct } from './get-product' +export { default as getSiteInfo } from './get-site-info' +export { default as login } from './login' diff --git a/framework/shopify/api/operations/login.ts b/framework/shopify/api/operations/login.ts new file mode 100644 index 0000000000..41e837a3f6 --- /dev/null +++ b/framework/shopify/api/operations/login.ts @@ -0,0 +1,48 @@ +import type { ServerResponse } from 'http' +import type { OperationContext } from '@commerce/api/operations' +import type { LoginOperation } from '../../types/login' +import type { ShopifyConfig, Provider } from '..' +import { + customerAccessTokenCreateMutation, + setCustomerToken, + throwUserErrors, +} from '../../utils' +import { CustomerAccessTokenCreateMutation } from '../../schema' + +export default function loginOperation({ + commerce, +}: OperationContext) { + async function login({ + query = customerAccessTokenCreateMutation, + variables, + config, + }: { + query?: string + variables: T['variables'] + res: ServerResponse + config?: ShopifyConfig + }): Promise { + config = commerce.getConfig(config) + + const { + data: { customerAccessTokenCreate }, + } = await config.fetch(query, { + variables, + }) + + throwUserErrors(customerAccessTokenCreate?.customerUserErrors) + + const customerAccessToken = customerAccessTokenCreate?.customerAccessToken + const accessToken = customerAccessToken?.accessToken + + if (accessToken) { + setCustomerToken(accessToken) + } + + return { + result: customerAccessToken?.accessToken, + } + } + + return login +} diff --git a/framework/shopify/api/utils/create-api-handler.ts b/framework/shopify/api/utils/create-api-handler.ts deleted file mode 100644 index 8820aeabcf..0000000000 --- a/framework/shopify/api/utils/create-api-handler.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' -import { ShopifyConfig, getConfig } from '..' - -export type ShopifyApiHandler< - T = any, - H extends ShopifyHandlers = {}, - Options extends {} = {} -> = ( - req: NextApiRequest, - res: NextApiResponse>, - config: ShopifyConfig, - handlers: H, - // Custom configs that may be used by a particular handler - options: Options -) => void | Promise - -export type ShopifyHandler = (options: { - req: NextApiRequest - res: NextApiResponse> - config: ShopifyConfig - body: Body -}) => void | Promise - -export type ShopifyHandlers = { - [k: string]: ShopifyHandler -} - -export type ShopifyApiResponse = { - data: T | null - errors?: { message: string; code?: string }[] -} - -export default function createApiHandler< - T = any, - H extends ShopifyHandlers = {}, - Options extends {} = {} ->( - handler: ShopifyApiHandler, - handlers: H, - defaultOptions: Options -) { - return function getApiHandler({ - config, - operations, - options, - }: { - config?: ShopifyConfig - operations?: Partial - options?: Options extends {} ? Partial : never - } = {}): NextApiHandler { - const ops = { ...operations, ...handlers } - const opts = { ...defaultOptions, ...options } - - return function apiHandler(req, res) { - return handler(req, res, getConfig(config), ops, opts) - } - } -} diff --git a/framework/shopify/api/utils/fetch-all-products.ts b/framework/shopify/api/utils/fetch-all-products.ts deleted file mode 100644 index 9fa70a5eec..0000000000 --- a/framework/shopify/api/utils/fetch-all-products.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ProductEdge } from '../../schema' -import { ShopifyConfig } from '..' - -const fetchAllProducts = async ({ - config, - query, - variables, - acc = [], - cursor, -}: { - config: ShopifyConfig - query: string - acc?: ProductEdge[] - variables?: any - cursor?: string -}): Promise => { - const { data } = await config.fetch(query, { - variables: { ...variables, cursor }, - }) - - const edges: ProductEdge[] = data.products?.edges ?? [] - const hasNextPage = data.products?.pageInfo?.hasNextPage - acc = acc.concat(edges) - - if (hasNextPage) { - const cursor = edges.pop()?.cursor - if (cursor) { - return fetchAllProducts({ - config, - query, - variables, - acc, - cursor, - }) - } - } - - return acc -} - -export default fetchAllProducts diff --git a/framework/shopify/api/wishlist/index.tsx b/framework/shopify/api/wishlist/index.tsx deleted file mode 100644 index a728566734..0000000000 --- a/framework/shopify/api/wishlist/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export type WishlistItem = { product: any; id: number } -export default function () {} diff --git a/framework/shopify/auth/use-login.tsx b/framework/shopify/auth/use-login.tsx index 7993822cd5..d4369b7c2b 100644 --- a/framework/shopify/auth/use-login.tsx +++ b/framework/shopify/auth/use-login.tsx @@ -1,31 +1,22 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' -import { CommerceError, ValidationError } from '@commerce/utils/errors' +import { CommerceError } from '@commerce/utils/errors' +import useLogin, { UseLogin } from '@commerce/auth/use-login' +import type { LoginHook } from '../types/login' import useCustomer from '../customer/use-customer' -import createCustomerAccessTokenMutation from '../utils/mutations/customer-access-token-create' + import { - CustomerAccessTokenCreateInput, - CustomerUserError, - Mutation, - MutationCheckoutCreateArgs, -} from '../schema' -import useLogin, { UseLogin } from '@commerce/auth/use-login' -import { setCustomerToken, throwUserErrors } from '../utils' + setCustomerToken, + throwUserErrors, + customerAccessTokenCreateMutation, +} from '../utils' +import { Mutation, MutationCustomerAccessTokenCreateArgs } from '../schema' export default useLogin as UseLogin -const getErrorMessage = ({ code, message }: CustomerUserError) => { - switch (code) { - case 'UNIDENTIFIED_CUSTOMER': - message = 'Cannot find an account that matches the provided credentials' - break - } - return message -} - -export const handler: MutationHook = { +export const handler: MutationHook = { fetchOptions: { - query: createCustomerAccessTokenMutation, + query: customerAccessTokenCreateMutation, }, async fetcher({ input: { email, password }, options, fetch }) { if (!(email && password)) { @@ -37,7 +28,7 @@ export const handler: MutationHook = { const { customerAccessTokenCreate } = await fetch< Mutation, - MutationCheckoutCreateArgs + MutationCustomerAccessTokenCreateArgs >({ ...options, variables: { diff --git a/framework/shopify/auth/use-logout.tsx b/framework/shopify/auth/use-logout.tsx index 81a3b8cdd3..30074b8d5d 100644 --- a/framework/shopify/auth/use-logout.tsx +++ b/framework/shopify/auth/use-logout.tsx @@ -1,13 +1,14 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' import useLogout, { UseLogout } from '@commerce/auth/use-logout' +import type { LogoutHook } from '../types/logout' import useCustomer from '../customer/use-customer' import customerAccessTokenDeleteMutation from '../utils/mutations/customer-access-token-delete' import { getCustomerToken, setCustomerToken } from '../utils/customer-token' export default useLogout as UseLogout -export const handler: MutationHook = { +export const handler: MutationHook = { fetchOptions: { query: customerAccessTokenDeleteMutation, }, diff --git a/framework/shopify/auth/use-signup.tsx b/framework/shopify/auth/use-signup.tsx index 9ca5c682f4..29557e9609 100644 --- a/framework/shopify/auth/use-signup.tsx +++ b/framework/shopify/auth/use-signup.tsx @@ -1,25 +1,20 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' -import { CommerceError, ValidationError } from '@commerce/utils/errors' +import { CommerceError } from '@commerce/utils/errors' import useSignup, { UseSignup } from '@commerce/auth/use-signup' +import type { SignupHook } from '../types/signup' import useCustomer from '../customer/use-customer' -import { - CustomerCreateInput, - Mutation, - MutationCustomerCreateArgs, -} from '../schema' +import { Mutation, MutationCustomerCreateArgs } from '../schema' -import { customerCreateMutation } from '../utils/mutations' -import { handleAutomaticLogin, throwUserErrors } from '../utils' +import { + handleAutomaticLogin, + throwUserErrors, + customerCreateMutation, +} from '../utils' export default useSignup as UseSignup -export const handler: MutationHook< - null, - {}, - CustomerCreateInput, - CustomerCreateInput -> = { +export const handler: MutationHook = { fetchOptions: { query: customerCreateMutation, }, diff --git a/framework/shopify/cart/use-add-item.tsx b/framework/shopify/cart/use-add-item.tsx index cce0950e9e..5f0809d018 100644 --- a/framework/shopify/cart/use-add-item.tsx +++ b/framework/shopify/cart/use-add-item.tsx @@ -2,18 +2,19 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' import { CommerceError } from '@commerce/utils/errors' import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item' +import type { AddItemHook } from '../types/cart' import useCart from './use-cart' + import { checkoutLineItemAddMutation, getCheckoutId, checkoutToCart, } from '../utils' -import { Cart, CartItemBody } from '../types' import { Mutation, MutationCheckoutLineItemsAddArgs } from '../schema' export default useAddItem as UseAddItem -export const handler: MutationHook = { +export const handler: MutationHook = { fetchOptions: { query: checkoutLineItemAddMutation, }, diff --git a/framework/shopify/cart/use-cart.tsx b/framework/shopify/cart/use-cart.tsx index 5f77360bbc..d920d058a9 100644 --- a/framework/shopify/cart/use-cart.tsx +++ b/framework/shopify/cart/use-cart.tsx @@ -1,22 +1,20 @@ import { useMemo } from 'react' -import useCommerceCart, { - FetchCartInput, - UseCart, -} from '@commerce/cart/use-cart' +import useCommerceCart, { UseCart } from '@commerce/cart/use-cart' -import { Cart } from '../types' import { SWRHook } from '@commerce/utils/types' import { checkoutCreate, checkoutToCart } from '../utils' import getCheckoutQuery from '../utils/queries/get-checkout-query' +import { GetCartHook } from '../types/cart' + +import { + GetCheckoutQuery, + GetCheckoutQueryVariables, + CheckoutDetailsFragment, +} from '../schema' export default useCommerceCart as UseCart -export const handler: SWRHook< - Cart | null, - {}, - FetchCartInput, - { isEmpty?: boolean } -> = { +export const handler: SWRHook = { fetchOptions: { query: getCheckoutQuery, }, diff --git a/framework/shopify/cart/use-remove-item.tsx b/framework/shopify/cart/use-remove-item.tsx index 8db38eac2e..bf9fb2d95e 100644 --- a/framework/shopify/cart/use-remove-item.tsx +++ b/framework/shopify/cart/use-remove-item.tsx @@ -3,30 +3,28 @@ import type { MutationHookContext, HookFetcherContext, } from '@commerce/utils/types' -import { RemoveCartItemBody } from '@commerce/types' import { ValidationError } from '@commerce/utils/errors' -import useRemoveItem, { - RemoveItemInput as RemoveItemInputBase, - UseRemoveItem, -} from '@commerce/cart/use-remove-item' +import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item' +import type { Cart, LineItem, RemoveItemHook } from '../types/cart' import useCart from './use-cart' + +export type RemoveItemFn = T extends LineItem + ? (input?: RemoveItemActionInput) => Promise + : (input: RemoveItemActionInput) => Promise + +export type RemoveItemActionInput = T extends LineItem + ? Partial + : RemoveItemHook['actionInput'] + +export default useRemoveItem as UseRemoveItem + import { checkoutLineItemRemoveMutation, getCheckoutId, checkoutToCart, } from '../utils' -import { Cart, LineItem } from '../types' -import { Mutation, MutationCheckoutLineItemsRemoveArgs } from '../schema' -export type RemoveItemFn = T extends LineItem - ? (input?: RemoveItemInput) => Promise - : (input: RemoveItemInput) => Promise - -export type RemoveItemInput = T extends LineItem - ? Partial - : RemoveItemInputBase - -export default useRemoveItem as UseRemoveItem +import { Mutation, MutationCheckoutLineItemsRemoveArgs } from '../schema' export const handler = { fetchOptions: { @@ -36,16 +34,14 @@ export const handler = { input: { itemId }, options, fetch, - }: HookFetcherContext) { + }: HookFetcherContext) { const data = await fetch({ ...options, variables: { checkoutId: getCheckoutId(), lineItemIds: [itemId] }, }) return checkoutToCart(data.checkoutLineItemsRemove) }, - useHook: ({ - fetch, - }: MutationHookContext) => < + useHook: ({ fetch }: MutationHookContext) => < T extends LineItem | undefined = undefined >( ctx: { item?: T } = {} diff --git a/framework/shopify/cart/use-update-item.tsx b/framework/shopify/cart/use-update-item.tsx index 49dd6be145..3f1cf43155 100644 --- a/framework/shopify/cart/use-update-item.tsx +++ b/framework/shopify/cart/use-update-item.tsx @@ -5,21 +5,21 @@ import type { MutationHookContext, } from '@commerce/utils/types' import { ValidationError } from '@commerce/utils/errors' -import useUpdateItem, { - UpdateItemInput as UpdateItemInputBase, - UseUpdateItem, -} from '@commerce/cart/use-update-item' +import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item' import useCart from './use-cart' import { handler as removeItemHandler } from './use-remove-item' -import type { Cart, LineItem, UpdateCartItemBody } from '../types' -import { checkoutToCart } from '../utils' -import { getCheckoutId, checkoutLineItemUpdateMutation } from '../utils' +import type { UpdateItemHook, LineItem } from '../types/cart' +import { + getCheckoutId, + checkoutLineItemUpdateMutation, + checkoutToCart, +} from '../utils' import { Mutation, MutationCheckoutLineItemsUpdateArgs } from '../schema' -export type UpdateItemInput = T extends LineItem - ? Partial> - : UpdateItemInputBase +export type UpdateItemActionInput = T extends LineItem + ? Partial + : UpdateItemHook['actionInput'] export default useUpdateItem as UseUpdateItem @@ -31,7 +31,7 @@ export const handler = { input: { itemId, item }, options, fetch, - }: HookFetcherContext) { + }: HookFetcherContext) { if (Number.isInteger(item.quantity)) { // Also allow the update hook to remove an item if the quantity is lower than 1 if (item.quantity! < 1) { @@ -64,9 +64,7 @@ export const handler = { return checkoutToCart(checkoutLineItemsUpdate) }, - useHook: ({ - fetch, - }: MutationHookContext) => < + useHook: ({ fetch }: MutationHookContext) => < T extends LineItem | undefined = undefined >( ctx: { @@ -78,7 +76,7 @@ export const handler = { const { mutate } = useCart() as any return useCallback( - debounce(async (input: UpdateItemInput) => { + debounce(async (input: UpdateItemActionInput) => { const itemId = input.id ?? item?.id const productId = input.productId ?? item?.productId const variantId = input.productId ?? item?.variantId diff --git a/framework/shopify/codegen.json b/framework/shopify/codegen.json new file mode 100644 index 0000000000..f0a757142b --- /dev/null +++ b/framework/shopify/codegen.json @@ -0,0 +1,32 @@ +{ + "schema": { + "https://${NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/api/2021-01/graphql.json": { + "headers": { + "X-Shopify-Storefront-Access-Token": "${NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN}" + } + } + }, + "documents": [ + { + "./framework/shopify/**/*.{ts,tsx}": { + "noRequire": true + } + } + ], + "generates": { + "./framework/shopify/schema.d.ts": { + "plugins": ["typescript", "typescript-operations"], + "config": { + "scalars": { + "ID": "string" + } + } + }, + "./framework/shopify/schema.graphql": { + "plugins": ["schema-ast"] + } + }, + "hooks": { + "afterAllFileWrite": ["prettier --write"] + } +} diff --git a/framework/shopify/common/get-all-pages.ts b/framework/shopify/common/get-all-pages.ts deleted file mode 100644 index 54231ed03b..0000000000 --- a/framework/shopify/common/get-all-pages.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getConfig, ShopifyConfig } from '../api' -import { PageEdge } from '../schema' -import { getAllPagesQuery } from '../utils/queries' - -type Variables = { - first?: number -} - -type ReturnType = { - pages: Page[] -} - -export type Page = { - id: string - name: string - url: string - sort_order?: number - body: string -} - -const getAllPages = async (options?: { - variables?: Variables - config: ShopifyConfig - preview?: boolean -}): Promise => { - let { config, variables = { first: 250 } } = options ?? {} - config = getConfig(config) - const { locale } = config - const { data } = await config.fetch(getAllPagesQuery, { variables }) - - const pages = data.pages?.edges?.map( - ({ node: { title: name, handle, ...node } }: PageEdge) => ({ - ...node, - url: `/${locale}/${handle}`, - name, - }) - ) - - return { pages } -} - -export default getAllPages diff --git a/framework/shopify/common/get-page.ts b/framework/shopify/common/get-page.ts deleted file mode 100644 index be934aa425..0000000000 --- a/framework/shopify/common/get-page.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getConfig, ShopifyConfig } from '../api' -import getPageQuery from '../utils/queries/get-page-query' -import { Page } from './get-all-pages' - -type Variables = { - id: string -} - -export type GetPageResult = T - -const getPage = async (options: { - variables: Variables - config: ShopifyConfig - preview?: boolean -}): Promise => { - let { config, variables } = options ?? {} - - config = getConfig(config) - const { locale } = config - - const { data } = await config.fetch(getPageQuery, { - variables, - }) - const page = data.node - - return { - page: page - ? { - ...page, - name: page.title, - url: `/${locale}/${page.handle}`, - } - : null, - } -} - -export default getPage diff --git a/framework/shopify/common/get-site-info.ts b/framework/shopify/common/get-site-info.ts deleted file mode 100644 index f9e67b5de0..0000000000 --- a/framework/shopify/common/get-site-info.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Category } from '@commerce/types' -import { getConfig, ShopifyConfig } from '../api' -import getCategories from '../utils/get-categories' -import getVendors, { Brands } from '../utils/get-vendors' - -export type GetSiteInfoResult< - T extends { categories: any[]; brands: any[] } = { - categories: Category[] - brands: Brands - } -> = T - -const getSiteInfo = async (options?: { - variables?: any - config: ShopifyConfig - preview?: boolean -}): Promise => { - let { config } = options ?? {} - - config = getConfig(config) - - const categories = await getCategories(config) - const brands = await getVendors(config) - - return { - categories, - brands, - } -} - -export default getSiteInfo diff --git a/framework/shopify/customer/get-customer-id.ts b/framework/shopify/customer/get-customer-id.ts deleted file mode 100644 index ca096645a5..0000000000 --- a/framework/shopify/customer/get-customer-id.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getConfig, ShopifyConfig } from '../api' -import getCustomerIdQuery from '../utils/queries/get-customer-id-query' -import Cookies from 'js-cookie' - -async function getCustomerId({ - customerToken: customerAccesToken, - config, -}: { - customerToken: string - config?: ShopifyConfig -}): Promise { - config = getConfig(config) - - const { data } = await config.fetch(getCustomerIdQuery, { - variables: { - customerAccesToken: - customerAccesToken || Cookies.get(config.customerCookie), - }, - }) - - return data.customer?.id -} - -export default getCustomerId diff --git a/framework/shopify/customer/use-customer.tsx b/framework/shopify/customer/use-customer.tsx index 7b600838e6..be097fe80b 100644 --- a/framework/shopify/customer/use-customer.tsx +++ b/framework/shopify/customer/use-customer.tsx @@ -1,18 +1,19 @@ import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' -import { Customer } from '@commerce/types' +import type { CustomerHook } from '../types/customer' import { SWRHook } from '@commerce/utils/types' import { getCustomerQuery, getCustomerToken } from '../utils' +import { GetCustomerQuery, GetCustomerQueryVariables } from '../schema' export default useCustomer as UseCustomer -export const handler: SWRHook = { +export const handler: SWRHook = { fetchOptions: { query: getCustomerQuery, }, async fetcher({ options, fetch }) { const customerAccessToken = getCustomerToken() if (customerAccessToken) { - const data = await fetch({ + const data = await fetch({ ...options, variables: { customerAccessToken: getCustomerToken() }, }) diff --git a/framework/shopify/fetcher.ts b/framework/shopify/fetcher.ts index a691505039..9a8d2d8d5a 100644 --- a/framework/shopify/fetcher.ts +++ b/framework/shopify/fetcher.ts @@ -8,13 +8,17 @@ const fetcher: Fetcher = async ({ variables, query, }) => { + const { locale, ...vars } = variables ?? {} return handleFetchResponse( await fetch(url, { method, - body: JSON.stringify({ query, variables }), + body: JSON.stringify({ query, variables: vars }), headers: { 'X-Shopify-Storefront-Access-Token': API_TOKEN!, 'Content-Type': 'application/json', + ...(locale && { + 'Accept-Language': locale, + }), }, }) ) diff --git a/framework/shopify/index.tsx b/framework/shopify/index.tsx index 5b25d6b217..13ff9d1f86 100644 --- a/framework/shopify/index.tsx +++ b/framework/shopify/index.tsx @@ -36,4 +36,4 @@ export function CommerceProvider({ children, ...config }: ShopifyProps) { ) } -export const useCommerce = () => useCoreCommerce() +export const useCommerce = () => useCoreCommerce() diff --git a/framework/shopify/product/get-all-collections.ts b/framework/shopify/product/get-all-collections.ts deleted file mode 100644 index 15c4bc51ac..0000000000 --- a/framework/shopify/product/get-all-collections.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CollectionEdge } from '../schema' -import { getConfig, ShopifyConfig } from '../api' -import getAllCollectionsQuery from '../utils/queries/get-all-collections-query' - -const getAllCollections = async (options?: { - variables?: any - config: ShopifyConfig - preview?: boolean -}) => { - let { config, variables = { first: 250 } } = options ?? {} - config = getConfig(config) - - const { data } = await config.fetch(getAllCollectionsQuery, { variables }) - const edges = data.collections?.edges ?? [] - - const categories = edges.map( - ({ node: { id: entityId, title: name, handle } }: CollectionEdge) => ({ - entityId, - name, - path: `/${handle}`, - }) - ) - - return { - categories, - } -} - -export default getAllCollections diff --git a/framework/shopify/product/get-all-product-paths.ts b/framework/shopify/product/get-all-product-paths.ts deleted file mode 100644 index e8ee040659..0000000000 --- a/framework/shopify/product/get-all-product-paths.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getConfig, ShopifyConfig } from '../api' -import { ProductEdge } from '../schema' -import getAllProductsPathsQuery from '../utils/queries/get-all-products-paths-query' - -type ProductPath = { - path: string -} - -export type ProductPathNode = { - node: ProductPath -} - -type ReturnType = { - products: ProductPathNode[] -} - -const getAllProductPaths = async (options?: { - variables?: any - config?: ShopifyConfig - preview?: boolean -}): Promise => { - let { config, variables = { first: 100, sortKey: 'BEST_SELLING' } } = - options ?? {} - config = getConfig(config) - - const { data } = await config.fetch(getAllProductsPathsQuery, { - variables, - }) - - return { - products: data.products?.edges?.map( - ({ node: { handle } }: ProductEdge) => ({ - node: { - path: `/${handle}`, - }, - }) - ), - } -} - -export default getAllProductPaths diff --git a/framework/shopify/product/get-all-products.ts b/framework/shopify/product/get-all-products.ts deleted file mode 100644 index 3915abebff..0000000000 --- a/framework/shopify/product/get-all-products.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { GraphQLFetcherResult } from '@commerce/api' -import { getConfig, ShopifyConfig } from '../api' -import { ProductEdge } from '../schema' -import { getAllProductsQuery } from '../utils/queries' -import { normalizeProduct } from '../utils/normalize' -import { Product } from '@commerce/types' - -type Variables = { - first?: number - field?: string -} - -type ReturnType = { - products: Product[] -} - -const getAllProducts = async (options: { - variables?: Variables - config?: ShopifyConfig - preview?: boolean -}): Promise => { - let { config, variables = { first: 250 } } = options ?? {} - config = getConfig(config) - - const { data }: GraphQLFetcherResult = await config.fetch( - getAllProductsQuery, - { variables } - ) - - const products = data.products?.edges?.map(({ node: p }: ProductEdge) => - normalizeProduct(p) - ) - - return { - products, - } -} - -export default getAllProducts diff --git a/framework/shopify/product/get-product.ts b/framework/shopify/product/get-product.ts deleted file mode 100644 index 1d861e1a1c..0000000000 --- a/framework/shopify/product/get-product.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { GraphQLFetcherResult } from '@commerce/api' -import { getConfig, ShopifyConfig } from '../api' -import { normalizeProduct, getProductQuery } from '../utils' - -type Variables = { - slug: string -} - -type ReturnType = { - product: any -} - -const getProduct = async (options: { - variables: Variables - config: ShopifyConfig - preview?: boolean -}): Promise => { - let { config, variables } = options ?? {} - config = getConfig(config) - - const { data }: GraphQLFetcherResult = await config.fetch(getProductQuery, { - variables, - }) - const { productByHandle } = data - - return { - product: productByHandle ? normalizeProduct(productByHandle) : null, - } -} - -export default getProduct diff --git a/framework/shopify/product/use-search.tsx b/framework/shopify/product/use-search.tsx index bf812af3de..9588b65a22 100644 --- a/framework/shopify/product/use-search.tsx +++ b/framework/shopify/product/use-search.tsx @@ -1,7 +1,14 @@ import { SWRHook } from '@commerce/utils/types' import useSearch, { UseSearch } from '@commerce/product/use-search' -import { ProductEdge } from '../schema' +import { + CollectionEdge, + GetAllProductsQuery, + GetProductsFromCollectionQueryVariables, + Product as ShopifyProduct, + ProductEdge, +} from '../schema' + import { getAllProductsQuery, getCollectionProductsQuery, @@ -9,56 +16,59 @@ import { normalizeProduct, } from '../utils' -import { Product } from '@commerce/types' - -export default useSearch as UseSearch +import type { SearchProductsHook } from '../types/product' export type SearchProductsInput = { search?: string - categoryId?: string - brandId?: string + categoryId?: number + brandId?: number sort?: string + locale?: string } -export type SearchProductsData = { - products: Product[] - found: boolean -} +export default useSearch as UseSearch -export const handler: SWRHook< - SearchProductsData, - SearchProductsInput, - SearchProductsInput -> = { +export const handler: SWRHook = { fetchOptions: { query: getAllProductsQuery, }, async fetcher({ input, options, fetch }) { const { categoryId, brandId } = input + const method = options?.method + const variables = getSearchVariables(input) + let products - const data = await fetch({ - query: categoryId ? getCollectionProductsQuery : options.query, - method: options?.method, - variables: getSearchVariables(input), - }) - - let edges - + // change the query to getCollectionProductsQuery when categoryId is set if (categoryId) { - edges = data.node?.products?.edges ?? [] - if (brandId) { - edges = edges.filter( - ({ node: { vendor } }: ProductEdge) => - vendor.replace(/\s+/g, '-').toLowerCase() === brandId - ) - } + const data = await fetch< + CollectionEdge, + GetProductsFromCollectionQueryVariables + >({ + query: getCollectionProductsQuery, + method, + variables, + }) + // filter on client when brandId & categoryId are set since is not available on collection product query + products = brandId + ? data.node.products.edges.filter( + ({ node: { vendor } }: ProductEdge) => + vendor.replace(/\s+/g, '-').toLowerCase() === brandId + ) + : data.node.products.edges } else { - edges = data.products?.edges ?? [] + const data = await fetch({ + query: options.query, + method, + variables, + }) + products = data.products.edges } return { - products: edges.map(({ node }: ProductEdge) => normalizeProduct(node)), - found: !!edges.length, + products: products?.map(({ node }) => + normalizeProduct(node as ShopifyProduct) + ), + found: !!products?.length, } }, useHook: ({ useData }) => (input = {}) => { @@ -68,6 +78,7 @@ export const handler: SWRHook< ['categoryId', input.categoryId], ['brandId', input.brandId], ['sort', input.sort], + ['locale', input.locale], ], swrOptions: { revalidateOnFocus: false, diff --git a/framework/shopify/schema.d.ts b/framework/shopify/schema.d.ts index b1b23a3e5e..328f0ff1b2 100644 --- a/framework/shopify/schema.d.ts +++ b/framework/shopify/schema.d.ts @@ -37,7 +37,7 @@ export type ApiVersion = { displayName: Scalars['String'] /** The unique identifier of an ApiVersion. All supported API versions have a date-based (YYYY-MM) or `unstable` handle. */ handle: Scalars['String'] - /** Whether the version is supported by Shopify. */ + /** Whether the version is actively supported by Shopify. Supported API versions are guaranteed to be stable. Unsupported API versions include unstable, release candidate, and end-of-life versions that are marked as unsupported. For more information, refer to [Versioning](https://shopify.dev/concepts/about-apis/versioning). */ supported: Scalars['Boolean'] } @@ -306,17 +306,17 @@ export enum BlogSortKeys { /** Card brand, such as Visa or Mastercard, which can be used for payments. */ export enum CardBrand { - /** Visa */ + /** Visa. */ Visa = 'VISA', - /** Mastercard */ + /** Mastercard. */ Mastercard = 'MASTERCARD', - /** Discover */ + /** Discover. */ Discover = 'DISCOVER', - /** American Express */ + /** American Express. */ AmericanExpress = 'AMERICAN_EXPRESS', - /** Diners Club */ + /** Diners Club. */ DinersClub = 'DINERS_CLUB', - /** JCB */ + /** JCB. */ Jcb = 'JCB', } @@ -1195,6 +1195,8 @@ export enum CountryCode { Am = 'AM', /** Aruba. */ Aw = 'AW', + /** Ascension Island. */ + Ac = 'AC', /** Australia. */ Au = 'AU', /** Austria. */ @@ -1613,6 +1615,8 @@ export enum CountryCode { To = 'TO', /** Trinidad & Tobago. */ Tt = 'TT', + /** Tristan da Cunha. */ + Ta = 'TA', /** Tunisia. */ Tn = 'TN', /** Turkey. */ @@ -1687,7 +1691,7 @@ export type CreditCard = { export type CreditCardPaymentInput = { /** The amount of the payment. */ amount: Scalars['Money'] - /** A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. */ + /** A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. For more information, refer to [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). */ idempotencyKey: Scalars['String'] /** The billing address for the payment. */ billingAddress: MailingAddressInput @@ -1704,7 +1708,7 @@ export type CreditCardPaymentInput = { export type CreditCardPaymentInputV2 = { /** The amount and currency of the payment. */ paymentAmount: MoneyInput - /** A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. */ + /** A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. For more information, refer to [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). */ idempotencyKey: Scalars['String'] /** The billing address for the payment. */ billingAddress: MailingAddressInput @@ -1766,10 +1770,6 @@ export enum CurrencyCode { Bhd = 'BHD', /** Burundian Franc (BIF). */ Bif = 'BIF', - /** Belarusian Ruble (BYN). */ - Byn = 'BYN', - /** Belarusian Ruble (BYR). */ - Byr = 'BYR', /** Belize Dollar (BZD). */ Bzd = 'BZD', /** Bermudian Dollar (BMD). */ @@ -1816,26 +1816,18 @@ export enum CurrencyCode { Czk = 'CZK', /** Danish Kroner (DKK). */ Dkk = 'DKK', - /** Djiboutian Franc (DJF). */ - Djf = 'DJF', /** Dominican Peso (DOP). */ Dop = 'DOP', /** East Caribbean Dollar (XCD). */ Xcd = 'XCD', /** Egyptian Pound (EGP). */ Egp = 'EGP', - /** Eritrean Nakfa (ERN). */ - Ern = 'ERN', /** Ethiopian Birr (ETB). */ Etb = 'ETB', - /** Falkland Islands Pounds (FKP). */ - Fkp = 'FKP', /** CFP Franc (XPF). */ Xpf = 'XPF', /** Fijian Dollars (FJD). */ Fjd = 'FJD', - /** Gibraltar Pounds (GIP). */ - Gip = 'GIP', /** Gambian Dalasi (GMD). */ Gmd = 'GMD', /** Ghanaian Cedi (GHS). */ @@ -1846,8 +1838,6 @@ export enum CurrencyCode { Gyd = 'GYD', /** Georgian Lari (GEL). */ Gel = 'GEL', - /** Guinean Franc (GNF). */ - Gnf = 'GNF', /** Haitian Gourde (HTG). */ Htg = 'HTG', /** Honduran Lempira (HNL). */ @@ -1864,8 +1854,6 @@ export enum CurrencyCode { Idr = 'IDR', /** Israeli New Shekel (NIS). */ Ils = 'ILS', - /** Iranian Rial (IRR). */ - Irr = 'IRR', /** Iraqi Dinar (IQD). */ Iqd = 'IQD', /** Jamaican Dollars (JMD). */ @@ -1880,8 +1868,6 @@ export enum CurrencyCode { Kzt = 'KZT', /** Kenyan Shilling (KES). */ Kes = 'KES', - /** Kiribati Dollar (KID). */ - Kid = 'KID', /** Kuwaiti Dinar (KWD). */ Kwd = 'KWD', /** Kyrgyzstani Som (KGS). */ @@ -1896,8 +1882,6 @@ export enum CurrencyCode { Lsl = 'LSL', /** Liberian Dollar (LRD). */ Lrd = 'LRD', - /** Libyan Dinar (LYD). */ - Lyd = 'LYD', /** Lithuanian Litai (LTL). */ Ltl = 'LTL', /** Malagasy Ariary (MGA). */ @@ -1910,8 +1894,6 @@ export enum CurrencyCode { Mwk = 'MWK', /** Maldivian Rufiyaa (MVR). */ Mvr = 'MVR', - /** Mauritanian Ouguiya (MRU). */ - Mru = 'MRU', /** Mexican Pesos (MXN). */ Mxn = 'MXN', /** Malaysian Ringgits (MYR). */ @@ -1966,8 +1948,6 @@ export enum CurrencyCode { Rwf = 'RWF', /** Samoan Tala (WST). */ Wst = 'WST', - /** Saint Helena Pounds (SHP). */ - Shp = 'SHP', /** Saudi Riyal (SAR). */ Sar = 'SAR', /** Sao Tome And Principe Dobra (STD). */ @@ -1976,14 +1956,10 @@ export enum CurrencyCode { Rsd = 'RSD', /** Seychellois Rupee (SCR). */ Scr = 'SCR', - /** Sierra Leonean Leone (SLL). */ - Sll = 'SLL', /** Singapore Dollars (SGD). */ Sgd = 'SGD', /** Sudanese Pound (SDG). */ Sdg = 'SDG', - /** Somali Shilling (SOS). */ - Sos = 'SOS', /** Syrian Pound (SYP). */ Syp = 'SYP', /** South African Rand (ZAR). */ @@ -2008,12 +1984,8 @@ export enum CurrencyCode { Twd = 'TWD', /** Thai baht (THB). */ Thb = 'THB', - /** Tajikistani Somoni (TJS). */ - Tjs = 'TJS', /** Tanzanian Shilling (TZS). */ Tzs = 'TZS', - /** Tongan Pa'anga (TOP). */ - Top = 'TOP', /** Trinidad and Tobago Dollars (TTD). */ Ttd = 'TTD', /** Tunisian Dinar (TND). */ @@ -2034,10 +2006,6 @@ export enum CurrencyCode { Uzs = 'UZS', /** Vanuatu Vatu (VUV). */ Vuv = 'VUV', - /** Venezuelan Bolivares (VEF). */ - Vef = 'VEF', - /** Venezuelan Bolivares (VES). */ - Ves = 'VES', /** Vietnamese đồng (VND). */ Vnd = 'VND', /** West African CFA franc (XOF). */ @@ -2046,6 +2014,42 @@ export enum CurrencyCode { Yer = 'YER', /** Zambian Kwacha (ZMW). */ Zmw = 'ZMW', + /** Belarusian Ruble (BYN). */ + Byn = 'BYN', + /** Belarusian Ruble (BYR). */ + Byr = 'BYR', + /** Djiboutian Franc (DJF). */ + Djf = 'DJF', + /** Eritrean Nakfa (ERN). */ + Ern = 'ERN', + /** Falkland Islands Pounds (FKP). */ + Fkp = 'FKP', + /** Gibraltar Pounds (GIP). */ + Gip = 'GIP', + /** Guinean Franc (GNF). */ + Gnf = 'GNF', + /** Iranian Rial (IRR). */ + Irr = 'IRR', + /** Kiribati Dollar (KID). */ + Kid = 'KID', + /** Libyan Dinar (LYD). */ + Lyd = 'LYD', + /** Mauritanian Ouguiya (MRU). */ + Mru = 'MRU', + /** Sierra Leonean Leone (SLL). */ + Sll = 'SLL', + /** Saint Helena Pounds (SHP). */ + Shp = 'SHP', + /** Somali Shilling (SOS). */ + Sos = 'SOS', + /** Tajikistani Somoni (TJS). */ + Tjs = 'TJS', + /** Tongan Pa'anga (TOP). */ + Top = 'TOP', + /** Venezuelan Bolivares (VEF). */ + Vef = 'VEF', + /** Venezuelan Bolivares (VES). */ + Ves = 'VES', } /** A customer represents a customer account with the shop. Customer accounts store contact information for the customer, saving logged-in customers the trouble of having to provide it at every checkout. */ @@ -3817,7 +3821,11 @@ export type Payment = Node & { errorMessage?: Maybe /** Globally unique identifier. */ id: Scalars['ID'] - /** A client-side generated token to identify a payment and perform idempotent operations. */ + /** + * A client-side generated token to identify a payment and perform idempotent operations. + * For more information, refer to + * [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). + */ idempotencyKey?: Maybe /** The URL where the customer needs to be redirected so they can complete the 3D Secure payment flow. */ nextActionUrl?: Maybe @@ -4386,7 +4394,9 @@ export type QueryRoot = { collections: CollectionConnection /** Find a customer by its access token. */ customer?: Maybe + /** Returns a specific node by ID. */ node?: Maybe + /** Returns the list of nodes with the given IDs. */ nodes: Array> /** Find a page by its handle. */ pageByHandle?: Maybe @@ -4768,7 +4778,7 @@ export type StringEdge = { export type TokenizedPaymentInput = { /** The amount of the payment. */ amount: Scalars['Money'] - /** A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. */ + /** A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. For more information, refer to [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). */ idempotencyKey: Scalars['String'] /** The billing address for the payment. */ billingAddress: MailingAddressInput @@ -4789,7 +4799,7 @@ export type TokenizedPaymentInput = { export type TokenizedPaymentInputV2 = { /** The amount and currency of the payment. */ paymentAmount: MoneyInput - /** A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. */ + /** A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. For more information, refer to [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). */ idempotencyKey: Scalars['String'] /** The billing address for the payment. */ billingAddress: MailingAddressInput @@ -4810,7 +4820,7 @@ export type TokenizedPaymentInputV2 = { export type TokenizedPaymentInputV3 = { /** The amount and currency of the payment. */ paymentAmount: MoneyInput - /** A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. */ + /** A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. For more information, refer to [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). */ idempotencyKey: Scalars['String'] /** The billing address for the payment. */ billingAddress: MailingAddressInput @@ -4847,18 +4857,32 @@ export type Transaction = { test: Scalars['Boolean'] } +/** The different kinds of order transactions. */ export enum TransactionKind { + /** An authorization and capture performed together in a single step. */ Sale = 'SALE', + /** A transfer of the money that was reserved during the authorization stage. */ Capture = 'CAPTURE', + /** + * An amount reserved against the cardholder's funding source. + * Money does not change hands until the authorization is captured. + */ Authorization = 'AUTHORIZATION', + /** An authorization for a payment taken with an EMV credit card reader. */ EmvAuthorization = 'EMV_AUTHORIZATION', + /** Money returned to the customer when they have paid too much. */ Change = 'CHANGE', } +/** Transaction statuses describe the status of a transaction. */ export enum TransactionStatus { + /** The transaction is pending. */ Pending = 'PENDING', + /** The transaction succeeded. */ Success = 'SUCCESS', + /** The transaction failed. */ Failure = 'FAILURE', + /** There was an error while processing the transaction. */ Error = 'ERROR', } @@ -4967,19 +4991,596 @@ export enum WeightUnit { Ounces = 'OUNCES', } -export type Unnamed_1_QueryVariables = Exact<{ +export type AssociateCustomerWithCheckoutMutationVariables = Exact<{ + checkoutId: Scalars['ID'] + customerAccessToken: Scalars['String'] +}> + +export type AssociateCustomerWithCheckoutMutation = { + __typename?: 'Mutation' +} & { + checkoutCustomerAssociateV2?: Maybe< + { __typename?: 'CheckoutCustomerAssociateV2Payload' } & { + checkout?: Maybe<{ __typename?: 'Checkout' } & Pick> + checkoutUserErrors: Array< + { __typename?: 'CheckoutUserError' } & Pick< + CheckoutUserError, + 'code' | 'field' | 'message' + > + > + customer?: Maybe<{ __typename?: 'Customer' } & Pick> + } + > +} + +export type CheckoutCreateMutationVariables = Exact<{ + input?: Maybe +}> + +export type CheckoutCreateMutation = { __typename?: 'Mutation' } & { + checkoutCreate?: Maybe< + { __typename?: 'CheckoutCreatePayload' } & { + checkoutUserErrors: Array< + { __typename?: 'CheckoutUserError' } & Pick< + CheckoutUserError, + 'code' | 'field' | 'message' + > + > + checkout?: Maybe<{ __typename?: 'Checkout' } & CheckoutDetailsFragment> + } + > +} + +export type CheckoutLineItemAddMutationVariables = Exact<{ + checkoutId: Scalars['ID'] + lineItems: Array | CheckoutLineItemInput +}> + +export type CheckoutLineItemAddMutation = { __typename?: 'Mutation' } & { + checkoutLineItemsAdd?: Maybe< + { __typename?: 'CheckoutLineItemsAddPayload' } & { + checkoutUserErrors: Array< + { __typename?: 'CheckoutUserError' } & Pick< + CheckoutUserError, + 'code' | 'field' | 'message' + > + > + checkout?: Maybe<{ __typename?: 'Checkout' } & CheckoutDetailsFragment> + } + > +} + +export type CheckoutLineItemRemoveMutationVariables = Exact<{ + checkoutId: Scalars['ID'] + lineItemIds: Array | Scalars['ID'] +}> + +export type CheckoutLineItemRemoveMutation = { __typename?: 'Mutation' } & { + checkoutLineItemsRemove?: Maybe< + { __typename?: 'CheckoutLineItemsRemovePayload' } & { + checkoutUserErrors: Array< + { __typename?: 'CheckoutUserError' } & Pick< + CheckoutUserError, + 'code' | 'field' | 'message' + > + > + checkout?: Maybe<{ __typename?: 'Checkout' } & CheckoutDetailsFragment> + } + > +} + +export type CheckoutLineItemUpdateMutationVariables = Exact<{ + checkoutId: Scalars['ID'] + lineItems: Array | CheckoutLineItemUpdateInput +}> + +export type CheckoutLineItemUpdateMutation = { __typename?: 'Mutation' } & { + checkoutLineItemsUpdate?: Maybe< + { __typename?: 'CheckoutLineItemsUpdatePayload' } & { + checkoutUserErrors: Array< + { __typename?: 'CheckoutUserError' } & Pick< + CheckoutUserError, + 'code' | 'field' | 'message' + > + > + checkout?: Maybe<{ __typename?: 'Checkout' } & CheckoutDetailsFragment> + } + > +} + +export type CustomerAccessTokenCreateMutationVariables = Exact<{ + input: CustomerAccessTokenCreateInput +}> + +export type CustomerAccessTokenCreateMutation = { __typename?: 'Mutation' } & { + customerAccessTokenCreate?: Maybe< + { __typename?: 'CustomerAccessTokenCreatePayload' } & { + customerAccessToken?: Maybe< + { __typename?: 'CustomerAccessToken' } & Pick< + CustomerAccessToken, + 'accessToken' | 'expiresAt' + > + > + customerUserErrors: Array< + { __typename?: 'CustomerUserError' } & Pick< + CustomerUserError, + 'code' | 'field' | 'message' + > + > + } + > +} + +export type CustomerAccessTokenDeleteMutationVariables = Exact<{ + customerAccessToken: Scalars['String'] +}> + +export type CustomerAccessTokenDeleteMutation = { __typename?: 'Mutation' } & { + customerAccessTokenDelete?: Maybe< + { __typename?: 'CustomerAccessTokenDeletePayload' } & Pick< + CustomerAccessTokenDeletePayload, + 'deletedAccessToken' | 'deletedCustomerAccessTokenId' + > & { + userErrors: Array< + { __typename?: 'UserError' } & Pick + > + } + > +} + +export type CustomerActivateByUrlMutationVariables = Exact<{ + activationUrl: Scalars['URL'] + password: Scalars['String'] +}> + +export type CustomerActivateByUrlMutation = { __typename?: 'Mutation' } & { + customerActivateByUrl?: Maybe< + { __typename?: 'CustomerActivateByUrlPayload' } & { + customer?: Maybe<{ __typename?: 'Customer' } & Pick> + customerAccessToken?: Maybe< + { __typename?: 'CustomerAccessToken' } & Pick< + CustomerAccessToken, + 'accessToken' | 'expiresAt' + > + > + customerUserErrors: Array< + { __typename?: 'CustomerUserError' } & Pick< + CustomerUserError, + 'code' | 'field' | 'message' + > + > + } + > +} + +export type CustomerActivateMutationVariables = Exact<{ + id: Scalars['ID'] + input: CustomerActivateInput +}> + +export type CustomerActivateMutation = { __typename?: 'Mutation' } & { + customerActivate?: Maybe< + { __typename?: 'CustomerActivatePayload' } & { + customer?: Maybe<{ __typename?: 'Customer' } & Pick> + customerAccessToken?: Maybe< + { __typename?: 'CustomerAccessToken' } & Pick< + CustomerAccessToken, + 'accessToken' | 'expiresAt' + > + > + customerUserErrors: Array< + { __typename?: 'CustomerUserError' } & Pick< + CustomerUserError, + 'code' | 'field' | 'message' + > + > + } + > +} + +export type CustomerCreateMutationVariables = Exact<{ + input: CustomerCreateInput +}> + +export type CustomerCreateMutation = { __typename?: 'Mutation' } & { + customerCreate?: Maybe< + { __typename?: 'CustomerCreatePayload' } & { + customerUserErrors: Array< + { __typename?: 'CustomerUserError' } & Pick< + CustomerUserError, + 'code' | 'field' | 'message' + > + > + customer?: Maybe<{ __typename?: 'Customer' } & Pick> + } + > +} + +export type GetSiteCollectionsQueryVariables = Exact<{ first: Scalars['Int'] }> -export type Unnamed_1_Query = { __typename?: 'QueryRoot' } & { +export type GetSiteCollectionsQuery = { __typename?: 'QueryRoot' } & { + collections: { __typename?: 'CollectionConnection' } & { + edges: Array< + { __typename?: 'CollectionEdge' } & { + node: { __typename?: 'Collection' } & Pick< + Collection, + 'id' | 'title' | 'handle' + > + } + > + } +} + +export type GetAllPagesQueryVariables = Exact<{ + first?: Maybe +}> + +export type GetAllPagesQuery = { __typename?: 'QueryRoot' } & { pages: { __typename?: 'PageConnection' } & { edges: Array< { __typename?: 'PageEdge' } & { - node: { __typename?: 'Page' } & Pick< - Page, - 'id' | 'title' | 'handle' | 'body' | 'bodySummary' | 'url' - > + node: { __typename?: 'Page' } & Pick } > } } + +export type GetAllProductVendorsQueryVariables = Exact<{ + first?: Maybe + cursor?: Maybe +}> + +export type GetAllProductVendorsQuery = { __typename?: 'QueryRoot' } & { + products: { __typename?: 'ProductConnection' } & { + pageInfo: { __typename?: 'PageInfo' } & Pick< + PageInfo, + 'hasNextPage' | 'hasPreviousPage' + > + edges: Array< + { __typename?: 'ProductEdge' } & Pick & { + node: { __typename?: 'Product' } & Pick + } + > + } +} + +export type GetAllProductPathsQueryVariables = Exact<{ + first?: Maybe + cursor?: Maybe +}> + +export type GetAllProductPathsQuery = { __typename?: 'QueryRoot' } & { + products: { __typename?: 'ProductConnection' } & { + pageInfo: { __typename?: 'PageInfo' } & Pick< + PageInfo, + 'hasNextPage' | 'hasPreviousPage' + > + edges: Array< + { __typename?: 'ProductEdge' } & Pick & { + node: { __typename?: 'Product' } & Pick + } + > + } +} + +export type ProductConnectionFragment = { __typename?: 'ProductConnection' } & { + pageInfo: { __typename?: 'PageInfo' } & Pick< + PageInfo, + 'hasNextPage' | 'hasPreviousPage' + > + edges: Array< + { __typename?: 'ProductEdge' } & { + node: { __typename?: 'Product' } & Pick< + Product, + 'id' | 'title' | 'vendor' | 'handle' + > & { + priceRange: { __typename?: 'ProductPriceRange' } & { + minVariantPrice: { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + } + images: { __typename?: 'ImageConnection' } & { + pageInfo: { __typename?: 'PageInfo' } & Pick< + PageInfo, + 'hasNextPage' | 'hasPreviousPage' + > + edges: Array< + { __typename?: 'ImageEdge' } & { + node: { __typename?: 'Image' } & Pick< + Image, + 'originalSrc' | 'altText' | 'width' | 'height' + > + } + > + } + } + } + > +} + +export type GetAllProductsQueryVariables = Exact<{ + first?: Maybe + query?: Maybe + sortKey?: Maybe + reverse?: Maybe +}> + +export type GetAllProductsQuery = { __typename?: 'QueryRoot' } & { + products: { __typename?: 'ProductConnection' } & ProductConnectionFragment +} + +export type CheckoutDetailsFragment = { __typename?: 'Checkout' } & Pick< + Checkout, + 'id' | 'webUrl' | 'completedAt' | 'createdAt' | 'taxesIncluded' +> & { + subtotalPriceV2: { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + totalTaxV2: { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + totalPriceV2: { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + lineItems: { __typename?: 'CheckoutLineItemConnection' } & { + pageInfo: { __typename?: 'PageInfo' } & Pick< + PageInfo, + 'hasNextPage' | 'hasPreviousPage' + > + edges: Array< + { __typename?: 'CheckoutLineItemEdge' } & { + node: { __typename?: 'CheckoutLineItem' } & Pick< + CheckoutLineItem, + 'id' | 'title' | 'quantity' + > & { + variant?: Maybe< + { __typename?: 'ProductVariant' } & Pick< + ProductVariant, + 'id' | 'sku' | 'title' + > & { + image?: Maybe< + { __typename?: 'Image' } & Pick< + Image, + 'originalSrc' | 'altText' | 'width' | 'height' + > + > + priceV2: { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + compareAtPriceV2?: Maybe< + { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + > + product: { __typename?: 'Product' } & Pick< + Product, + 'handle' + > + } + > + } + } + > + } + } + +export type GetCheckoutQueryVariables = Exact<{ + checkoutId: Scalars['ID'] +}> + +export type GetCheckoutQuery = { __typename?: 'QueryRoot' } & { + node?: Maybe< + | { __typename?: 'AppliedGiftCard' } + | { __typename?: 'Article' } + | { __typename?: 'Blog' } + | ({ __typename?: 'Checkout' } & CheckoutDetailsFragment) + | { __typename?: 'CheckoutLineItem' } + | { __typename?: 'Collection' } + | { __typename?: 'Comment' } + | { __typename?: 'ExternalVideo' } + | { __typename?: 'MailingAddress' } + | { __typename?: 'MediaImage' } + | { __typename?: 'Metafield' } + | { __typename?: 'Model3d' } + | { __typename?: 'Order' } + | { __typename?: 'Page' } + | { __typename?: 'Payment' } + | { __typename?: 'Product' } + | { __typename?: 'ProductOption' } + | { __typename?: 'ProductVariant' } + | { __typename?: 'ShopPolicy' } + | { __typename?: 'Video' } + > +} + +export type GetProductsFromCollectionQueryVariables = Exact<{ + categoryId: Scalars['ID'] + first?: Maybe + sortKey?: Maybe + reverse?: Maybe +}> + +export type GetProductsFromCollectionQuery = { __typename?: 'QueryRoot' } & { + node?: Maybe< + | ({ __typename?: 'AppliedGiftCard' } & Pick) + | ({ __typename?: 'Article' } & Pick) + | ({ __typename?: 'Blog' } & Pick) + | ({ __typename?: 'Checkout' } & Pick) + | ({ __typename?: 'CheckoutLineItem' } & Pick) + | ({ __typename?: 'Collection' } & Pick & { + products: { + __typename?: 'ProductConnection' + } & ProductConnectionFragment + }) + | ({ __typename?: 'Comment' } & Pick) + | ({ __typename?: 'ExternalVideo' } & Pick) + | ({ __typename?: 'MailingAddress' } & Pick) + | ({ __typename?: 'MediaImage' } & Pick) + | ({ __typename?: 'Metafield' } & Pick) + | ({ __typename?: 'Model3d' } & Pick) + | ({ __typename?: 'Order' } & Pick) + | ({ __typename?: 'Page' } & Pick) + | ({ __typename?: 'Payment' } & Pick) + | ({ __typename?: 'Product' } & Pick) + | ({ __typename?: 'ProductOption' } & Pick) + | ({ __typename?: 'ProductVariant' } & Pick) + | ({ __typename?: 'ShopPolicy' } & Pick) + | ({ __typename?: 'Video' } & Pick) + > +} + +export type GetCustomerIdQueryVariables = Exact<{ + customerAccessToken: Scalars['String'] +}> + +export type GetCustomerIdQuery = { __typename?: 'QueryRoot' } & { + customer?: Maybe<{ __typename?: 'Customer' } & Pick> +} + +export type GetCustomerQueryVariables = Exact<{ + customerAccessToken: Scalars['String'] +}> + +export type GetCustomerQuery = { __typename?: 'QueryRoot' } & { + customer?: Maybe< + { __typename?: 'Customer' } & Pick< + Customer, + | 'id' + | 'firstName' + | 'lastName' + | 'displayName' + | 'email' + | 'phone' + | 'tags' + | 'acceptsMarketing' + | 'createdAt' + > + > +} + +export type GetPageQueryVariables = Exact<{ + id: Scalars['ID'] +}> + +export type GetPageQuery = { __typename?: 'QueryRoot' } & { + node?: Maybe< + | ({ __typename?: 'AppliedGiftCard' } & Pick) + | ({ __typename?: 'Article' } & Pick) + | ({ __typename?: 'Blog' } & Pick) + | ({ __typename?: 'Checkout' } & Pick) + | ({ __typename?: 'CheckoutLineItem' } & Pick) + | ({ __typename?: 'Collection' } & Pick) + | ({ __typename?: 'Comment' } & Pick) + | ({ __typename?: 'ExternalVideo' } & Pick) + | ({ __typename?: 'MailingAddress' } & Pick) + | ({ __typename?: 'MediaImage' } & Pick) + | ({ __typename?: 'Metafield' } & Pick) + | ({ __typename?: 'Model3d' } & Pick) + | ({ __typename?: 'Order' } & Pick) + | ({ __typename?: 'Page' } & Pick< + Page, + 'title' | 'handle' | 'body' | 'bodySummary' | 'id' + >) + | ({ __typename?: 'Payment' } & Pick) + | ({ __typename?: 'Product' } & Pick) + | ({ __typename?: 'ProductOption' } & Pick) + | ({ __typename?: 'ProductVariant' } & Pick) + | ({ __typename?: 'ShopPolicy' } & Pick) + | ({ __typename?: 'Video' } & Pick) + > +} + +export type GetProductBySlugQueryVariables = Exact<{ + slug: Scalars['String'] +}> + +export type GetProductBySlugQuery = { __typename?: 'QueryRoot' } & { + productByHandle?: Maybe< + { __typename?: 'Product' } & Pick< + Product, + | 'id' + | 'handle' + | 'title' + | 'productType' + | 'vendor' + | 'description' + | 'descriptionHtml' + > & { + options: Array< + { __typename?: 'ProductOption' } & Pick< + ProductOption, + 'id' | 'name' | 'values' + > + > + priceRange: { __typename?: 'ProductPriceRange' } & { + maxVariantPrice: { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + minVariantPrice: { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + } + variants: { __typename?: 'ProductVariantConnection' } & { + pageInfo: { __typename?: 'PageInfo' } & Pick< + PageInfo, + 'hasNextPage' | 'hasPreviousPage' + > + edges: Array< + { __typename?: 'ProductVariantEdge' } & { + node: { __typename?: 'ProductVariant' } & Pick< + ProductVariant, + 'id' | 'title' | 'sku' + > & { + selectedOptions: Array< + { __typename?: 'SelectedOption' } & Pick< + SelectedOption, + 'name' | 'value' + > + > + priceV2: { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + compareAtPriceV2?: Maybe< + { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + > + } + } + > + } + images: { __typename?: 'ImageConnection' } & { + pageInfo: { __typename?: 'PageInfo' } & Pick< + PageInfo, + 'hasNextPage' | 'hasPreviousPage' + > + edges: Array< + { __typename?: 'ImageEdge' } & { + node: { __typename?: 'Image' } & Pick< + Image, + 'originalSrc' | 'altText' | 'width' | 'height' + > + } + > + } + } + > +} + +export type GetSiteInfoQueryVariables = Exact<{ [key: string]: never }> + +export type GetSiteInfoQuery = { __typename?: 'QueryRoot' } & { + shop: { __typename?: 'Shop' } & Pick +} diff --git a/framework/shopify/schema.graphql b/framework/shopify/schema.graphql index 822e6007eb..9c657fe43d 100644 --- a/framework/shopify/schema.graphql +++ b/framework/shopify/schema.graphql @@ -13,6 +13,16 @@ directive @accessRestricted( reason: String = null ) on FIELD_DEFINITION | OBJECT +""" +Contextualize data. +""" +directive @inContext( + """ + The country code for context. + """ + country: CountryCode! +) on QUERY | MUTATION + """ A version of the API. """ @@ -28,7 +38,7 @@ type ApiVersion { handle: String! """ - Whether the version is supported by Shopify. + Whether the version is actively supported by Shopify. Supported API versions are guaranteed to be stable. Unsupported API versions include unstable, release candidate, and end-of-life versions that are marked as unsupported. For more information, refer to [Versioning](https://shopify.dev/concepts/about-apis/versioning). """ supported: Boolean! } @@ -547,32 +557,32 @@ Card brand, such as Visa or Mastercard, which can be used for payments. """ enum CardBrand { """ - Visa + Visa. """ VISA """ - Mastercard + Mastercard. """ MASTERCARD """ - Discover + Discover. """ DISCOVER """ - American Express + American Express. """ AMERICAN_EXPRESS """ - Diners Club + Diners Club. """ DINERS_CLUB """ - JCB + JCB. """ JCB } @@ -2142,6 +2152,11 @@ enum CountryCode { """ AW + """ + Ascension Island. + """ + AC + """ Australia. """ @@ -3187,6 +3202,11 @@ enum CountryCode { """ TT + """ + Tristan da Cunha. + """ + TA + """ Tunisia. """ @@ -3354,7 +3374,7 @@ input CreditCardPaymentInput { amount: Money! """ - A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. + A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. For more information, refer to [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). """ idempotencyKey: String! @@ -3385,7 +3405,7 @@ input CreditCardPaymentInputV2 { paymentAmount: MoneyInput! """ - A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. + A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. For more information, refer to [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). """ idempotencyKey: String! @@ -3529,16 +3549,6 @@ enum CurrencyCode { """ BIF - """ - Belarusian Ruble (BYN). - """ - BYN - - """ - Belarusian Ruble (BYR). - """ - BYR - """ Belize Dollar (BZD). """ @@ -3654,11 +3664,6 @@ enum CurrencyCode { """ DKK - """ - Djiboutian Franc (DJF). - """ - DJF - """ Dominican Peso (DOP). """ @@ -3674,21 +3679,11 @@ enum CurrencyCode { """ EGP - """ - Eritrean Nakfa (ERN). - """ - ERN - """ Ethiopian Birr (ETB). """ ETB - """ - Falkland Islands Pounds (FKP). - """ - FKP - """ CFP Franc (XPF). """ @@ -3699,11 +3694,6 @@ enum CurrencyCode { """ FJD - """ - Gibraltar Pounds (GIP). - """ - GIP - """ Gambian Dalasi (GMD). """ @@ -3729,11 +3719,6 @@ enum CurrencyCode { """ GEL - """ - Guinean Franc (GNF). - """ - GNF - """ Haitian Gourde (HTG). """ @@ -3774,11 +3759,6 @@ enum CurrencyCode { """ ILS - """ - Iranian Rial (IRR). - """ - IRR - """ Iraqi Dinar (IQD). """ @@ -3814,11 +3794,6 @@ enum CurrencyCode { """ KES - """ - Kiribati Dollar (KID). - """ - KID - """ Kuwaiti Dinar (KWD). """ @@ -3854,11 +3829,6 @@ enum CurrencyCode { """ LRD - """ - Libyan Dinar (LYD). - """ - LYD - """ Lithuanian Litai (LTL). """ @@ -3889,11 +3859,6 @@ enum CurrencyCode { """ MVR - """ - Mauritanian Ouguiya (MRU). - """ - MRU - """ Mexican Pesos (MXN). """ @@ -4029,11 +3994,6 @@ enum CurrencyCode { """ WST - """ - Saint Helena Pounds (SHP). - """ - SHP - """ Saudi Riyal (SAR). """ @@ -4054,11 +4014,6 @@ enum CurrencyCode { """ SCR - """ - Sierra Leonean Leone (SLL). - """ - SLL - """ Singapore Dollars (SGD). """ @@ -4069,11 +4024,6 @@ enum CurrencyCode { """ SDG - """ - Somali Shilling (SOS). - """ - SOS - """ Syrian Pound (SYP). """ @@ -4134,21 +4084,11 @@ enum CurrencyCode { """ THB - """ - Tajikistani Somoni (TJS). - """ - TJS - """ Tanzanian Shilling (TZS). """ TZS - """ - Tongan Pa'anga (TOP). - """ - TOP - """ Trinidad and Tobago Dollars (TTD). """ @@ -4199,16 +4139,6 @@ enum CurrencyCode { """ VUV - """ - Venezuelan Bolivares (VEF). - """ - VEF - - """ - Venezuelan Bolivares (VES). - """ - VES - """ Vietnamese đồng (VND). """ @@ -4228,6 +4158,96 @@ enum CurrencyCode { Zambian Kwacha (ZMW). """ ZMW + + """ + Belarusian Ruble (BYN). + """ + BYN + + """ + Belarusian Ruble (BYR). + """ + BYR + + """ + Djiboutian Franc (DJF). + """ + DJF + + """ + Eritrean Nakfa (ERN). + """ + ERN + + """ + Falkland Islands Pounds (FKP). + """ + FKP + + """ + Gibraltar Pounds (GIP). + """ + GIP + + """ + Guinean Franc (GNF). + """ + GNF + + """ + Iranian Rial (IRR). + """ + IRR + + """ + Kiribati Dollar (KID). + """ + KID + + """ + Libyan Dinar (LYD). + """ + LYD + + """ + Mauritanian Ouguiya (MRU). + """ + MRU + + """ + Sierra Leonean Leone (SLL). + """ + SLL + + """ + Saint Helena Pounds (SHP). + """ + SHP + + """ + Somali Shilling (SOS). + """ + SOS + + """ + Tajikistani Somoni (TJS). + """ + TJS + + """ + Tongan Pa'anga (TOP). + """ + TOP + + """ + Venezuelan Bolivares (VEF). + """ + VEF + + """ + Venezuelan Bolivares (VES). + """ + VES } """ @@ -7355,6 +7375,8 @@ type Payment implements Node { """ A client-side generated token to identify a payment and perform idempotent operations. + For more information, refer to + [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). """ idempotencyKey: String @@ -8589,12 +8611,20 @@ type QueryRoot { """ customerAccessToken: String! ): Customer + + """ + Returns a specific node by ID. + """ node( """ The ID of the Node to return. """ id: ID! ): Node + + """ + Returns the list of nodes with the given IDs. + """ nodes( """ The IDs of the Nodes to return. @@ -9246,7 +9276,7 @@ input TokenizedPaymentInput { amount: Money! """ - A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. + A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. For more information, refer to [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). """ idempotencyKey: String! @@ -9287,7 +9317,7 @@ input TokenizedPaymentInputV2 { paymentAmount: MoneyInput! """ - A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. + A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. For more information, refer to [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). """ idempotencyKey: String! @@ -9328,7 +9358,7 @@ input TokenizedPaymentInputV3 { paymentAmount: MoneyInput! """ - A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. + A unique client generated key used to avoid duplicate charges. When a duplicate payment is found, the original is returned instead of creating a new one. For more information, refer to [Idempotent requests](https://shopify.dev/concepts/about-apis/idempotent-requests). """ idempotencyKey: String! @@ -9393,18 +9423,59 @@ type Transaction { test: Boolean! } +""" +The different kinds of order transactions. +""" enum TransactionKind { + """ + An authorization and capture performed together in a single step. + """ SALE + + """ + A transfer of the money that was reserved during the authorization stage. + """ CAPTURE + + """ + An amount reserved against the cardholder's funding source. + Money does not change hands until the authorization is captured. + """ AUTHORIZATION + + """ + An authorization for a payment taken with an EMV credit card reader. + """ EMV_AUTHORIZATION + + """ + Money returned to the customer when they have paid too much. + """ CHANGE } +""" +Transaction statuses describe the status of a transaction. +""" enum TransactionStatus { + """ + The transaction is pending. + """ PENDING + + """ + The transaction succeeded. + """ SUCCESS + + """ + The transaction failed. + """ FAILURE + + """ + There was an error while processing the transaction. + """ ERROR } diff --git a/framework/shopify/types.ts b/framework/shopify/types.ts deleted file mode 100644 index e7bcb2476c..0000000000 --- a/framework/shopify/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as Core from '@commerce/types' -import { CheckoutLineItem } from './schema' - -export type ShopifyCheckout = { - id: string - webUrl: string - lineItems: CheckoutLineItem[] -} - -export type Cart = Core.Cart & { - lineItems: LineItem[] -} -export interface LineItem extends Core.LineItem { - options?: any[] -} - -/** - * Cart mutations - */ - -export type OptionSelections = { - option_id: number - option_value: number | string -} - -export type CartItemBody = Core.CartItemBody & { - productId: string // The product id is always required for BC - optionSelections?: OptionSelections -} - -export type GetCartHandlerBody = Core.GetCartHandlerBody - -export type AddCartItemBody = Core.AddCartItemBody - -export type AddCartItemHandlerBody = Core.AddCartItemHandlerBody - -export type UpdateCartItemBody = Core.UpdateCartItemBody - -export type UpdateCartItemHandlerBody = Core.UpdateCartItemHandlerBody - -export type RemoveCartItemBody = Core.RemoveCartItemBody - -export type RemoveCartItemHandlerBody = Core.RemoveCartItemHandlerBody diff --git a/framework/shopify/types/cart.ts b/framework/shopify/types/cart.ts new file mode 100644 index 0000000000..09410740a2 --- /dev/null +++ b/framework/shopify/types/cart.ts @@ -0,0 +1,32 @@ +import * as Core from '@commerce/types/cart' + +export * from '@commerce/types/cart' + +export type ShopifyCart = {} + +/** + * Extend core cart types + */ + +export type Cart = Core.Cart & { + lineItems: Core.LineItem[] + url?: string +} + +export type CartTypes = Core.CartTypes + +export type CartHooks = Core.CartHooks + +export type GetCartHook = CartHooks['getCart'] +export type AddItemHook = CartHooks['addItem'] +export type UpdateItemHook = CartHooks['updateItem'] +export type RemoveItemHook = CartHooks['removeItem'] + +export type CartSchema = Core.CartSchema + +export type CartHandlers = Core.CartHandlers + +export type GetCartHandler = CartHandlers['getCart'] +export type AddItemHandler = CartHandlers['addItem'] +export type UpdateItemHandler = CartHandlers['updateItem'] +export type RemoveItemHandler = CartHandlers['removeItem'] diff --git a/framework/shopify/types/checkout.ts b/framework/shopify/types/checkout.ts new file mode 100644 index 0000000000..4e2412ef6c --- /dev/null +++ b/framework/shopify/types/checkout.ts @@ -0,0 +1 @@ +export * from '@commerce/types/checkout' diff --git a/framework/shopify/types/common.ts b/framework/shopify/types/common.ts new file mode 100644 index 0000000000..b52c33a4de --- /dev/null +++ b/framework/shopify/types/common.ts @@ -0,0 +1 @@ +export * from '@commerce/types/common' diff --git a/framework/shopify/types/customer.ts b/framework/shopify/types/customer.ts new file mode 100644 index 0000000000..427bc0b03c --- /dev/null +++ b/framework/shopify/types/customer.ts @@ -0,0 +1,5 @@ +import * as Core from '@commerce/types/customer' + +export * from '@commerce/types/customer' + +export type CustomerSchema = Core.CustomerSchema diff --git a/framework/shopify/types/index.ts b/framework/shopify/types/index.ts new file mode 100644 index 0000000000..7ab0b7f64f --- /dev/null +++ b/framework/shopify/types/index.ts @@ -0,0 +1,25 @@ +import * as Cart from './cart' +import * as Checkout from './checkout' +import * as Common from './common' +import * as Customer from './customer' +import * as Login from './login' +import * as Logout from './logout' +import * as Page from './page' +import * as Product from './product' +import * as Signup from './signup' +import * as Site from './site' +import * as Wishlist from './wishlist' + +export type { + Cart, + Checkout, + Common, + Customer, + Login, + Logout, + Page, + Product, + Signup, + Site, + Wishlist, +} diff --git a/framework/shopify/types/login.ts b/framework/shopify/types/login.ts new file mode 100644 index 0000000000..964ac89e2e --- /dev/null +++ b/framework/shopify/types/login.ts @@ -0,0 +1,8 @@ +import * as Core from '@commerce/types/login' +import type { CustomerAccessTokenCreateInput } from '../schema' + +export * from '@commerce/types/login' + +export type LoginOperation = Core.LoginOperation & { + variables: CustomerAccessTokenCreateInput +} diff --git a/framework/shopify/types/logout.ts b/framework/shopify/types/logout.ts new file mode 100644 index 0000000000..9f0a466afb --- /dev/null +++ b/framework/shopify/types/logout.ts @@ -0,0 +1 @@ +export * from '@commerce/types/logout' diff --git a/framework/shopify/types/page.ts b/framework/shopify/types/page.ts new file mode 100644 index 0000000000..2bccfade2b --- /dev/null +++ b/framework/shopify/types/page.ts @@ -0,0 +1,11 @@ +import * as Core from '@commerce/types/page' +export * from '@commerce/types/page' + +export type Page = Core.Page + +export type PageTypes = { + page: Page +} + +export type GetAllPagesOperation = Core.GetAllPagesOperation +export type GetPageOperation = Core.GetPageOperation diff --git a/framework/shopify/types/product.ts b/framework/shopify/types/product.ts new file mode 100644 index 0000000000..c776d58fa3 --- /dev/null +++ b/framework/shopify/types/product.ts @@ -0,0 +1 @@ +export * from '@commerce/types/product' diff --git a/framework/shopify/types/signup.ts b/framework/shopify/types/signup.ts new file mode 100644 index 0000000000..58543c6f65 --- /dev/null +++ b/framework/shopify/types/signup.ts @@ -0,0 +1 @@ +export * from '@commerce/types/signup' diff --git a/framework/shopify/types/site.ts b/framework/shopify/types/site.ts new file mode 100644 index 0000000000..bfef69cf97 --- /dev/null +++ b/framework/shopify/types/site.ts @@ -0,0 +1 @@ +export * from '@commerce/types/site' diff --git a/framework/shopify/types/wishlist.ts b/framework/shopify/types/wishlist.ts new file mode 100644 index 0000000000..8907fbf821 --- /dev/null +++ b/framework/shopify/types/wishlist.ts @@ -0,0 +1 @@ +export * from '@commerce/types/wishlist' diff --git a/framework/shopify/utils/checkout-to-cart.ts b/framework/shopify/utils/checkout-to-cart.ts index 034ff11d7f..e2531cc789 100644 --- a/framework/shopify/utils/checkout-to-cart.ts +++ b/framework/shopify/utils/checkout-to-cart.ts @@ -1,4 +1,4 @@ -import { Cart } from '../types' +import type { Cart } from '../types/cart' import { CommerceError } from '@commerce/utils/errors' import { @@ -27,12 +27,6 @@ export type CheckoutPayload = | CheckoutQuery const checkoutToCart = (checkoutPayload?: Maybe): Cart => { - if (!checkoutPayload) { - throw new CommerceError({ - message: 'Missing checkout payload from response', - }) - } - const checkout = checkoutPayload?.checkout throwUserErrors(checkoutPayload?.checkoutUserErrors) diff --git a/framework/shopify/utils/get-vendors.ts b/framework/shopify/utils/get-brands.ts similarity index 56% rename from framework/shopify/utils/get-vendors.ts rename to framework/shopify/utils/get-brands.ts index 24843f177b..3065e4ae8d 100644 --- a/framework/shopify/utils/get-vendors.ts +++ b/framework/shopify/utils/get-brands.ts @@ -1,5 +1,8 @@ +import { + GetAllProductVendorsQuery, + GetAllProductVendorsQueryVariables, +} from '../schema' import { ShopifyConfig } from '../api' -import fetchAllProducts from '../api/utils/fetch-all-products' import getAllProductVendors from './queries/get-all-product-vendors-query' export type Brand = { @@ -14,16 +17,17 @@ export type BrandEdge = { export type Brands = BrandEdge[] -const getVendors = async (config: ShopifyConfig): Promise => { - const vendors = await fetchAllProducts({ - config, - query: getAllProductVendors, +const getBrands = async (config: ShopifyConfig): Promise => { + const { data } = await config.fetch< + GetAllProductVendorsQuery, + GetAllProductVendorsQueryVariables + >(getAllProductVendors, { variables: { first: 250, }, }) - let vendorsStrings = vendors.map(({ node: { vendor } }) => vendor) + let vendorsStrings = data.products.edges.map(({ node: { vendor } }) => vendor) return [...new Set(vendorsStrings)].map((v) => { const id = v.replace(/\s+/g, '-').toLowerCase() @@ -37,4 +41,4 @@ const getVendors = async (config: ShopifyConfig): Promise => { }) } -export default getVendors +export default getBrands diff --git a/framework/shopify/utils/get-categories.ts b/framework/shopify/utils/get-categories.ts index 3884fe1937..543ee2fa1e 100644 --- a/framework/shopify/utils/get-categories.ts +++ b/framework/shopify/utils/get-categories.ts @@ -1,23 +1,32 @@ +import type { Category } from '../types/site' import { ShopifyConfig } from '../api' import { CollectionEdge } from '../schema' +import { normalizeCategory } from './normalize' import getSiteCollectionsQuery from './queries/get-all-collections-query' -import { Category } from '@commerce/types' -const getCategories = async (config: ShopifyConfig): Promise => { - const { data } = await config.fetch(getSiteCollectionsQuery, { - variables: { - first: 250, +const getCategories = async ({ + fetch, + locale, +}: ShopifyConfig): Promise => { + const { data } = await fetch( + getSiteCollectionsQuery, + { + variables: { + first: 250, + }, }, - }) + { + ...(locale && { + headers: { + 'Accept-Language': locale, + }, + }), + } + ) return ( - data.collections?.edges?.map( - ({ node: { id, title: name, handle } }: CollectionEdge) => ({ - id, - name, - slug: handle, - path: `/${handle}`, - }) + data.collections?.edges?.map(({ node }: CollectionEdge) => + normalizeCategory(node) ) ?? [] ) } diff --git a/framework/shopify/utils/get-search-variables.ts b/framework/shopify/utils/get-search-variables.ts index c1b40ae5d7..f4863650dd 100644 --- a/framework/shopify/utils/get-search-variables.ts +++ b/framework/shopify/utils/get-search-variables.ts @@ -1,26 +1,30 @@ import getSortVariables from './get-sort-variables' -import type { SearchProductsInput } from '../product/use-search' +import { SearchProductsBody } from '../types/product' export const getSearchVariables = ({ brandId, search, categoryId, sort, -}: SearchProductsInput) => { + locale, +}: SearchProductsBody) => { let query = '' if (search) { - query += `product_type:${search} OR title:${search} OR tag:${search}` + query += `product_type:${search} OR title:${search} OR tag:${search} ` } if (brandId) { - query += `${search ? ' AND ' : ''}vendor:${brandId}` + query += `${search ? 'AND ' : ''}vendor:${brandId}` } return { categoryId, query, ...getSortVariables(sort, !!categoryId), + ...(locale && { + locale, + }), } } diff --git a/framework/shopify/utils/index.ts b/framework/shopify/utils/index.ts index 61e5975d7a..a8454ffca8 100644 --- a/framework/shopify/utils/index.ts +++ b/framework/shopify/utils/index.ts @@ -1,7 +1,7 @@ export { default as handleFetchResponse } from './handle-fetch-response' 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 getBrands } from './get-brands' export { default as getCategories } from './get-categories' export { default as getCheckoutId } from './get-checkout-id' export { default as checkoutCreate } from './checkout-create' diff --git a/framework/shopify/utils/mutations/checkout-create.ts b/framework/shopify/utils/mutations/checkout-create.ts index ffbd555c7c..7bff7e7571 100644 --- a/framework/shopify/utils/mutations/checkout-create.ts +++ b/framework/shopify/utils/mutations/checkout-create.ts @@ -1,17 +1,19 @@ import { checkoutDetailsFragment } from '../queries/get-checkout-query' const checkoutCreateMutation = /* GraphQL */ ` - mutation { - checkoutCreate(input: {}) { + mutation checkoutCreate($input: CheckoutCreateInput = {}) { + checkoutCreate(input: $input) { checkoutUserErrors { code field message } checkout { - ${checkoutDetailsFragment} + ...checkoutDetails } } } + + ${checkoutDetailsFragment} ` export default checkoutCreateMutation diff --git a/framework/shopify/utils/mutations/checkout-line-item-add.ts b/framework/shopify/utils/mutations/checkout-line-item-add.ts index 2282c4e268..02f5b7107a 100644 --- a/framework/shopify/utils/mutations/checkout-line-item-add.ts +++ b/framework/shopify/utils/mutations/checkout-line-item-add.ts @@ -1,7 +1,10 @@ import { checkoutDetailsFragment } from '../queries/get-checkout-query' const checkoutLineItemAddMutation = /* GraphQL */ ` - mutation($checkoutId: ID!, $lineItems: [CheckoutLineItemInput!]!) { + mutation checkoutLineItemAdd( + $checkoutId: ID! + $lineItems: [CheckoutLineItemInput!]! + ) { checkoutLineItemsAdd(checkoutId: $checkoutId, lineItems: $lineItems) { checkoutUserErrors { code @@ -9,9 +12,11 @@ const checkoutLineItemAddMutation = /* GraphQL */ ` message } checkout { - ${checkoutDetailsFragment} + ...checkoutDetails } } } + + ${checkoutDetailsFragment} ` export default checkoutLineItemAddMutation diff --git a/framework/shopify/utils/mutations/checkout-line-item-remove.ts b/framework/shopify/utils/mutations/checkout-line-item-remove.ts index 8dea4ce084..30cb83028b 100644 --- a/framework/shopify/utils/mutations/checkout-line-item-remove.ts +++ b/framework/shopify/utils/mutations/checkout-line-item-remove.ts @@ -1,7 +1,7 @@ import { checkoutDetailsFragment } from '../queries/get-checkout-query' const checkoutLineItemRemoveMutation = /* GraphQL */ ` - mutation($checkoutId: ID!, $lineItemIds: [ID!]!) { + mutation checkoutLineItemRemove($checkoutId: ID!, $lineItemIds: [ID!]!) { checkoutLineItemsRemove( checkoutId: $checkoutId lineItemIds: $lineItemIds @@ -12,9 +12,10 @@ const checkoutLineItemRemoveMutation = /* GraphQL */ ` message } checkout { - ${checkoutDetailsFragment} + ...checkoutDetails } } } + ${checkoutDetailsFragment} ` export default checkoutLineItemRemoveMutation diff --git a/framework/shopify/utils/mutations/checkout-line-item-update.ts b/framework/shopify/utils/mutations/checkout-line-item-update.ts index 76254341e9..fca617fb73 100644 --- a/framework/shopify/utils/mutations/checkout-line-item-update.ts +++ b/framework/shopify/utils/mutations/checkout-line-item-update.ts @@ -1,7 +1,10 @@ import { checkoutDetailsFragment } from '../queries/get-checkout-query' const checkoutLineItemUpdateMutation = /* GraphQL */ ` - mutation($checkoutId: ID!, $lineItems: [CheckoutLineItemUpdateInput!]!) { + mutation checkoutLineItemUpdate( + $checkoutId: ID! + $lineItems: [CheckoutLineItemUpdateInput!]! + ) { checkoutLineItemsUpdate(checkoutId: $checkoutId, lineItems: $lineItems) { checkoutUserErrors { code @@ -9,9 +12,11 @@ const checkoutLineItemUpdateMutation = /* GraphQL */ ` message } checkout { - ${checkoutDetailsFragment} + ...checkoutDetails } } } + + ${checkoutDetailsFragment} ` export default checkoutLineItemUpdateMutation diff --git a/framework/shopify/utils/normalize.ts b/framework/shopify/utils/normalize.ts index 4ebc3a1ae7..e86872ef9b 100644 --- a/framework/shopify/utils/normalize.ts +++ b/framework/shopify/utils/normalize.ts @@ -1,4 +1,7 @@ -import { Product } from '@commerce/types' +import type { Page } from '../types/page' +import type { Product } from '../types/product' +import type { Cart, LineItem } from '../types/cart' +import type { Category } from '../types/site' import { Product as ShopifyProduct, @@ -9,9 +12,11 @@ import { ProductVariantConnection, MoneyV2, ProductOption, + Page as ShopifyPage, + PageEdge, + Collection, } from '../schema' - -import type { Cart, LineItem } from '../types' +import { colorMap } from '@lib/colors' const money = ({ amount, currencyCode }: MoneyV2) => { return { @@ -28,15 +33,18 @@ const normalizeProductOption = ({ return { __typename: 'MultipleChoiceOption', id, - displayName, + displayName: displayName.toLowerCase(), values: values.map((value) => { let output: any = { label: value, } if (displayName.match(/colou?r/gi)) { - output = { - ...output, - hexColors: [value], + const mapedColor = colorMap[value.toLowerCase().replace(/ /g, '')] + if (mapedColor) { + output = { + ...output, + hexColors: [mapedColor], + } } } return output @@ -53,7 +61,16 @@ const normalizeProductImages = ({ edges }: ImageConnection) => const normalizeProductVariants = ({ edges }: ProductVariantConnection) => { return edges?.map( ({ - node: { id, selectedOptions, sku, title, priceV2, compareAtPriceV2 }, + node: { + id, + selectedOptions, + sku, + title, + priceV2, + compareAtPriceV2, + requiresShipping, + availableForSale, + }, }) => { return { id, @@ -61,7 +78,8 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => { sku: sku ?? id, price: +priceV2.amount, listPrice: +compareAtPriceV2?.amount, - requiresShipping: true, + requiresShipping, + availableForSale, options: selectedOptions.map(({ name, value }: SelectedOption) => { const options = normalizeProductOption({ id, @@ -75,22 +93,21 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => { ) } -export function normalizeProduct(productNode: ShopifyProduct): Product { - const { - id, - title: name, - vendor, - images, - variants, - description, - descriptionHtml, - handle, - priceRange, - options, - ...rest - } = productNode - - const product = { +export function normalizeProduct({ + id, + title: name, + vendor, + images, + variants, + description, + descriptionHtml, + handle, + priceRange, + options, + metafields, + ...rest +}: ShopifyProduct): Product { + return { id, name, vendor, @@ -108,13 +125,12 @@ export function normalizeProduct(productNode: ShopifyProduct): Product { ...(descriptionHtml && { descriptionHtml }), ...rest, } - - return product } export function normalizeCart(checkout: Checkout): Cart { return { id: checkout.id, + url: checkout.webUrl, customerId: '', email: '', createdAt: checkout.createdAt, @@ -131,7 +147,7 @@ export function normalizeCart(checkout: Checkout): Cart { } function normalizeLineItem({ - node: { id, title, variant, quantity, ...rest }, + node: { id, title, variant, quantity }, }: CheckoutLineItemEdge): LineItem { return { id, @@ -144,7 +160,7 @@ function normalizeLineItem({ sku: variant?.sku ?? '', name: variant?.title!, image: { - url: variant?.image?.originalSrc ?? '/product-img-placeholder.svg', + url: variant?.image?.originalSrc || '/product-img-placeholder.svg', }, requiresShipping: variant?.requiresShipping ?? false, price: variant?.priceV2?.amount, @@ -152,14 +168,29 @@ function normalizeLineItem({ }, path: String(variant?.product?.handle), discounts: [], - options: - // By default Shopify adds a default variant with default names, we're removing it. https://community.shopify.com/c/Shopify-APIs-SDKs/Adding-new-product-variant-is-automatically-adding-quot-Default/td-p/358095 - variant?.title == 'Default Title' - ? [] - : [ - { - value: variant?.title, - }, - ], + options: variant?.title == 'Default Title' ? [] : variant?.selectedOptions, } } + +export const normalizePage = ( + { title: name, handle, ...page }: ShopifyPage, + locale: string +): Page => ({ + ...page, + url: `/${locale}/${handle}`, + name, +}) + +export const normalizePages = (edges: PageEdge[], locale: string): Page[] => + edges?.map((edge) => normalizePage(edge.node, locale)) + +export const normalizeCategory = ({ + title: name, + handle, + id, +}: Collection): Category => ({ + id, + name, + slug: handle, + path: `/${handle}`, +}) diff --git a/framework/shopify/utils/queries/get-all-products-query.ts b/framework/shopify/utils/queries/get-all-products-query.ts index f48140d315..179cf9812d 100644 --- a/framework/shopify/utils/queries/get-all-products-query.ts +++ b/framework/shopify/utils/queries/get-all-products-query.ts @@ -1,46 +1,38 @@ -export const productConnection = ` -pageInfo { - hasNextPage - hasPreviousPage -} -edges { - node { - id - title - vendor - handle - priceRange { - minVariantPrice { - amount - currencyCode - } +export const productConnectionFragment = /* GraphQL */ ` + fragment productConnection on ProductConnection { + pageInfo { + hasNextPage + hasPreviousPage } - images(first: 1) { - pageInfo { - hasNextPage - hasPreviousPage - } - edges { - node { - originalSrc - altText - width - height + edges { + node { + id + title + vendor + handle + priceRange { + minVariantPrice { + amount + currencyCode + } + } + images(first: 1) { + pageInfo { + hasNextPage + hasPreviousPage + } + edges { + node { + originalSrc + altText + width + height + } + } } } } } -}` - -export const productsFragment = ` -products( - first: $first - sortKey: $sortKey - reverse: $reverse - query: $query -) { - ${productConnection} -} ` const getAllProductsQuery = /* GraphQL */ ` @@ -50,7 +42,16 @@ const getAllProductsQuery = /* GraphQL */ ` $sortKey: ProductSortKeys = RELEVANCE $reverse: Boolean = false ) { - ${productsFragment} + products( + first: $first + sortKey: $sortKey + reverse: $reverse + query: $query + ) { + ...productConnection + } } + + ${productConnectionFragment} ` export default getAllProductsQuery diff --git a/framework/shopify/utils/queries/get-checkout-query.ts b/framework/shopify/utils/queries/get-checkout-query.ts index d8758e3215..9969e67c07 100644 --- a/framework/shopify/utils/queries/get-checkout-query.ts +++ b/framework/shopify/utils/queries/get-checkout-query.ts @@ -1,65 +1,70 @@ -export const checkoutDetailsFragment = ` - id - webUrl - subtotalPriceV2{ - amount - currencyCode - } - totalTaxV2 { - amount - currencyCode - } - totalPriceV2 { - amount - currencyCode - } - completedAt - createdAt - taxesIncluded - lineItems(first: 250) { - pageInfo { - hasNextPage - hasPreviousPage +export const checkoutDetailsFragment = /* GraphQL */ ` + fragment checkoutDetails on Checkout { + id + webUrl + subtotalPriceV2 { + amount + currencyCode + } + totalTaxV2 { + amount + currencyCode + } + totalPriceV2 { + amount + currencyCode } - edges { - node { - id - title - variant { + completedAt + createdAt + taxesIncluded + lineItems(first: 250) { + pageInfo { + hasNextPage + hasPreviousPage + } + edges { + node { id - sku title - image { - originalSrc - altText - width - height - } - priceV2{ - amount - currencyCode - } - compareAtPriceV2{ - amount - currencyCode - } - product { - handle + variant { + id + sku + title + selectedOptions { + name + value + } + image { + originalSrc + altText + width + height + } + priceV2 { + amount + currencyCode + } + compareAtPriceV2 { + amount + currencyCode + } + product { + handle + } } + quantity } - quantity } } } ` const getCheckoutQuery = /* GraphQL */ ` - query($checkoutId: ID!) { + query getCheckout($checkoutId: ID!) { node(id: $checkoutId) { - ... on Checkout { - ${checkoutDetailsFragment} - } + ...checkoutDetails } } + ${checkoutDetailsFragment} ` export default getCheckoutQuery diff --git a/framework/shopify/utils/queries/get-collection-products-query.ts b/framework/shopify/utils/queries/get-collection-products-query.ts index 04766caa42..b773a7e658 100644 --- a/framework/shopify/utils/queries/get-collection-products-query.ts +++ b/framework/shopify/utils/queries/get-collection-products-query.ts @@ -1,4 +1,4 @@ -import { productConnection } from './get-all-products-query' +import { productConnectionFragment } from './get-all-products-query' const getCollectionProductsQuery = /* GraphQL */ ` query getProductsFromCollection( @@ -10,15 +10,12 @@ const getCollectionProductsQuery = /* GraphQL */ ` node(id: $categoryId) { id ... on Collection { - products( - first: $first - sortKey: $sortKey - reverse: $reverse - ) { - ${productConnection} + products(first: $first, sortKey: $sortKey, reverse: $reverse) { + ...productConnection } } } } + ${productConnectionFragment} ` export default getCollectionProductsQuery diff --git a/framework/shopify/utils/queries/get-page-query.ts b/framework/shopify/utils/queries/get-page-query.ts index 2ca79abd42..7939f0278b 100644 --- a/framework/shopify/utils/queries/get-page-query.ts +++ b/framework/shopify/utils/queries/get-page-query.ts @@ -1,5 +1,5 @@ export const getPageQuery = /* GraphQL */ ` - query($id: ID!) { + query getPage($id: ID!) { node(id: $id) { id ... on Page { diff --git a/framework/shopify/utils/queries/get-product-query.ts b/framework/shopify/utils/queries/get-product-query.ts index 5c109901b8..b2998a40a6 100644 --- a/framework/shopify/utils/queries/get-product-query.ts +++ b/framework/shopify/utils/queries/get-product-query.ts @@ -3,6 +3,7 @@ const getProductQuery = /* GraphQL */ ` productByHandle(handle: $slug) { id handle + availableForSale title productType vendor @@ -33,6 +34,8 @@ const getProductQuery = /* GraphQL */ ` id title sku + availableForSale + requiresShipping selectedOptions { name value diff --git a/framework/shopify/utils/queries/get-site-info-query.ts b/framework/shopify/utils/queries/get-site-info-query.ts new file mode 100644 index 0000000000..74215572ab --- /dev/null +++ b/framework/shopify/utils/queries/get-site-info-query.ts @@ -0,0 +1,8 @@ +const getSiteInfoQuery = /* GraphQL */ ` + query getSiteInfo { + shop { + name + } + } +` +export default getSiteInfoQuery diff --git a/framework/shopify/utils/queries/index.ts b/framework/shopify/utils/queries/index.ts index e19be9c8ca..953113491d 100644 --- a/framework/shopify/utils/queries/index.ts +++ b/framework/shopify/utils/queries/index.ts @@ -8,3 +8,4 @@ export { default as getCheckoutQuery } from './get-checkout-query' export { default as getAllPagesQuery } from './get-all-pages-query' export { default as getPageQuery } from './get-page-query' export { default as getCustomerQuery } from './get-customer-query' +export { default as getSiteInfoQuery } from './get-site-info-query' diff --git a/lib/api/commerce.ts b/lib/api/commerce.ts new file mode 100644 index 0000000000..4991370043 --- /dev/null +++ b/lib/api/commerce.ts @@ -0,0 +1,3 @@ +import { getCommerceApi } from '@framework/api' + +export default getCommerceApi() diff --git a/lib/colors.ts b/lib/colors.ts index 139cda23d3..43947c3222 100644 --- a/lib/colors.ts +++ b/lib/colors.ts @@ -42,7 +42,7 @@ function hexToRgb(hex: string = '') { return [r, g, b] } -const colorMap: Record = { +export const colorMap: Record = { aliceblue: '#F0F8FF', antiquewhite: '#FAEBD7', aqua: '#00FFFF', @@ -56,6 +56,8 @@ const colorMap: Record = { blueviolet: '#8A2BE2', brown: '#A52A2A', burlywood: '#DEB887', + burgandy: '#800020', + burgundy: '#800020', cadetblue: '#5F9EA0', chartreuse: '#7FFF00', chocolate: '#D2691E', @@ -177,6 +179,8 @@ const colorMap: Record = { slateblue: '#6A5ACD', slategray: '#708090', slategrey: '#708090', + spacegrey: '#65737e', + spacegray: '#65737e', snow: '#FFFAFA', springgreen: '#00FF7F', steelblue: '#4682B4', diff --git a/next.config.js b/next.config.js index b1e48cf1e1..607d4eba8c 100644 --- a/next.config.js +++ b/next.config.js @@ -20,13 +20,13 @@ module.exports = withCommerceConfig({ return [ (isBC || isShopify || isSwell || isVendure) && { source: '/checkout', - destination: '/api/bigcommerce/checkout', + destination: '/api/checkout', }, // The logout is also an action so this route is not required, but it's also another way // you can allow a logout! isBC && { source: '/logout', - destination: '/api/bigcommerce/customers/logout?redirect_to=/', + destination: '/api/logout?redirect_to=/', }, // For Vendure, rewrite the local api url to the remote (external) api url. This is required // to make the session cookies work. diff --git a/package.json b/package.json index 5a9d40b790..85daa3158f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "prettier-fix": "prettier --write .", "find:unused": "next-unused", "generate": "graphql-codegen", + "generate:shopify": "DOTENV_CONFIG_PATH=./.env.local graphql-codegen -r dotenv/config --config framework/shopify/codegen.json", "generate:vendure": "graphql-codegen --config framework/vendure/codegen.json", "generate:definitions": "node framework/bigcommerce/scripts/generate-definitions.js" }, diff --git a/pages/[...pages].tsx b/pages/[...pages].tsx index eae7dca393..c63963ef63 100644 --- a/pages/[...pages].tsx +++ b/pages/[...pages].tsx @@ -3,29 +3,31 @@ import type { GetStaticPropsContext, InferGetStaticPropsType, } from 'next' +import commerce from '@lib/api/commerce' import { Text } from '@components/ui' import { Layout } from '@components/common' import getSlug from '@lib/get-slug' import { missingLocaleInPages } from '@lib/usage-warns' -import { getConfig } from '@framework/api' -import getPage from '@framework/common/get-page' -import getAllPages from '@framework/common/get-all-pages' -import getSiteInfo from '@framework/common/get-site-info' export async function getStaticProps({ preview, params, locale, + locales, }: GetStaticPropsContext<{ pages: string[] }>) { - const config = getConfig({ locale }) - const { pages } = await getAllPages({ preview, config }) - const { categories } = await getSiteInfo({ config, preview }) + const config = { locale, locales } + const { pages } = await commerce.getAllPages({ config, preview }) + const { categories } = await commerce.getSiteInfo({ config, preview }) const path = params?.pages.join('/') const slug = locale ? `${locale}/${path}` : path const pageItem = pages.find((p) => (p.url ? getSlug(p.url) === slug : false)) const data = pageItem && - (await getPage({ variables: { id: pageItem.id! }, config, preview })) + (await commerce.getPage({ + variables: { id: pageItem.id! }, + config, + preview, + })) const page = data?.page if (!page) { @@ -40,7 +42,8 @@ export async function getStaticProps({ } export async function getStaticPaths({ locales }: GetStaticPathsContext) { - const { pages } = await getAllPages() + const config = { locales } + const { pages } = await commerce.getAllPages({ config }) const [invalidPaths, log] = missingLocaleInPages() const paths = pages .map((page) => page.url) diff --git a/pages/api/bigcommerce/cart.ts b/pages/api/bigcommerce/cart.ts deleted file mode 100644 index 68ffc3b159..0000000000 --- a/pages/api/bigcommerce/cart.ts +++ /dev/null @@ -1,3 +0,0 @@ -import cartApi from '@framework/api/cart' - -export default cartApi() diff --git a/pages/api/bigcommerce/catalog/products.ts b/pages/api/bigcommerce/catalog/products.ts deleted file mode 100644 index ac342c82ad..0000000000 --- a/pages/api/bigcommerce/catalog/products.ts +++ /dev/null @@ -1,3 +0,0 @@ -import catalogProductsApi from '@framework/api/catalog/products' - -export default catalogProductsApi() diff --git a/pages/api/bigcommerce/checkout.ts b/pages/api/bigcommerce/checkout.ts deleted file mode 100644 index bd754deaba..0000000000 --- a/pages/api/bigcommerce/checkout.ts +++ /dev/null @@ -1,3 +0,0 @@ -import checkoutApi from '@framework/api/checkout' - -export default checkoutApi() diff --git a/pages/api/bigcommerce/customers/index.ts b/pages/api/bigcommerce/customers/index.ts deleted file mode 100644 index 7b55d3aa8f..0000000000 --- a/pages/api/bigcommerce/customers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import customersApi from '@framework/api/customers' - -export default customersApi() diff --git a/pages/api/bigcommerce/customers/login.ts b/pages/api/bigcommerce/customers/login.ts deleted file mode 100644 index aac529751c..0000000000 --- a/pages/api/bigcommerce/customers/login.ts +++ /dev/null @@ -1,3 +0,0 @@ -import loginApi from '@framework/api/customers/login' - -export default loginApi() diff --git a/pages/api/bigcommerce/customers/logout.ts b/pages/api/bigcommerce/customers/logout.ts deleted file mode 100644 index e872ff95d2..0000000000 --- a/pages/api/bigcommerce/customers/logout.ts +++ /dev/null @@ -1,3 +0,0 @@ -import logoutApi from '@framework/api/customers/logout' - -export default logoutApi() diff --git a/pages/api/bigcommerce/customers/signup.ts b/pages/api/bigcommerce/customers/signup.ts deleted file mode 100644 index 59f2f840ad..0000000000 --- a/pages/api/bigcommerce/customers/signup.ts +++ /dev/null @@ -1,3 +0,0 @@ -import signupApi from '@framework/api/customers/signup' - -export default signupApi() diff --git a/pages/api/bigcommerce/wishlist.ts b/pages/api/bigcommerce/wishlist.ts deleted file mode 100644 index 0d6a895a57..0000000000 --- a/pages/api/bigcommerce/wishlist.ts +++ /dev/null @@ -1,3 +0,0 @@ -import wishlistApi from '@framework/api/wishlist' - -export default wishlistApi() diff --git a/pages/api/cart.ts b/pages/api/cart.ts new file mode 100644 index 0000000000..6428911079 --- /dev/null +++ b/pages/api/cart.ts @@ -0,0 +1,4 @@ +import cartApi from '@framework/api/endpoints/cart' +import commerce from '@lib/api/commerce' + +export default cartApi(commerce) diff --git a/pages/api/catalog/products.ts b/pages/api/catalog/products.ts new file mode 100644 index 0000000000..631bfd516f --- /dev/null +++ b/pages/api/catalog/products.ts @@ -0,0 +1,4 @@ +import productsApi from '@framework/api/endpoints/catalog/products' +import commerce from '@lib/api/commerce' + +export default productsApi(commerce) diff --git a/pages/api/checkout.ts b/pages/api/checkout.ts new file mode 100644 index 0000000000..7bf0fd9aab --- /dev/null +++ b/pages/api/checkout.ts @@ -0,0 +1,4 @@ +import checkoutApi from '@framework/api/endpoints/checkout' +import commerce from '@lib/api/commerce' + +export default checkoutApi(commerce) diff --git a/pages/api/customer.ts b/pages/api/customer.ts new file mode 100644 index 0000000000..0c86e76e59 --- /dev/null +++ b/pages/api/customer.ts @@ -0,0 +1,4 @@ +import customerApi from '@framework/api/endpoints/customer' +import commerce from '@lib/api/commerce' + +export default customerApi(commerce) diff --git a/pages/api/login.ts b/pages/api/login.ts new file mode 100644 index 0000000000..9d0b6ae570 --- /dev/null +++ b/pages/api/login.ts @@ -0,0 +1,4 @@ +import loginApi from '@framework/api/endpoints/login' +import commerce from '@lib/api/commerce' + +export default loginApi(commerce) diff --git a/pages/api/logout.ts b/pages/api/logout.ts new file mode 100644 index 0000000000..0cf0fc4d2b --- /dev/null +++ b/pages/api/logout.ts @@ -0,0 +1,4 @@ +import logoutApi from '@framework/api/endpoints/logout' +import commerce from '@lib/api/commerce' + +export default logoutApi(commerce) diff --git a/pages/api/signup.ts b/pages/api/signup.ts new file mode 100644 index 0000000000..e19d67ee8e --- /dev/null +++ b/pages/api/signup.ts @@ -0,0 +1,4 @@ +import singupApi from '@framework/api/endpoints/signup' +import commerce from '@lib/api/commerce' + +export default singupApi(commerce) diff --git a/pages/api/wishlist.ts b/pages/api/wishlist.ts new file mode 100644 index 0000000000..3b9681209f --- /dev/null +++ b/pages/api/wishlist.ts @@ -0,0 +1,4 @@ +import wishlistApi from '@framework/api/endpoints/wishlist' +import commerce from '@lib/api/commerce' + +export default wishlistApi(commerce) diff --git a/pages/cart.tsx b/pages/cart.tsx index c8deb1a020..dff4a201ee 100644 --- a/pages/cart.tsx +++ b/pages/cart.tsx @@ -1,21 +1,20 @@ import type { GetStaticPropsContext } from 'next' -import { getConfig } from '@framework/api' -import getAllPages from '@framework/common/get-all-pages' import useCart from '@framework/cart/use-cart' import usePrice from '@framework/product/use-price' +import commerce from '@lib/api/commerce' import { Layout } from '@components/common' import { Button, Text } from '@components/ui' import { Bag, Cross, Check, MapPin, CreditCard } from '@components/icons' import { CartItem } from '@components/cart' -import getSiteInfo from '@framework/common/get-site-info' export async function getStaticProps({ preview, locale, + locales, }: GetStaticPropsContext) { - const config = getConfig({ locale }) - const { categories } = await getSiteInfo({ config, preview }) - const { pages } = await getAllPages({ config, preview }) + const config = { locale, locales } + const { pages } = await commerce.getAllPages({ config, preview }) + const { categories } = await commerce.getSiteInfo({ config, preview }) return { props: { pages, categories }, } diff --git a/pages/index.tsx b/pages/index.tsx index 3a466c6066..02142f3b08 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,32 +1,29 @@ +import commerce from '@lib/api/commerce' import { Layout } from '@components/common' import { ProductCard } from '@components/product' import { Grid, Marquee, Hero } from '@components/ui' // import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid' import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' -import { getConfig } from '@framework/api' -import getAllProducts from '@framework/product/get-all-products' -import getSiteInfo from '@framework/common/get-site-info' -import getAllPages from '@framework/common/get-all-pages' export async function getStaticProps({ preview, locale, + locales, }: GetStaticPropsContext) { - const config = getConfig({ locale }) - const { pages } = await getAllPages({ config, preview }) - const { categories } = await getSiteInfo({ config, preview }) - - const { products } = await getAllProducts({ + const config = { locale, locales } + const { products } = await commerce.getAllProducts({ variables: { first: 12 }, config, preview, }) + const { categories, brands } = await commerce.getSiteInfo({ config, preview }) + const { pages } = await commerce.getAllPages({ config, preview }) return { props: { products, categories, - brands: [], + brands, pages, }, revalidate: 14400, diff --git a/pages/orders.tsx b/pages/orders.tsx index ee93f3f7fa..ddea62d056 100644 --- a/pages/orders.tsx +++ b/pages/orders.tsx @@ -1,18 +1,17 @@ import type { GetStaticPropsContext } from 'next' +import commerce from '@lib/api/commerce' import { Bag } from '@components/icons' -import { getConfig } from '@framework/api' import { Layout } from '@components/common' import { Container, Text } from '@components/ui' -import getAllPages from '@framework/common/get-all-pages' -import getSiteInfo from '@framework/common/get-site-info' export async function getStaticProps({ preview, locale, + locales, }: GetStaticPropsContext) { - const config = getConfig({ locale }) - const { categories } = await getSiteInfo({ config, preview }) - const { pages } = await getAllPages({ config, preview }) + const config = { locale, locales } + const { pages } = await commerce.getAllPages({ config, preview }) + const { categories } = await commerce.getSiteInfo({ config, preview }) return { props: { pages, categories }, diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index ac55dc4be7..3b41216a88 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -4,28 +4,24 @@ import type { InferGetStaticPropsType, } from 'next' import { useRouter } from 'next/router' +import commerce from '@lib/api/commerce' import { Layout } from '@components/common' import { ProductView } from '@components/product' -import { getConfig } from '@framework/api' -import getProduct from '@framework/product/get-product' -import getAllPages from '@framework/common/get-all-pages' -import getAllProductPaths from '@framework/product/get-all-product-paths' -import getSiteInfo from '@framework/common/get-site-info' - export async function getStaticProps({ params, locale, + locales, preview, }: GetStaticPropsContext<{ slug: string }>) { - const config = getConfig({ locale }) - const { pages } = await getAllPages({ config, preview }) - const { product } = await getProduct({ + const config = { locale, locales } + const { pages } = await commerce.getAllPages({ config, preview }) + const { product } = await commerce.getProduct({ variables: { slug: params!.slug }, config, preview, }) - const { categories } = await getSiteInfo({ config, preview }) + const { categories } = await commerce.getSiteInfo({ config, preview }) if (!product) { throw new Error(`Product with slug '${params!.slug}' not found`) @@ -42,18 +38,18 @@ export async function getStaticProps({ } export async function getStaticPaths({ locales }: GetStaticPathsContext) { - const { products } = await getAllProductPaths() + const { products } = await commerce.getAllProductPaths() return { paths: locales ? locales.reduce((arr, locale) => { // Add a product path for every locale products.forEach((product) => { - arr.push(`/${locale}/product${product.node.path}`) + arr.push(`/${locale}/product${product.path}`) }) return arr }, []) - : products.map((product) => `/product${product.node.path}`), + : products.map((product) => `/product${product.path}`), fallback: 'blocking', } } diff --git a/pages/profile.tsx b/pages/profile.tsx index b1e8e6628d..1f575c8f29 100644 --- a/pages/profile.tsx +++ b/pages/profile.tsx @@ -1,18 +1,18 @@ import type { GetStaticPropsContext } from 'next' -import { getConfig } from '@framework/api' -import getAllPages from '@framework/common/get-all-pages' import useCustomer from '@framework/customer/use-customer' +import commerce from '@lib/api/commerce' import { Layout } from '@components/common' import { Container, Text } from '@components/ui' -import getSiteInfo from '@framework/common/get-site-info' export async function getStaticProps({ preview, locale, + locales, }: GetStaticPropsContext) { - const config = getConfig({ locale }) - const { categories } = await getSiteInfo({ config, preview }) - const { pages } = await getAllPages({ config, preview }) + const config = { locale, locales } + const { pages } = await commerce.getAllPages({ config, preview }) + const { categories } = await commerce.getSiteInfo({ config, preview }) + return { props: { pages, categories }, } diff --git a/pages/search.tsx b/pages/search.tsx index 9a0330127e..55c57c5934 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -6,15 +6,22 @@ import { useRouter } from 'next/router' import { Layout } from '@components/common' import { ProductCard } from '@components/product' +import type { Product } from '@commerce/types/product' import { Container, Grid, Skeleton } from '@components/ui' -import { getConfig } from '@framework/api' import useSearch from '@framework/product/use-search' -import getAllPages from '@framework/common/get-all-pages' -import getSiteInfo from '@framework/common/get-site-info' +import commerce from '@lib/api/commerce' +import rangeMap from '@lib/range-map' +import { + filterQuery, + getCategoryPath, + getDesignerPath, + useSearchMeta, +} from '@lib/search' + +// TODO(bc) Remove this. This should come from the API import getSlug from '@lib/get-slug' -import rangeMap from '@lib/range-map' const SORT = Object.entries({ 'latest-desc': 'Latest arrivals', @@ -23,21 +30,14 @@ const SORT = Object.entries({ 'price-desc': 'Price: High to low', }) -import { - filterQuery, - getCategoryPath, - getDesignerPath, - useSearchMeta, -} from '@lib/search' -import { Product } from '@commerce/types' - export async function getStaticProps({ preview, locale, + locales, }: GetStaticPropsContext) { - const config = getConfig({ locale }) - const { pages } = await getAllPages({ config, preview }) - const { categories, brands } = await getSiteInfo({ config, preview }) + const config = { locale, locales } + const { pages } = await commerce.getAllPages({ config, preview }) + const { categories, brands } = await commerce.getSiteInfo({ config, preview }) return { props: { pages, @@ -55,7 +55,7 @@ export default function Search({ const [toggleFilter, setToggleFilter] = useState(false) const router = useRouter() - const { asPath } = router + const { asPath, locale } = router const { q, sort } = router.query // `q` can be included but because categories and designers can't be searched // in the same way of products, it's better to ignore the search input if one @@ -63,9 +63,7 @@ export default function Search({ const query = filterQuery({ sort }) const { pathname, category, brand } = useSearchMeta(asPath) - const activeCategory = categories.find( - (cat) => getSlug(cat.path) === category - ) + const activeCategory = categories.find((cat) => cat.slug === category) const activeBrand = brands.find( (b) => getSlug(b.node.path) === `brands/${brand}` )?.node @@ -75,6 +73,7 @@ export default function Search({ categoryId: activeCategory?.id, brandId: (activeBrand as any)?.entityId, sort: typeof sort === 'string' ? sort : '', + locale, }) const handleClick = (event: any, filter: string) => { diff --git a/pages/wishlist.tsx b/pages/wishlist.tsx index 0e6732ae25..2728e29841 100644 --- a/pages/wishlist.tsx +++ b/pages/wishlist.tsx @@ -1,17 +1,16 @@ import type { GetStaticPropsContext } from 'next' +import commerce from '@lib/api/commerce' import { Heart } from '@components/icons' -import { getConfig } from '@framework/api' import { Layout } from '@components/common' import { Text, Container } from '@components/ui' import { useCustomer } from '@framework/customer' import { WishlistCard } from '@components/wishlist' import useWishlist from '@framework/wishlist/use-wishlist' -import getAllPages from '@framework/common/get-all-pages' -import getSiteInfo from '@framework/common/get-site-info' export async function getStaticProps({ preview, locale, + locales, }: GetStaticPropsContext) { // Disabling page if Feature is not available if (!process.env.COMMERCE_WISHLIST_ENABLED) { @@ -20,9 +19,10 @@ export async function getStaticProps({ } } - const config = getConfig({ locale }) - const { categories } = await getSiteInfo({ config, preview }) - const { pages } = await getAllPages({ config, preview }) + const config = { locale, locales } + const { pages } = await commerce.getAllPages({ config, preview }) + const { categories } = await commerce.getSiteInfo({ config, preview }) + return { props: { pages, diff --git a/tsconfig.json b/tsconfig.json index 9e712fb18c..96e4359e58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,10 +22,10 @@ "@components/*": ["components/*"], "@commerce": ["framework/commerce"], "@commerce/*": ["framework/commerce/*"], - "@framework": ["framework/bigcommerce"], - "@framework/*": ["framework/bigcommerce/*"] + "@framework": ["framework/shopify"], + "@framework/*": ["framework/shopify/*"] } }, "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "framework/swell", "framework/vendure"] }