diff --git a/codegen.json b/codegen.json index 1f14e88ac..56167f954 100644 --- a/codegen.json +++ b/codegen.json @@ -1,23 +1,14 @@ { "schema": { - "https://buybutton.store/graphql": { - "headers": { - "Authorization": "Bearer xzy" - } + "http://localhost:3000/graphql": { + "headers": {} } }, - "documents": [ - { - "./framework/bigcommerce/api/**/*.ts": { - "noRequire": true - } - } - ], "generates": { - "./framework/bigcommerce/schema.d.ts": { + "./framework/reactioncommerce/schema.d.ts": { "plugins": ["typescript", "typescript-operations"] }, - "./framework/bigcommerce/schema.graphql": { + "./framework/reactioncommerce/schema.graphql": { "plugins": ["schema-ast"] } }, diff --git a/commerce.config.json b/commerce.config.json index bef7db222..ce78b1b10 100644 --- a/commerce.config.json +++ b/commerce.config.json @@ -1,7 +1,7 @@ { - "provider": "bigcommerce", + "provider": "reactioncommerce", "features": { - "wishlist": true, - "customCheckout": false + "wishlist": false, + "customCheckout": true } } diff --git a/components/cart/CartItem/CartItem.tsx b/components/cart/CartItem/CartItem.tsx index cb7f8600e..9343d1ecf 100644 --- a/components/cart/CartItem/CartItem.tsx +++ b/components/cart/CartItem/CartItem.tsx @@ -39,6 +39,7 @@ const CartItem = ({ const [removing, setRemoving] = useState(false) const updateQuantity = async (val: number) => { + console.log('updateQuantity', val) await updateItem({ quantity: val }) } diff --git a/components/product/ProductCard/ProductCard.tsx b/components/product/ProductCard/ProductCard.tsx index ade53380c..45a19d2dd 100644 --- a/components/product/ProductCard/ProductCard.tsx +++ b/components/product/ProductCard/ProductCard.tsx @@ -34,7 +34,7 @@ const ProductCard: FC = ({ {product?.images && ( {product.name = ({ {product.name = ({ product }) => { const { openSidebar } = useUI() const [loading, setLoading] = useState(false) const [choices, setChoices] = useState({ - size: null, color: null, }) @@ -40,9 +39,17 @@ const ProductView: FC = ({ product }) => { const addToCart = async () => { setLoading(true) try { + const selectedVariant = variant ? variant : product.variants[0] + + console.log('selected variant', selectedVariant) + await addItem({ productId: String(product.id), - variantId: String(variant ? variant.id : product.variants[0].id), + variantId: String(selectedVariant.id), + pricing: { + amount: selectedVariant.price, + currencyCode: product.price.currencyCode, + }, }) openSidebar() setLoading(false) diff --git a/framework/commerce/api/index.ts b/framework/commerce/api/index.ts index 77b2eeb7e..56ac58dad 100644 --- a/framework/commerce/api/index.ts +++ b/framework/commerce/api/index.ts @@ -5,6 +5,7 @@ export interface CommerceAPIConfig { commerceUrl: string apiToken: string cartCookie: string + cartIdCookie: string cartCookieMaxAge: number customerCookie: string fetch( diff --git a/framework/commerce/with-config.js b/framework/commerce/with-config.js index 1eb1acc19..bb34d5ac8 100644 --- a/framework/commerce/with-config.js +++ b/framework/commerce/with-config.js @@ -4,7 +4,7 @@ const merge = require('deepmerge') -const PROVIDERS = ['bigcommerce', 'shopify'] +const PROVIDERS = ['bigcommerce', 'shopify', 'reactioncommerce'] function getProviderName() { // TODO: OSOT. diff --git a/framework/reactioncommerce/.env.template b/framework/reactioncommerce/.env.template new file mode 100644 index 000000000..24521c2a1 --- /dev/null +++ b/framework/reactioncommerce/.env.template @@ -0,0 +1,2 @@ +SHOPIFY_STORE_DOMAIN= +SHOPIFY_STOREFRONT_ACCESS_TOKEN= diff --git a/framework/reactioncommerce/README.md b/framework/reactioncommerce/README.md new file mode 100644 index 000000000..fc6a70ce3 --- /dev/null +++ b/framework/reactioncommerce/README.md @@ -0,0 +1,260 @@ +## Table of Contents + +- [Getting Started](#getting-started) + - [Modifications](#modifications) + - [Adding item to Cart](#adding-item-to-cart) + - [Proceed to Checkout](#proceed-to-checkout) +- [General Usage](#general-usage) + - [CommerceProvider](#commerceprovider) + - [useCommerce](#usecommerce) +- [Hooks](#hooks) + - [usePrice](#useprice) + - [useAddItem](#useadditem) + - [useRemoveItem](#useremoveitem) + - [useUpdateItem](#useupdateitem) +- [APIs](#apis) + - [getProduct](#getproduct) + - [getAllProducts](#getallproducts) + - [getAllCollections](#getallcollections) + - [getAllPages](#getallpages) + +# Shopify Storefront Data Hooks + +Collection of hooks and data fetching functions to integrate Shopify in a React application. Designed to work with [Next.js Commerce](https://demo.vercel.store/). + +## Getting Started + +1. Install dependencies: + +``` +yarn install shopify-buy +yarn install -D @types/shopify-buy +``` + +3. Environment variables need to be set: + +``` +SHOPIFY_STORE_DOMAIN= +SHOPIFY_STOREFRONT_ACCESS_TOKEN= +NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN= +NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN= +``` + +4. Point the framework to `shopify` by updating `tsconfig.json`: + +``` +"@framework/*": ["framework/shopify/*"], +"@framework": ["framework/shopify"] +``` + +### Modifications + +These modifications are temporarily until contributions are made to remove them. + +#### Adding item to Cart + +```js +// components/product/ProductView/ProductView.tsx +const ProductView: FC = ({ product }) => { + const addToCart = async () => { + setLoading(true) + try { + await addItem({ + productId: product.id, + variantId: variant ? variant.id : product.variants[0].id, + }) + openSidebar() + setLoading(false) + } catch (err) { + setLoading(false) + } + } +} +``` + +#### Proceed to Checkout + +```js +// components/cart/CartSidebarView/CartSidebarView.tsx +import { useCommerce } from '@framework' + +const CartSidebarView: FC = () => { + const { checkout } = useCommerce() + return ( + + ) +} +``` + +## General Usage + +### CommerceProvider + +Provider component that creates the commerce context for children. + +```js +import { CommerceProvider } from '@framework' + +const App = ({ children }) => { + return {children} +} + +export default App +``` + +### useCommerce + +Returns the configs that are defined in the nearest `CommerceProvider`. Also provides access to Shopify's `checkout` and `shop`. + +```js +import { useCommerce } from 'nextjs-commerce-shopify' + +const { checkout, shop } = useCommerce() +``` + +- `checkout`: The information required to checkout items and pay ([Documentation](https://shopify.dev/docs/storefront-api/reference/checkouts/checkout)). +- `shop`: Represents a collection of the general settings and information about the shop ([Documentation](https://shopify.dev/docs/storefront-api/reference/online-store/shop/index)). + +## Hooks + +### usePrice + +Display the product variant price according to currency and locale. + +```js +import usePrice from '@framework/product/use-price' + +const { price } = usePrice({ + amount, +}) +``` + +Takes in either `amount` or `variant`: + +- `amount`: A price value for a particular item if the amount is known. +- `variant`: A shopify product variant. Price will be extracted from the variant. + +### useAddItem + +```js +import { useAddItem } from '@framework/cart' + +const AddToCartButton = ({ variantId, quantity }) => { + const addItem = useAddItem() + + const addToCart = async () => { + await addItem({ + variantId, + }) + } + + return +} +``` + +### useRemoveItem + +```js +import { useRemoveItem } from '@framework/cart' + +const RemoveButton = ({ item }) => { + const removeItem = useRemoveItem() + + const handleRemove = async () => { + await removeItem({ id: item.id }) + } + + return +} +``` + +### useUpdateItem + +```js +import { useUpdateItem } from '@framework/cart' + +const CartItem = ({ item }) => { + const [quantity, setQuantity] = useState(item.quantity) + const updateItem = useUpdateItem(item) + + const updateQuantity = async (e) => { + const val = e.target.value + await updateItem({ quantity: val }) + } + + return ( + + ) +} +``` + +## APIs + +Collections of APIs to fetch data from a Shopify store. + +The data is fetched using the [Shopify JavaScript Buy SDK](https://github.com/Shopify/js-buy-sdk#readme). Read the [Shopify Storefront API reference](https://shopify.dev/docs/storefront-api/reference) for more information. + +### getProduct + +Get a single product by its `handle`. + +```js +import getProduct from '@framework/product/get-product' +import { getConfig } from '@framework/api' + +const config = getConfig() + +const product = await getProduct({ + variables: { slug }, + config, +}) +``` + +### getAllProducts + +```js +import getAllProducts from '@framework/product/get-all-products' +import { getConfig } from '@framework/api' + +const config = getConfig() + +const { products } = await getAllProducts({ + variables: { first: 12 }, + config, +}) +``` + +### getAllCollections + +```js +import getAllCollections from '@framework/product/get-all-collections' +import { getConfig } from '@framework/api' + +const config = getConfig() + +const collections = await getAllCollections({ + config, +}) +``` + +### getAllPages + +```js +import getAllPages from '@framework/common/get-all-pages' +import { getConfig } from '@framework/api' + +const config = getConfig() + +const pages = await getAllPages({ + variables: { first: 12 }, + config, +}) +``` diff --git a/framework/reactioncommerce/api/cart/handlers/add-item.ts b/framework/reactioncommerce/api/cart/handlers/add-item.ts new file mode 100644 index 000000000..c8ccb36af --- /dev/null +++ b/framework/reactioncommerce/api/cart/handlers/add-item.ts @@ -0,0 +1,93 @@ +import type { CartHandlers } from '..' +import { + addCartItemsMutation, + checkoutCreateMutation, +} from '@framework/utils/mutations' +import getCartCookie from '@framework/api/utils/get-cart-cookie' +import { + REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + REACTION_CART_ID_COOKIE, +} from '@framework/const' + +const addItem: CartHandlers['addItem'] = async ({ + req: { + cookies: { + [REACTION_ANONYMOUS_CART_TOKEN_COOKIE]: anonymousCartToken, + [REACTION_CART_ID_COOKIE]: cartId, + }, + }, + res, + body: { item }, + config, +}) => { + console.log('add-item API', item.productId) + console.log('variantId', item.variantId) + + if (!item) { + return res.status(400).json({ + data: null, + errors: [{ message: 'Missing item' }], + }) + } + if (!item.quantity) item.quantity = 1 + + if (cartId === config.dummyEmptyCartId) { + const createdCart = await config.fetch(checkoutCreateMutation, { + variables: { + input: { + shopId: config.shopId, + items: [ + { + productConfiguration: { + productId: item.productId, + productVariantId: item.variantId, + }, + quantity: item.quantity, + price: item.pricing, + }, + ], + }, + }, + }) + + console.log('cart token', createdCart.data.createCart.token) + console.log('created cart', createdCart.data.createCart.cart) + + res.setHeader('Set-Cookie', [ + getCartCookie(config.cartCookie, createdCart.data.createCart.token, 999), + getCartCookie( + config.cartIdCookie, + createdCart.data.createCart.cart._id, + 999 + ), + ]) + return res.status(200).json(createdCart.data) + } else if (cartId && anonymousCartToken) { + const updatedCart = await config.fetch(addCartItemsMutation, { + variables: { + input: { + cartId, + cartToken: anonymousCartToken, + items: [ + { + productConfiguration: { + productId: item.productId, + productVariantId: item.variantId, + }, + quantity: item.quantity, + price: item.pricing, + }, + ], + }, + }, + }) + + console.log('updatedCart', updatedCart) + + return res.status(200).json(updatedCart.data) + } + + res.status(200) +} + +export default addItem diff --git a/framework/reactioncommerce/api/cart/handlers/get-cart.ts b/framework/reactioncommerce/api/cart/handlers/get-cart.ts new file mode 100644 index 000000000..0e6eb5c36 --- /dev/null +++ b/framework/reactioncommerce/api/cart/handlers/get-cart.ts @@ -0,0 +1,50 @@ +import type { Cart } from '../../../types' +import type { CartHandlers } from '../' +import getAnomymousCartQuery from '@framework/utils/queries/get-anonymous-cart' +import getCartCookie from '@framework/api/utils/get-cart-cookie' +import { + REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + REACTION_CART_ID_COOKIE, +} from '@framework/const.ts' +import { normalizeCart } from '@framework/utils' + +// Return current cart info +const getCart: CartHandlers['getCart'] = async ({ + req: { + cookies: { + [REACTION_ANONYMOUS_CART_TOKEN_COOKIE]: anonymousCartToken, + [REACTION_CART_ID_COOKIE]: cartId, + }, + }, + res, + config, +}) => { + let normalizedCart + + console.log('get-cart API') + console.log('anonymousCartToken', anonymousCartToken) + console.log('cartId', cartId) + console.log('shopId', config.shopId) + + if (cartId && anonymousCartToken) { + const { + data: { cart: rawCart }, + } = await config.fetch(getAnomymousCartQuery, { + variables: { + cartId, + cartToken: anonymousCartToken, + }, + }) + + normalizedCart = normalizeCart(rawCart) + } else { + res.setHeader( + 'Set-Cookie', + getCartCookie(config.cartCookie, config.dummyEmptyCartId, 999) + ) + } + + res.status(200).json({ data: normalizedCart ?? null }) +} + +export default getCart diff --git a/framework/reactioncommerce/api/cart/index.ts b/framework/reactioncommerce/api/cart/index.ts new file mode 100644 index 000000000..7a2197ce7 --- /dev/null +++ b/framework/reactioncommerce/api/cart/index.ts @@ -0,0 +1,60 @@ +import isAllowedMethod from '../utils/is-allowed-method' +import createApiHandler, { + ReactionCommerceApiHandler, + ReactionCommerceHandler, +} from '../utils/create-api-handler' +import { ReactionCommerceApiError } from '../utils/errors' +import getCart from './handlers/get-cart' +import addItem from './handlers/add-item' +import type { + Cart, + GetCartHandlerBody, + AddCartItemHandlerBody, +} from '../../types' + +export type CartHandlers = { + getCart: ReactionCommerceHandler + addItem: ReactionCommerceHandler +} + +const METHODS = ['GET', 'POST'] + +// TODO: a complete implementation should have schema validation for `req.body` +const cartApi: ReactionCommerceApiHandler = 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 }) + } + } catch (error) { + console.error(error) + + const message = + error instanceof ReactionCommerceApiError + ? 'An unexpected error occurred with the Reaction Commerce API' + : 'An unexpected error occurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export const handlers = { getCart, addItem } + +export default createApiHandler(cartApi, handlers, {}) diff --git a/framework/reactioncommerce/api/catalog/index.ts b/framework/reactioncommerce/api/catalog/index.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/reactioncommerce/api/catalog/index.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/reactioncommerce/api/catalog/products.ts b/framework/reactioncommerce/api/catalog/products.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/reactioncommerce/api/catalog/products.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/reactioncommerce/api/checkout/index.ts b/framework/reactioncommerce/api/checkout/index.ts new file mode 100644 index 000000000..de2cb835c --- /dev/null +++ b/framework/reactioncommerce/api/checkout/index.ts @@ -0,0 +1,50 @@ +import isAllowedMethod from '../utils/is-allowed-method' +import createApiHandler, { + ReactionCommerceApiHandler, +} from '../utils/create-api-handler' + +import { + REACTION_ANONYMOUS_CART_TOKEN_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: ReactionCommerceApiHandler = async ( + req, + res, + config +) => { + if (!isAllowedMethod(req, res, METHODS)) return + + config = getConfig() + + const { cookies } = req + const checkoutUrl = cookies[SHOPIFY_CHECKOUT_URL_COOKIE] + const customerCookie = cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE] + + if (customerCookie) { + try { + await config.fetch(associateCustomerWithCheckoutMutation, { + variables: { + checkoutId: cookies[REACTION_ANONYMOUS_CART_TOKEN_COOKIE], + customerAccessToken: cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE], + }, + }) + } catch (error) { + console.error(error) + } + } + + if (checkoutUrl) { + res.redirect(checkoutUrl) + } else { + res.redirect('/cart') + } +} + +export default createApiHandler(checkoutApi, {}, {}) diff --git a/framework/reactioncommerce/api/customer.ts b/framework/reactioncommerce/api/customer.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/reactioncommerce/api/customer.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/reactioncommerce/api/customers/index.ts b/framework/reactioncommerce/api/customers/index.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/reactioncommerce/api/customers/index.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/reactioncommerce/api/customers/login.ts b/framework/reactioncommerce/api/customers/login.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/reactioncommerce/api/customers/login.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/reactioncommerce/api/customers/logout.ts b/framework/reactioncommerce/api/customers/logout.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/reactioncommerce/api/customers/logout.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/reactioncommerce/api/customers/signup.ts b/framework/reactioncommerce/api/customers/signup.ts new file mode 100644 index 000000000..ea9b101e1 --- /dev/null +++ b/framework/reactioncommerce/api/customers/signup.ts @@ -0,0 +1 @@ +export default function () {} diff --git a/framework/reactioncommerce/api/index.ts b/framework/reactioncommerce/api/index.ts new file mode 100644 index 000000000..2b7c47b89 --- /dev/null +++ b/framework/reactioncommerce/api/index.ts @@ -0,0 +1,60 @@ +import type { CommerceAPIConfig } from '@commerce/api' + +import { + API_URL, + REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + REACTION_CART_ID_COOKIE, + REACTION_EMPTY_DUMMY_CART_ID, + SHOPIFY_CUSTOMER_TOKEN_COOKIE, + SHOPIFY_COOKIE_EXPIRE, + SHOP_ID, +} from '../const' + +if (!API_URL) { + throw new Error( + `The environment variable NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN is missing and it's required to access your store` + ) +} + +import fetchGraphqlApi from './utils/fetch-graphql-api' + +export interface ReactionCommerceConfig extends CommerceAPIConfig {} + +export class Config { + private config: ReactionCommerceConfig + + constructor(config: ReactionCommerceConfig) { + 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', + commerceUrl: API_URL, + cartCookie: REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + cartIdCookie: REACTION_CART_ID_COOKIE, + dummyEmptyCartId: REACTION_EMPTY_DUMMY_CART_ID, + cartCookieMaxAge: SHOPIFY_COOKIE_EXPIRE, + fetch: fetchGraphqlApi, + customerCookie: SHOPIFY_CUSTOMER_TOKEN_COOKIE, + shopId: SHOP_ID, +}) + +export function getConfig(userConfig?: Partial) { + return config.getConfig(userConfig) +} + +export function setConfig(newConfig: Partial) { + return config.setConfig(newConfig) +} diff --git a/framework/reactioncommerce/api/operations/get-page.ts b/framework/reactioncommerce/api/operations/get-page.ts new file mode 100644 index 000000000..7fd1ddb7f --- /dev/null +++ b/framework/reactioncommerce/api/operations/get-page.ts @@ -0,0 +1,25 @@ +import { Page } from '../../schema' +import { ReactionCommerceConfig, getConfig } from '..' + +export type GetPageResult = T + +export type PageVariables = { + id: string +} + +async function getPage({ + url, + variables, + config, + preview, +}: { + url?: string + variables: PageVariables + config?: ReactionCommerceConfig + preview?: boolean +}): Promise { + config = getConfig(config) + return {} +} + +export default getPage diff --git a/framework/reactioncommerce/api/utils/create-api-handler.ts b/framework/reactioncommerce/api/utils/create-api-handler.ts new file mode 100644 index 000000000..92dd11af8 --- /dev/null +++ b/framework/reactioncommerce/api/utils/create-api-handler.ts @@ -0,0 +1,60 @@ +import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' +import { ReactionCommerceConfig, getConfig } from '..' + +export type ReactionCommerceApiHandler< + T = any, + H extends ReactionCommerceHandlers = {}, + Options extends {} = {} +> = ( + req: NextApiRequest, + res: NextApiResponse>, + config: ReactionCommerceConfig, + handlers: H, + // Custom configs that may be used by a particular handler + options: Options +) => void | Promise + +export type ReactionCommerceHandler = (options: { + req: NextApiRequest + res: NextApiResponse> + config: ReactionCommerceConfig + body: Body +}) => void | Promise + +export type ReactionCommerceHandlers = { + [k: string]: ReactionCommerceHandler +} + +export type ReactionCommerceApiResponse = { + data: T | null + errors?: { message: string; code?: string }[] +} + +export default function createApiHandler< + T = any, + H extends ReactionCommerceHandlers = {}, + Options extends {} = {} +>( + handler: ReactionCommerceApiHandler, + handlers: H, + defaultOptions: Options +) { + console.log('next api handler', defaultOptions) + + return function getApiHandler({ + config, + operations, + options, + }: { + config?: ReactionCommerceConfig + 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/reactioncommerce/api/utils/errors.ts b/framework/reactioncommerce/api/utils/errors.ts new file mode 100644 index 000000000..5b4296ac6 --- /dev/null +++ b/framework/reactioncommerce/api/utils/errors.ts @@ -0,0 +1,25 @@ +import type { Response } from '@vercel/fetch' + +// Used for GraphQL errors +export class ReactionCommerceGraphQLError extends Error {} + +export class ReactionCommerceApiError extends Error { + status: number + res: Response + data: any + + constructor(msg: string, res: Response, data?: any) { + super(msg) + this.name = 'ReactionCommerceApiError' + this.status = res.status + this.res = res + this.data = data + } +} + +export class ReactionCommerceNetworkError extends Error { + constructor(msg: string) { + super(msg) + this.name = 'ReactionCommerceNetworkError' + } +} diff --git a/framework/reactioncommerce/api/utils/fetch-all-products.ts b/framework/reactioncommerce/api/utils/fetch-all-products.ts new file mode 100644 index 000000000..94f5c7211 --- /dev/null +++ b/framework/reactioncommerce/api/utils/fetch-all-products.ts @@ -0,0 +1,41 @@ +import { ProductEdge } from '../../schema' +import { ReactionCommerceConfig } from '..' + +const fetchAllProducts = async ({ + config, + query, + variables, + acc = [], + cursor, +}: { + config: ReactionCommerceConfig + 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/reactioncommerce/api/utils/fetch-graphql-api.ts b/framework/reactioncommerce/api/utils/fetch-graphql-api.ts new file mode 100644 index 000000000..dfbcf0343 --- /dev/null +++ b/framework/reactioncommerce/api/utils/fetch-graphql-api.ts @@ -0,0 +1,33 @@ +import type { GraphQLFetcher } from '@commerce/api' +import fetch from './fetch' + +import { API_URL } from '../../const' +import { getError } from '../../utils/handle-fetch-response' + +const fetchGraphqlApi: GraphQLFetcher = async ( + query: string, + { variables } = {}, + fetchOptions +) => { + const res = await fetch(API_URL, { + ...fetchOptions, + method: 'POST', + headers: { + ...fetchOptions?.headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + variables, + }), + }) + + const { data, errors, status } = await res.json() + + if (errors) { + throw getError(errors, status) + } + + return { data, res } +} +export default fetchGraphqlApi diff --git a/framework/reactioncommerce/api/utils/fetch.ts b/framework/reactioncommerce/api/utils/fetch.ts new file mode 100644 index 000000000..0b8367102 --- /dev/null +++ b/framework/reactioncommerce/api/utils/fetch.ts @@ -0,0 +1,2 @@ +import zeitFetch from '@vercel/fetch' +export default zeitFetch() diff --git a/framework/reactioncommerce/api/utils/get-cart-cookie.ts b/framework/reactioncommerce/api/utils/get-cart-cookie.ts new file mode 100644 index 000000000..7ca6cd5e4 --- /dev/null +++ b/framework/reactioncommerce/api/utils/get-cart-cookie.ts @@ -0,0 +1,20 @@ +import { serialize, CookieSerializeOptions } from 'cookie' + +export default function getCartCookie( + name: string, + cartId?: string, + maxAge?: number +) { + const options: CookieSerializeOptions = + cartId && maxAge + ? { + maxAge, + expires: new Date(Date.now() + maxAge * 1000), + secure: process.env.NODE_ENV === 'production', + path: '/', + sameSite: 'lax', + } + : { maxAge: -1, path: '/' } // Removes the cookie + + return serialize(name, cartId || '', options) +} diff --git a/framework/reactioncommerce/api/utils/is-allowed-method.ts b/framework/reactioncommerce/api/utils/is-allowed-method.ts new file mode 100644 index 000000000..78bbba568 --- /dev/null +++ b/framework/reactioncommerce/api/utils/is-allowed-method.ts @@ -0,0 +1,28 @@ +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/reactioncommerce/auth/use-login.tsx b/framework/reactioncommerce/auth/use-login.tsx new file mode 100644 index 000000000..188dd54a2 --- /dev/null +++ b/framework/reactioncommerce/auth/use-login.tsx @@ -0,0 +1,76 @@ +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import { CommerceError, ValidationError } from '@commerce/utils/errors' +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 } from '../utils' + +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 = { + fetchOptions: { + query: createCustomerAccessTokenMutation, + }, + async fetcher({ input: { email, password }, options, fetch }) { + if (!(email && password)) { + throw new CommerceError({ + message: + 'A first name, last name, email and password are required to login', + }) + } + + const { customerAccessTokenCreate } = await fetch< + Mutation, + MutationCheckoutCreateArgs + >({ + ...options, + variables: { + input: { email, password }, + }, + }) + + const errors = customerAccessTokenCreate?.customerUserErrors + + if (errors && errors.length) { + throw new ValidationError({ + message: getErrorMessage(errors[0]), + }) + } + const customerAccessToken = customerAccessTokenCreate?.customerAccessToken + const accessToken = customerAccessToken?.accessToken + + if (accessToken) { + setCustomerToken(accessToken) + } + + return null + }, + useHook: ({ fetch }) => () => { + const { revalidate } = useCustomer() + + return useCallback( + async function login(input) { + const data = await fetch({ input }) + await revalidate() + return data + }, + [fetch, revalidate] + ) + }, +} diff --git a/framework/reactioncommerce/auth/use-logout.tsx b/framework/reactioncommerce/auth/use-logout.tsx new file mode 100644 index 000000000..81a3b8cdd --- /dev/null +++ b/framework/reactioncommerce/auth/use-logout.tsx @@ -0,0 +1,36 @@ +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import useLogout, { UseLogout } from '@commerce/auth/use-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 = { + fetchOptions: { + query: customerAccessTokenDeleteMutation, + }, + async fetcher({ options, fetch }) { + await fetch({ + ...options, + variables: { + customerAccessToken: getCustomerToken(), + }, + }) + setCustomerToken(null) + return null + }, + useHook: ({ fetch }) => () => { + const { mutate } = useCustomer() + + return useCallback( + async function logout() { + const data = await fetch() + await mutate(null, false) + return data + }, + [fetch, mutate] + ) + }, +} diff --git a/framework/reactioncommerce/auth/use-signup.tsx b/framework/reactioncommerce/auth/use-signup.tsx new file mode 100644 index 000000000..7f66448d3 --- /dev/null +++ b/framework/reactioncommerce/auth/use-signup.tsx @@ -0,0 +1,74 @@ +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 useCustomer from '../customer/use-customer' +import { CustomerCreateInput } from '../schema' + +import { + customerCreateMutation, + customerAccessTokenCreateMutation, +} from '../utils/mutations' +import handleLogin from '../utils/handle-login' + +export default useSignup as UseSignup + +export const handler: MutationHook< + null, + {}, + CustomerCreateInput, + CustomerCreateInput +> = { + fetchOptions: { + query: customerCreateMutation, + }, + async fetcher({ + input: { firstName, lastName, email, password }, + options, + fetch, + }) { + if (!(firstName && lastName && email && password)) { + throw new CommerceError({ + message: + 'A first name, last name, email and password are required to signup', + }) + } + const data = await fetch({ + ...options, + variables: { + input: { + firstName, + lastName, + email, + password, + }, + }, + }) + + try { + const loginData = await fetch({ + query: customerAccessTokenCreateMutation, + variables: { + input: { + email, + password, + }, + }, + }) + handleLogin(loginData) + } catch (error) {} + return data + }, + useHook: ({ fetch }) => () => { + const { revalidate } = useCustomer() + + return useCallback( + async function signup(input) { + const data = await fetch({ input }) + await revalidate() + return data + }, + [fetch, revalidate] + ) + }, +} diff --git a/framework/reactioncommerce/cart/index.ts b/framework/reactioncommerce/cart/index.ts new file mode 100644 index 000000000..3d288b1df --- /dev/null +++ b/framework/reactioncommerce/cart/index.ts @@ -0,0 +1,3 @@ +export { default as useCart } from './use-cart' +export { default as useAddItem } from './use-add-item' +export { default as useRemoveItem } from './use-remove-item' diff --git a/framework/reactioncommerce/cart/use-add-item.tsx b/framework/reactioncommerce/cart/use-add-item.tsx new file mode 100644 index 000000000..228d99bbe --- /dev/null +++ b/framework/reactioncommerce/cart/use-add-item.tsx @@ -0,0 +1,46 @@ +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 { Cart, CartItemBody, AddCartItemBody } from '../types' +import useCart from './use-cart' + +export default useAddItem as UseAddItem + +export const handler: MutationHook = { + fetchOptions: { + url: '/api/reactioncommerce/cart', + method: 'POST', + }, + async fetcher({ input: item, options, fetch }) { + console.log('add cart item', item) + + if ( + item.quantity && + (!Number.isInteger(item.quantity) || item.quantity! < 1) + ) { + throw new CommerceError({ + message: 'The item quantity has to be a valid integer greater than 0', + }) + } + + const data = await fetch({ + ...options, + body: { item }, + }) + + return data + }, + useHook: ({ fetch }) => () => { + const { mutate } = useCart() + + return useCallback( + async function addItem(input) { + const data = await fetch({ input }) + await mutate(data, false) + return data + }, + [fetch, mutate] + ) + }, +} diff --git a/framework/reactioncommerce/cart/use-cart.tsx b/framework/reactioncommerce/cart/use-cart.tsx new file mode 100644 index 000000000..b2bb66fec --- /dev/null +++ b/framework/reactioncommerce/cart/use-cart.tsx @@ -0,0 +1,41 @@ +import { useMemo } from 'react' +import { SWRHook } from '@commerce/utils/types' +import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart' +import type { Cart } from '../types' + +export default useCart as UseCart + +export const handler: SWRHook< + Cart | null, + {}, + FetchCartInput, + { isEmpty?: boolean } +> = { + fetchOptions: { + method: 'GET', + url: '/api/reactioncommerce/cart', + }, + async fetcher({ input: { cartId }, options, fetch }) { + console.log('cart API fetcher', options) + const data = await fetch(options) + return data + }, + useHook: ({ useData }) => (input) => { + const response = useData({ + swrOptions: { revalidateOnFocus: false, ...input?.swrOptions }, + }) + + return useMemo( + () => + Object.create(response, { + isEmpty: { + get() { + return (response.data?.lineItems.length ?? 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, +} diff --git a/framework/reactioncommerce/cart/use-remove-item.tsx b/framework/reactioncommerce/cart/use-remove-item.tsx new file mode 100644 index 000000000..de0ecd64f --- /dev/null +++ b/framework/reactioncommerce/cart/use-remove-item.tsx @@ -0,0 +1,85 @@ +import { useCallback } from 'react' + +import type { + MutationHookContext, + HookFetcherContext, +} from '@commerce/utils/types' + +import { ValidationError } from '@commerce/utils/errors' + +import useRemoveItem, { + RemoveItemInput as RemoveItemInputBase, + UseRemoveItem, +} from '@commerce/cart/use-remove-item' + +import useCart from './use-cart' +import { + removeCartItemsMutation, + getAnonymousCartToken, + getCartId, + normalizeCart, +} from '../utils' +import { Cart, LineItem } from '../types' +import { Mutation, MutationUpdateCartItemsQuantityArgs } from '../schema' +import { RemoveCartItemBody } from '@commerce/types' + +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 + +export const handler = { + fetchOptions: { + query: removeCartItemsMutation, + }, + async fetcher({ + input: { itemId }, + options, + fetch, + }: HookFetcherContext) { + const { removeCartItems } = await fetch< + Mutation, + MutationUpdateCartItemsQuantityArgs + >({ + ...options, + variables: { + input: { + cartId: getCartId(), + cartToken: getAnonymousCartToken(), + cartItemIds: [itemId], + }, + }, + }) + return normalizeCart(removeCartItems?.cart) + }, + useHook: ({ + fetch, + }: MutationHookContext) => < + T extends LineItem | undefined = undefined + >( + ctx: { item?: T } = {} + ) => { + const { item } = ctx + const { mutate } = useCart() + const removeItem: RemoveItemFn = async (input) => { + const itemId = input?.id ?? item?.id + + if (!itemId) { + throw new ValidationError({ + message: 'Invalid input used for this operation', + }) + } + + const data = await fetch({ input: { itemId } }) + await mutate(data, false) + return data + } + + return useCallback(removeItem as RemoveItemFn, [fetch, mutate]) + }, +} diff --git a/framework/reactioncommerce/cart/use-update-item.tsx b/framework/reactioncommerce/cart/use-update-item.tsx new file mode 100644 index 000000000..5a6a1b9f9 --- /dev/null +++ b/framework/reactioncommerce/cart/use-update-item.tsx @@ -0,0 +1,116 @@ +import { useCallback } from 'react' +import debounce from 'lodash.debounce' +import type { + HookFetcherContext, + MutationHookContext, +} from '@commerce/utils/types' +import { ValidationError } from '@commerce/utils/errors' +import useUpdateItem, { + UpdateItemInput as UpdateItemInputBase, + 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 { + getAnonymousCartToken, + getCartId, + updateCartItemsQuantityMutation, + normalizeCart, +} from '../utils' +import { Mutation, MutationUpdateCartItemsQuantityArgs } from '../schema' + +export type UpdateItemInput = T extends LineItem + ? Partial> + : UpdateItemInputBase + +export default useUpdateItem as UseUpdateItem + +export const handler = { + fetchOptions: { + query: updateCartItemsQuantityMutation, + }, + async fetcher({ + input: { itemId, item }, + options, + fetch, + }: 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) { + return removeItemHandler.fetcher({ + options: removeItemHandler.fetchOptions, + input: { + itemId, + }, + fetch, + }) + } + } else if (item.quantity) { + throw new ValidationError({ + message: 'The item quantity has to be a valid integer', + }) + } + const { updateCartItemsQuantity } = await fetch< + Mutation, + MutationUpdateCartItemsQuantityArgs + >({ + ...options, + variables: { + updateCartItemsQuantityInput: { + cartId: getCartId(), + cartToken: getAnonymousCartToken(), + items: [ + { + cartItemId: itemId, + quantity: item.quantity, + }, + ], + }, + }, + }) + + return normalizeCart(updateCartItemsQuantity?.cart) + }, + useHook: ({ + fetch, + }: MutationHookContext) => < + T extends LineItem | undefined = undefined + >( + ctx: { + item?: T + wait?: number + } = {} + ) => { + const { item } = ctx + const { mutate } = useCart() as any + + return useCallback( + debounce(async (input: UpdateItemInput) => { + const itemId = input.id ?? item?.id + const productId = input.productId ?? item?.productId + const variantId = input.productId ?? item?.variantId + if (!itemId || !productId || !variantId) { + throw new ValidationError({ + message: 'Invalid input used for this operation', + }) + } + + const data = await fetch({ + input: { + item: { + productId, + variantId, + quantity: input.quantity, + }, + itemId, + }, + }) + await mutate(data, false) + return data + }, ctx.wait ?? 500), + [fetch, mutate] + ) + }, +} diff --git a/framework/reactioncommerce/cart/utils/checkout-create.ts b/framework/reactioncommerce/cart/utils/checkout-create.ts new file mode 100644 index 000000000..f072b5992 --- /dev/null +++ b/framework/reactioncommerce/cart/utils/checkout-create.ts @@ -0,0 +1,34 @@ +import { + REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + SHOPIFY_CHECKOUT_URL_COOKIE, + SHOPIFY_COOKIE_EXPIRE, +} from '../../const' + +import checkoutCreateMutation from '../../utils/mutations/checkout-create' +import Cookies from 'js-cookie' + +export const checkoutCreate = async (fetch: any) => { + const data = await fetch({ + query: checkoutCreateMutation, + variables: { + input: { + shopId, + }, + }, + }) + + const checkout = data.checkoutCreate?.checkout + const checkoutId = checkout?.id + + if (checkoutId) { + const options = { + expires: SHOPIFY_COOKIE_EXPIRE, + } + Cookies.set(REACTION_ANONYMOUS_CART_TOKEN_COOKIE, checkoutId, options) + Cookies.set(SHOPIFY_CHECKOUT_URL_COOKIE, checkout?.webUrl, options) + } + + return checkout +} + +export default checkoutCreate diff --git a/framework/reactioncommerce/cart/utils/fetcher.ts b/framework/reactioncommerce/cart/utils/fetcher.ts new file mode 100644 index 000000000..6afb55f18 --- /dev/null +++ b/framework/reactioncommerce/cart/utils/fetcher.ts @@ -0,0 +1,31 @@ +import { HookFetcherFn } from '@commerce/utils/types' +import { Cart } from '@commerce/types' +import { checkoutCreate, checkoutToCart } from '.' +import { FetchCartInput } from '@commerce/cart/use-cart' + +const fetcher: HookFetcherFn = async ({ + options, + input: { cartId: checkoutId }, + fetch, +}) => { + let checkout + + if (checkoutId) { + const data = await fetch({ + ...options, + variables: { + checkoutId, + }, + }) + checkout = data.node + } + + if (checkout?.completedAt || !checkoutId) { + checkout = await checkoutCreate(fetch) + } + + // TODO: Fix this type + return checkoutToCart({ checkout } as any) +} + +export default fetcher diff --git a/framework/reactioncommerce/cart/utils/index.ts b/framework/reactioncommerce/cart/utils/index.ts new file mode 100644 index 000000000..20d04955d --- /dev/null +++ b/framework/reactioncommerce/cart/utils/index.ts @@ -0,0 +1,2 @@ +export { default as checkoutToCart } from './checkout-to-cart' +export { default as checkoutCreate } from './checkout-create' diff --git a/framework/reactioncommerce/commerce.config.json b/framework/reactioncommerce/commerce.config.json new file mode 100644 index 000000000..5cbc67209 --- /dev/null +++ b/framework/reactioncommerce/commerce.config.json @@ -0,0 +1,6 @@ +{ + "provider": "reactioncommerce", + "features": { + "wishlist": false + } +} diff --git a/framework/reactioncommerce/common/get-all-pages.ts b/framework/reactioncommerce/common/get-all-pages.ts new file mode 100644 index 000000000..b844bc2a5 --- /dev/null +++ b/framework/reactioncommerce/common/get-all-pages.ts @@ -0,0 +1,42 @@ +import { getConfig, ReactionCommerceConfig } 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: ReactionCommerceConfig + 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/reactioncommerce/common/get-page.ts b/framework/reactioncommerce/common/get-page.ts new file mode 100644 index 000000000..5f63191ba --- /dev/null +++ b/framework/reactioncommerce/common/get-page.ts @@ -0,0 +1,37 @@ +import { getConfig, ReactionCommerceConfig } 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: ReactionCommerceConfig + 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/reactioncommerce/common/get-site-info.ts b/framework/reactioncommerce/common/get-site-info.ts new file mode 100644 index 000000000..e84ffaccf --- /dev/null +++ b/framework/reactioncommerce/common/get-site-info.ts @@ -0,0 +1,31 @@ +import getCategories, { Category } from '../utils/get-categories' +import getVendors, { Brands } from '../utils/get-vendors' + +import { getConfig, ReactionCommerceConfig } from '../api' + +export type GetSiteInfoResult< + T extends { categories: any[]; brands: any[] } = { + categories: Category[] + brands: Brands + } +> = T + +const getSiteInfo = async (options?: { + variables?: any + config: ReactionCommerceConfig + 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/reactioncommerce/const.ts b/framework/reactioncommerce/const.ts new file mode 100644 index 000000000..80090505f --- /dev/null +++ b/framework/reactioncommerce/const.ts @@ -0,0 +1,18 @@ +export const REACTION_ANONYMOUS_CART_TOKEN_COOKIE = + 'reaction_anonymousCartToken' + +export const REACTION_CART_ID_COOKIE = 'reaction_cartId' + +export const REACTION_EMPTY_DUMMY_CART_ID = 'DUMMY_EMPTY_CART_ID' + +export const SHOPIFY_CHECKOUT_URL_COOKIE = 'shopify_checkoutUrl' + +export const SHOPIFY_CUSTOMER_TOKEN_COOKIE = 'shopify_customerToken' + +export const STORE_DOMAIN = process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN + +export const SHOPIFY_COOKIE_EXPIRE = 30 + +export const API_URL = `http://127.0.0.1:3000/graphql` + +export const SHOP_ID = 'cmVhY3Rpb24vc2hvcDplcnBESFlDdzc5cFRBV0FHUg==' diff --git a/framework/reactioncommerce/customer/get-customer-id.ts b/framework/reactioncommerce/customer/get-customer-id.ts new file mode 100644 index 000000000..931615691 --- /dev/null +++ b/framework/reactioncommerce/customer/get-customer-id.ts @@ -0,0 +1,24 @@ +import { getConfig, ReactionCommerceConfig } 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?: ReactionCommerceConfig +}): 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/reactioncommerce/customer/index.ts b/framework/reactioncommerce/customer/index.ts new file mode 100644 index 000000000..6c903ecc5 --- /dev/null +++ b/framework/reactioncommerce/customer/index.ts @@ -0,0 +1 @@ +export { default as useCustomer } from './use-customer' diff --git a/framework/reactioncommerce/customer/use-customer.tsx b/framework/reactioncommerce/customer/use-customer.tsx new file mode 100644 index 000000000..137f0da74 --- /dev/null +++ b/framework/reactioncommerce/customer/use-customer.tsx @@ -0,0 +1,27 @@ +import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' +import { Customer } from '@commerce/types' +import { SWRHook } from '@commerce/utils/types' +import { getCustomerQuery, getCustomerToken } from '../utils' + +export default useCustomer as UseCustomer + +export const handler: SWRHook = { + fetchOptions: { + query: getCustomerQuery, + }, + async fetcher({ options, fetch }) { + const data = await fetch({ + ...options, + variables: { customerAccessToken: getCustomerToken() }, + }) + return data.customer ?? null + }, + useHook: ({ useData }) => (input) => { + return useData({ + swrOptions: { + revalidateOnFocus: false, + ...input?.swrOptions, + }, + }) + }, +} diff --git a/framework/reactioncommerce/fetcher.ts b/framework/reactioncommerce/fetcher.ts new file mode 100644 index 000000000..b4c57097b --- /dev/null +++ b/framework/reactioncommerce/fetcher.ts @@ -0,0 +1,57 @@ +import { FetcherError } from '@commerce/utils/errors' +import type { Fetcher } from '@commerce/utils/types' +import { handleFetchResponse } from './utils' +import { API_URL } from './const' + +async function getText(res: Response) { + try { + return (await res.text()) || res.statusText + } catch (error) { + return res.statusText + } +} + +async function getError(res: Response) { + if (res.headers.get('Content-Type')?.includes('application/json')) { + const data = await res.json() + return new FetcherError({ errors: data.errors, status: res.status }) + } + return new FetcherError({ message: await getText(res), status: res.status }) +} + +const fetcher: Fetcher = async ({ + url, + method = 'GET', + variables, + body: bodyObj, + query, +}) => { + // if no URL is passed but we have a `query` param, we assume it's GraphQL + if (!url && query) { + return handleFetchResponse( + await fetch(API_URL, { + method: 'POST', + body: JSON.stringify({ query, variables }), + headers: { + 'Content-Type': 'application/json', + }, + }) + ) + } + + const hasBody = Boolean(variables || bodyObj) && method !== 'GET' + const body = hasBody + ? JSON.stringify(variables ? { variables } : bodyObj) + : undefined + const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined + const res = await fetch(url!, { method, body, headers }) + + if (res.ok) { + const { data } = await res.json() + return data + } + + throw await getError(res) +} + +export default fetcher diff --git a/framework/reactioncommerce/index.tsx b/framework/reactioncommerce/index.tsx new file mode 100644 index 000000000..eb6fc8877 --- /dev/null +++ b/framework/reactioncommerce/index.tsx @@ -0,0 +1,44 @@ +import * as React from 'react' +import { ReactNode } from 'react' + +import { + CommerceConfig, + CommerceProvider as CoreCommerceProvider, + useCommerce as useCoreCommerce, +} from '@commerce' + +import { reactionCommerceProvider, ReactionCommerceProvider } from './provider' +import { REACTION_ANONYMOUS_CART_TOKEN_COOKIE, SHOP_ID } from './const' + +export { reactionCommerceProvider } +export type { ReactionCommerceProvider } + +export const reactionCommerceConfig: CommerceConfig = { + locale: 'en-us', + cartCookie: REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + shopId: SHOP_ID, +} + +export type ReactionCommerceConfig = Partial + +export type ReactionCommerceProps = { + children?: ReactNode + locale: string +} & ReactionCommerceConfig + +export function CommerceProvider({ + children, + ...config +}: ReactionCommerceProps) { + return ( + + {children} + + ) +} + +export const useCommerce = () => useCoreCommerce() diff --git a/framework/reactioncommerce/next.config.js b/framework/reactioncommerce/next.config.js new file mode 100644 index 000000000..ce46b706f --- /dev/null +++ b/framework/reactioncommerce/next.config.js @@ -0,0 +1,8 @@ +const commerce = require('./commerce.config.json') + +module.exports = { + commerce, + images: { + domains: ['localhost'], + }, +} diff --git a/framework/reactioncommerce/product/get-all-collections.ts b/framework/reactioncommerce/product/get-all-collections.ts new file mode 100644 index 000000000..fcddcbcc1 --- /dev/null +++ b/framework/reactioncommerce/product/get-all-collections.ts @@ -0,0 +1,29 @@ +import { CollectionEdge } from '../schema' +import { getConfig, ReactionCommerceConfig } from '../api' +import getAllCollectionsQuery from '../utils/queries/get-all-collections-query' + +const getAllCollections = async (options?: { + variables?: any + config: ReactionCommerceConfig + 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/reactioncommerce/product/get-all-product-paths.ts b/framework/reactioncommerce/product/get-all-product-paths.ts new file mode 100644 index 000000000..0dd3e5970 --- /dev/null +++ b/framework/reactioncommerce/product/get-all-product-paths.ts @@ -0,0 +1,51 @@ +import { Product } from '@commerce/types' +import { getConfig, ReactionCommerceConfig } from '../api' +import fetchAllProducts from '../api/utils/fetch-all-products' +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?: ReactionCommerceConfig + preview?: boolean +}): Promise => { + let { config, variables = { first: 250 } } = options ?? {} + config = getConfig(config) + + const products = await fetchAllProducts({ + config, + query: getAllProductsPathsQuery, + variables: { + ...variables, + shopIds: [config.shopId], + }, + }) + + return { + products: products?.map( + ({ + node: { + product: { slug }, + }, + }: CatalogItemEdge) => ({ + node: { + path: `/${slug}`, + }, + }) + ), + } +} + +export default getAllProductPaths diff --git a/framework/reactioncommerce/product/get-all-products.ts b/framework/reactioncommerce/product/get-all-products.ts new file mode 100644 index 000000000..49405ebe9 --- /dev/null +++ b/framework/reactioncommerce/product/get-all-products.ts @@ -0,0 +1,42 @@ +import { GraphQLFetcherResult } from '@commerce/api' +import { getConfig, ReactionCommerceConfig } from '../api' +import { CatalogItemEdge } from '../schema' +import { catalogItemsQuery } from '../utils/queries' +import { normalizeProduct } from '../utils/normalize' +import { Product } from '@commerce/types' + +type Variables = { + first?: number + shopIds?: string[] +} + +type ReturnType = { + products: CatalogItemConnection[] +} + +const getAllProducts = async (options: { + variables?: Variables + config?: ReactionCommerceConfig + preview?: boolean +}): Promise => { + let { config, variables = { first: 250 } } = options ?? {} + config = getConfig(config) + + const { data }: GraphQLFetcherResult = await config.fetch(catalogItemsQuery, { + variables: { + ...variables, + shopIds: [config.shopId], + }, + }) + + const catalogItems = + data.catalogItems?.edges?.map(({ node: p }: CatalogItemEdge) => + normalizeProduct(p) + ) ?? [] + + return { + products: catalogItems, + } +} + +export default getAllProducts diff --git a/framework/reactioncommerce/product/get-product.ts b/framework/reactioncommerce/product/get-product.ts new file mode 100644 index 000000000..b568c2543 --- /dev/null +++ b/framework/reactioncommerce/product/get-product.ts @@ -0,0 +1,32 @@ +import { GraphQLFetcherResult } from '@commerce/api' +import { getConfig, ReactionCommerceConfig } from '../api' +import { normalizeProduct, getProductQuery } from '../utils' + +type Variables = { + slug: string +} + +type ReturnType = { + product: any +} + +const getProduct = async (options: { + variables: Variables + config: ReactionCommerceConfig + preview?: boolean +}): Promise => { + let { config, variables } = options ?? {} + config = getConfig(config) + + const { data }: GraphQLFetcherResult = await config.fetch(getProductQuery, { + variables, + }) + + const { catalogItemProduct: product } = data + + return { + product: product ? normalizeProduct(product) : null, + } +} + +export default getProduct diff --git a/framework/reactioncommerce/product/use-price.tsx b/framework/reactioncommerce/product/use-price.tsx new file mode 100644 index 000000000..0174faf5e --- /dev/null +++ b/framework/reactioncommerce/product/use-price.tsx @@ -0,0 +1,2 @@ +export * from '@commerce/product/use-price' +export { default } from '@commerce/product/use-price' diff --git a/framework/reactioncommerce/product/use-search.tsx b/framework/reactioncommerce/product/use-search.tsx new file mode 100644 index 000000000..612601457 --- /dev/null +++ b/framework/reactioncommerce/product/use-search.tsx @@ -0,0 +1,80 @@ +import { SWRHook } from '@commerce/utils/types' +import useSearch, { UseSearch } from '@commerce/product/use-search' + +import { CatalogItemEdge } from '../schema' +import { + catalogItemsQuery, + getCollectionProductsQuery, + getSearchVariables, + normalizeProduct, +} from '../utils' + +import { Product } from '@commerce/types' + +export default useSearch as UseSearch + +export type SearchProductsInput = { + search?: string + categoryId?: string + brandId?: string + sort?: string + shopId?: string +} + +export type SearchProductsData = { + products: Product[] + found: boolean +} + +export const handler: SWRHook< + SearchProductsData, + SearchProductsInput, + SearchProductsInput +> = { + fetchOptions: { + query: catalogItemsQuery, + }, + async fetcher({ input, options, fetch }) { + const { brandId, shopId } = input + + const data = await fetch({ + query: options.query, + method: options?.method, + variables: { + ...getSearchVariables(input), + shopIds: [shopId], + }, + }) + + let edges + + edges = data.catalogItems?.edges ?? [] + if (brandId) { + edges = edges.filter( + ({ node: { vendor } }: CatalogItemEdge) => vendor === brandId + ) + } + + return { + products: edges.map(({ node }: CatalogItemEdge) => + normalizeProduct(node) + ), + found: !!edges.length, + } + }, + useHook: ({ useData }) => (input = {}) => { + return useData({ + input: [ + ['search', input.search], + ['categoryId', input.categoryId], + ['brandId', input.brandId], + ['sort', input.sort], + ['shopId', input.shopId], + ], + swrOptions: { + revalidateOnFocus: false, + ...input.swrOptions, + }, + }) + }, +} diff --git a/framework/reactioncommerce/provider.ts b/framework/reactioncommerce/provider.ts new file mode 100644 index 000000000..13b7af197 --- /dev/null +++ b/framework/reactioncommerce/provider.ts @@ -0,0 +1,36 @@ +import { + REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + REACTION_CART_ID_COOKIE, + STORE_DOMAIN, +} from './const' + +import { handler as useCart } from './cart/use-cart' +import { handler as useAddItem } from './cart/use-add-item' +import { handler as useUpdateItem } from './cart/use-update-item' +import { handler as useRemoveItem } from './cart/use-remove-item' + +import { handler as useCustomer } from './customer/use-customer' +import { handler as useSearch } from './product/use-search' + +import { handler as useLogin } from './auth/use-login' +import { handler as useLogout } from './auth/use-logout' +import { handler as useSignup } from './auth/use-signup' + +import fetcher from './fetcher' + +export const reactionCommerceProvider = { + locale: 'en-us', + cartCookie: REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + cartIdCookie: REACTION_CART_ID_COOKIE, + storeDomain: STORE_DOMAIN, + fetcher, + cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, + customer: { useCustomer }, + products: { useSearch }, + auth: { useLogin, useLogout, useSignup }, + features: { + wishlist: false, + }, +} + +export type ReactionCommerceProvider = typeof reactionCommerceProvider diff --git a/framework/reactioncommerce/schema.d.ts b/framework/reactioncommerce/schema.d.ts new file mode 100644 index 000000000..945444080 --- /dev/null +++ b/framework/reactioncommerce/schema.d.ts @@ -0,0 +1,7593 @@ +export type Maybe = T | null +export type Exact = { + [K in keyof T]: T[K] +} +export type MakeOptional = Omit & + { [SubKey in K]?: Maybe } +export type MakeMaybe = Omit & + { [SubKey in K]: Maybe } +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string + String: string + Boolean: boolean + Int: number + Float: number + /** A string email address */ + Email: any + /** + * + * An opaque string that identifies a particular result within a connection, + * allowing you to request a subset of results before or after that result. + * + */ + ConnectionCursor: any + /** + * + * An integer between 1 and 50, inclusive. Values less than 1 become 1 and + * values greater than 50 become 50. + * + */ + ConnectionLimitInt: any + /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ + DateTime: any + /** An object with any fields */ + JSONObject: any + /** A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ + Date: any +} + +/** Queries return all requested data, without any side effects */ +export type Query = { + __typename?: 'Query' + /** A test query */ + ping: Scalars['String'] + /** Returns the primary shop for the domain */ + primaryShop?: Maybe + /** Returns the ID of the primary shop for the domain */ + primaryShopId?: Maybe + /** Returns a shop by ID */ + shop?: Maybe + /** Returns a shop by slug */ + shopBySlug?: Maybe + shops?: Maybe + /** + * Returns app settings that are not shop specific. Plugins extend the GlobalSettings type to support + * whatever settings they need. + */ + globalSettings: GlobalSettings + /** + * Returns app settings for a specific shop. Plugins extend the ShopSettings type to support + * whatever settings they need. + */ + shopSettings: ShopSettings + /** + * Get a list of errors and suggested properly formatted addresses for an address. If no address + * validation service is active for the shop, this will return as if the address is valid even + * though no check actually occurred. + */ + addressValidation: AddressValidationResults + /** Get a full list of all registered address validation services */ + addressValidationServices: Array> + /** Returns a list of defined address validation rules for a shop */ + addressValidationRules: AddressValidationRuleConnection + /** SystemInformation object */ + systemInformation: SystemInformation + /** Retrieves a list of email templates */ + emailTemplates?: Maybe + /** Returns the account with the provided ID */ + account?: Maybe + /** Returns accounts optionally filtered by account groups */ + accounts: AccountConnection + /** Returns customer accounts */ + customers: AccountConnection + /** Returns the account for the authenticated user */ + viewer?: Maybe + /** Returns a single group by ID. */ + group?: Maybe + /** Returns a list of groups for the shop with ID `shopId`, as a Relay-compatible connection. */ + groups?: Maybe + /** Returns all pending staff member invitations */ + invitations: InvitationConnection + /** Returns a paged list of all roles associated with a shop */ + roles?: Maybe + /** Query for a single Product */ + product?: Maybe + /** Query for a list of Products */ + products?: Maybe + /** Gets items from a shop catalog */ + catalogItems?: Maybe + /** Gets product from catalog */ + catalogItemProduct?: Maybe + /** Returns a list of product in a tag */ + productsByTagId: TagProductConnection + /** Returns a tag from a provided tag ID or slug. Tags with isVisible set to false are excluded by default. */ + tag?: Maybe + /** Returns a paged list of tags for a shop. You must include a shopId when querying. */ + tags?: Maybe + /** + * Get the SimpleInventory info for a product configuration. Returns `null` if `updateSimpleInventory` + * has never been called for this product configuration. + */ + simpleInventory?: Maybe + /** Finds a cart by the cart ID and anonymous cart token. */ + anonymousCartByCartId?: Maybe + /** Find a cart for a given account ID. */ + accountCartByAccountId?: Maybe + /** Get an order by its ID */ + orderById?: Maybe + /** Get all orders for a single account, optionally limited to certain shop IDs and certain orderStatus */ + orders: OrderConnection + /** Get all orders for a single account, optionally limited to certain shop IDs and certain orderStatus */ + ordersByAccountId: OrdersByAccountIdConnection + /** Get an order by its reference ID (the ID shown to customers) */ + orderByReferenceId?: Maybe + /** Get refunds applied to an order by order ID */ + refunds?: Maybe>> + /** Get refunds applied to a specific payment by payment ID */ + refundsByPaymentId?: Maybe>> + /** + * Get a list of all payment methods available during a checkout. This may filter by auth, + * active/inactive, IP/region, shop, etc. To get the full list, use the `paymentMethods` + * query with proper authorization. + */ + availablePaymentMethods: Array> + /** Get a full list of all payment methods */ + paymentMethods: Array> + /** Gets discount codes */ + discountCodes?: Maybe + /** Get the full list of surcharges. */ + surcharges: SurchargeConnection + /** Get a single surcharge definition by its ID */ + surchargeById?: Maybe + /** Get a flat rate fulfillment method */ + flatRateFulfillmentMethod: FlatRateFulfillmentMethod + /** Get a flat rate fulfillment methods */ + flatRateFulfillmentMethods: FlatRateFulfillmentMethodConnection + /** Get the full list of flat rate fulfillment method restrictions. */ + getFlatRateFulfillmentRestrictions: FlatRateFulfillmentRestrictionConnection + /** Get a single flat rate fulfillment method restriction. */ + getFlatRateFulfillmentRestriction?: Maybe + /** List all tax codes supported by the current active tax service for the shop */ + taxCodes: Array> + /** Get a full list of all tax services for the shop */ + taxServices: Array> + /** Gets tax rates */ + taxRates?: Maybe + /** Returns a navigation tree by its ID in the specified language */ + navigationTreeById?: Maybe + /** Returns the navigation items for a shop */ + navigationItemsByShopId?: Maybe + /** Returns Sitemap object for a shop based on the handle param */ + sitemap?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryShopArgs = { + id: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryShopBySlugArgs = { + slug: Scalars['String'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryShopsArgs = { + shopIds?: Maybe>> + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryShopSettingsArgs = { + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryAddressValidationArgs = { + address: AddressInput + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryAddressValidationRulesArgs = { + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + serviceNames?: Maybe>> + shopId: Scalars['ID'] + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QuerySystemInformationArgs = { + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryEmailTemplatesArgs = { + shopId: Scalars['ID'] + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryAccountArgs = { + id: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryAccountsArgs = { + groupIds?: Maybe>> + notInAnyGroups?: Maybe + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryCustomersArgs = { + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryGroupArgs = { + id: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryGroupsArgs = { + shopId: Scalars['ID'] + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryInvitationsArgs = { + shopIds?: Maybe>> + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryRolesArgs = { + shopId: Scalars['ID'] + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryProductArgs = { + productId: Scalars['ID'] + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryProductsArgs = { + isArchived?: Maybe + isVisible?: Maybe + metafieldKey?: Maybe + metafieldValue?: Maybe + priceMax?: Maybe + priceMin?: Maybe + productIds?: Maybe>> + query?: Maybe + shopIds: Array> + tagIds?: Maybe>> + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryCatalogItemsArgs = { + shopIds: Array> + tagIds?: Maybe>> + booleanFilters?: Maybe>> + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortByPriceCurrencyCode?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryCatalogItemProductArgs = { + shopId?: Maybe + slugOrId?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryProductsByTagIdArgs = { + shopId: Scalars['ID'] + tagId: Scalars['ID'] + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryTagArgs = { + slugOrId: Scalars['String'] + shopId: Scalars['ID'] + shouldIncludeInvisible?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryTagsArgs = { + shopId: Scalars['ID'] + filter?: Maybe + excludedTagIds?: Maybe>> + isTopLevel?: Maybe + shouldIncludeDeleted?: Maybe + shouldIncludeInvisible?: Maybe + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QuerySimpleInventoryArgs = { + shopId: Scalars['ID'] + productConfiguration: ProductConfigurationInput +} + +/** Queries return all requested data, without any side effects */ +export type QueryAnonymousCartByCartIdArgs = { + cartId: Scalars['ID'] + cartToken: Scalars['String'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryAccountCartByAccountIdArgs = { + accountId: Scalars['ID'] + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryOrderByIdArgs = { + id: Scalars['ID'] + shopId: Scalars['ID'] + token?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryOrdersArgs = { + filters?: Maybe + shopIds?: Maybe>> + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryOrdersByAccountIdArgs = { + accountId: Scalars['ID'] + orderStatus?: Maybe>> + shopIds: Array> + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryOrderByReferenceIdArgs = { + id: Scalars['ID'] + shopId: Scalars['ID'] + token?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryRefundsArgs = { + orderId: Scalars['ID'] + shopId: Scalars['ID'] + token?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryRefundsByPaymentIdArgs = { + orderId: Scalars['ID'] + paymentId: Scalars['ID'] + shopId: Scalars['ID'] + token?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryAvailablePaymentMethodsArgs = { + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryPaymentMethodsArgs = { + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryDiscountCodesArgs = { + shopId: Scalars['ID'] + filters?: Maybe + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QuerySurchargesArgs = { + shopId: Scalars['ID'] + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QuerySurchargeByIdArgs = { + shopId: Scalars['ID'] + surchargeId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryFlatRateFulfillmentMethodArgs = { + methodId: Scalars['ID'] + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryFlatRateFulfillmentMethodsArgs = { + shopId: Scalars['ID'] + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryGetFlatRateFulfillmentRestrictionsArgs = { + shopId: Scalars['ID'] + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryGetFlatRateFulfillmentRestrictionArgs = { + restrictionId: Scalars['ID'] + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryTaxCodesArgs = { + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryTaxServicesArgs = { + shopId: Scalars['ID'] +} + +/** Queries return all requested data, without any side effects */ +export type QueryTaxRatesArgs = { + shopId: Scalars['ID'] + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryNavigationTreeByIdArgs = { + id: Scalars['ID'] + language: Scalars['String'] + shopId: Scalars['ID'] + shouldIncludeSecondary?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QueryNavigationItemsByShopIdArgs = { + shopId: Scalars['ID'] + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Queries return all requested data, without any side effects */ +export type QuerySitemapArgs = { + handle: Scalars['String'] + shopUrl: Scalars['String'] +} + +/** Represents a Reaction shop */ +export type Shop = Node & { + __typename?: 'Shop' + /** The shop ID */ + _id: Scalars['ID'] + /** An the shop's default address */ + addressBook?: Maybe>> + /** Whether to allow user to checkout without creating an account */ + allowGuestCheckout?: Maybe + /** The base unit of length */ + baseUOL?: Maybe + /** The base unit of Measure */ + baseUOM?: Maybe + /** URLs for various shop assets in various sizes */ + brandAssets?: Maybe + /** The default shop currency */ + currency: Currency + /** Default parcel size for this shop */ + defaultParcelSize?: Maybe + /** Shop description */ + description?: Maybe + /** The shop's default email record */ + emails?: Maybe>> + /** Shop's keywords */ + keywords?: Maybe + /** Shop default language */ + language: Scalars['String'] + /** Shop name */ + name: Scalars['String'] + /** Returns URLs for shop logos */ + shopLogoUrls?: Maybe + /** Shop's type */ + shopType?: Maybe + /** Shop's slug */ + slug?: Maybe + /** Returns URLs for various storefront routes */ + storefrontUrls?: Maybe + /** Shop default timezone */ + timezone?: Maybe + /** The shop's units of length */ + unitsOfLength?: Maybe>> + /** The shop's units of measure */ + unitsOfMeasure?: Maybe>> + /** Returns a list of groups for this shop, as a Relay-compatible connection. */ + groups?: Maybe + /** Returns a list of roles for this shop, as a Relay-compatible connection. */ + roles?: Maybe + /** Returns a paged list of tags for this shop */ + tags?: Maybe + /** The default navigation tree for this shop */ + defaultNavigationTree?: Maybe + /** The ID of the shop's default navigation tree */ + defaultNavigationTreeId?: Maybe +} + +/** Represents a Reaction shop */ +export type ShopGroupsArgs = { + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Represents a Reaction shop */ +export type ShopRolesArgs = { + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Represents a Reaction shop */ +export type ShopTagsArgs = { + isTopLevel?: Maybe + shouldIncludeDeleted?: Maybe + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Represents a Reaction shop */ +export type ShopDefaultNavigationTreeArgs = { + language: Scalars['String'] + shouldIncludeSecondary?: Maybe +} + +/** Objects implementing the Node interface will always have an _id field that is globally unique. */ +export type Node = { + /** The ID of the object */ + _id: Scalars['ID'] +} + +/** Represents a physical or mailing address somewhere on Earth */ +export type Address = { + __typename?: 'Address' + /** The address ID */ + _id?: Maybe + /** The street address / first line */ + address1: Scalars['String'] + /** Optional second line */ + address2?: Maybe + /** City */ + city: Scalars['String'] + /** Optional company name, if it's a business address */ + company?: Maybe + /** Country */ + country: Scalars['String'] + /** + * The first name of a person at this address + * This is an optional field to support legacy and third party platforms + * We use fullName internally, and use first and last name fields to combine into a full name if needed + */ + firstName?: Maybe + /** The full name of a person at this address */ + fullName: Scalars['String'] + /** Is this the default address for billing purposes? */ + isBillingDefault?: Maybe + /** Is this a commercial address? */ + isCommercial: Scalars['Boolean'] + /** Is this the default address to use when selecting a shipping address at checkout? */ + isShippingDefault?: Maybe + /** + * The last name of a person at this address + * This is an optional field to support legacy and third party platforms + * We use fullName internally, and use first and last name fields to combine into a full name if needed + */ + lastName?: Maybe + /** Arbitrary additional metadata about this address */ + metafields?: Maybe>> + /** A phone number for someone at this address */ + phone: Scalars['String'] + /** Postal code */ + postal: Scalars['String'] + /** Region. For example, a U.S. state */ + region: Scalars['String'] +} + +/** User defined attributes */ +export type Metafield = { + __typename?: 'Metafield' + /** Field description */ + description?: Maybe + /** Field key */ + key?: Maybe + /** Field namespace */ + namespace?: Maybe + /** Field scope */ + scope?: Maybe + /** Field value */ + value?: Maybe + /** Field value type */ + valueType?: Maybe +} + +/** URLs for various shop assets in various sizes */ +export type ShopBrandAssets = { + __typename?: 'ShopBrandAssets' + /** URLs for the navigation bar brand logo image */ + navbarBrandImage?: Maybe + /** Internal navigation bar brand logo image ID */ + navbarBrandImageId?: Maybe +} + +/** A list of URLs for various sizes of an image */ +export type ImageSizes = { + __typename?: 'ImageSizes' + /** Use this URL to get a large resolution file for this image */ + large?: Maybe + /** Use this URL to get a medium resolution file for this image */ + medium?: Maybe + /** + * Use this URL to get this image with its original resolution as uploaded. This may not be + * the true original size if there is a hard cap on how big image files can be. + */ + original?: Maybe + /** Use this URL to get a small resolution file for this image */ + small?: Maybe + /** Use this URL to get a thumbnail resolution file for this image */ + thumbnail?: Maybe +} + +/** Represents one type of currency */ +export type Currency = Node & { + __typename?: 'Currency' + /** ID */ + _id: Scalars['ID'] + /** Currency code */ + code: Scalars['String'] + /** Decimal symbol */ + decimal?: Maybe + /** Format string */ + format: Scalars['String'] + /** Exchange rate from shop default currency, if known */ + rate?: Maybe + /** The decimal scale used by this currency */ + scale?: Maybe + /** Currency symbol */ + symbol: Scalars['String'] + /** Thousands separator symbol */ + thousand?: Maybe +} + +/** Parcel size */ +export type ShopParcelSize = { + __typename?: 'ShopParcelSize' + /** Parcel height */ + height?: Maybe + /** Parcel length */ + length?: Maybe + /** Parcel weight */ + weight?: Maybe + /** Parcel width */ + width?: Maybe +} + +/** A confirmable email record */ +export type EmailRecord = { + __typename?: 'EmailRecord' + /** The actual email address */ + address?: Maybe + /** The services provided by this address */ + provides?: Maybe + /** Has this address been verified? */ + verified?: Maybe +} + +/** Shop logo URLs */ +export type ShopLogoUrls = { + __typename?: 'ShopLogoUrls' + /** The primary logo URL for this shop. Setting this overrides any uploaded logo. */ + primaryShopLogoUrl?: Maybe +} + +/** Storefront route URLs */ +export type StorefrontUrls = { + __typename?: 'StorefrontUrls' + /** Storefront Account Profile URL (can include `:accountId` in string) */ + storefrontAccountProfileUrl?: Maybe + /** Storefront Home URL */ + storefrontHomeUrl?: Maybe + /** Storefront login URL */ + storefrontLoginUrl?: Maybe + /** Storefront single order URL (can include `:orderReferenceId` and `:orderToken` in string) */ + storefrontOrderUrl?: Maybe + /** Storefront orders URL (can include `:accountId` in string) */ + storefrontOrdersUrl?: Maybe +} + +/** Units of length */ +export type UnitOfLength = { + __typename?: 'UnitOfLength' + /** Whether this unit of length is the default */ + default?: Maybe + /** The name of the unit of length */ + label?: Maybe + /** Unit of length */ + uol?: Maybe +} + +/** Units of measure */ +export type UnitOfMeasure = { + __typename?: 'UnitOfMeasure' + /** Whether this unit of measure is the default */ + default?: Maybe + /** The name of the unit of measure */ + label?: Maybe + /** Unit of measure */ + uom?: Maybe +} + +/** The order in which the connection results should be sorted, based on the sortBy field. */ +export enum SortOrder { + /** ascending */ + Asc = 'asc', + /** descending */ + Desc = 'desc', +} + +/** The fields by which you are allowed to sort any query that returns an `GroupConnection` */ +export enum GroupSortByField { + /** Group ID */ + Id = '_id', + /** Date and time at which this group was created */ + CreatedAt = 'createdAt', + /** Group name */ + Name = 'name', + /** Date and time at which this group was last updated */ + UpdatedAt = 'updatedAt', +} + +/** + * Wraps a list of `Groups`, providing pagination cursors and information. + * + * For information about what Relay-compatible connections are and how to use them, see the following articles: + * - [Relay Connection Documentation](https://facebook.github.io/relay/docs/en/graphql-server-specification.html#connections) + * - [Relay Connection Specification](https://facebook.github.io/relay/graphql/connections.htm) + * - [Using Relay-style Connections With Apollo Client](https://www.apollographql.com/docs/react/recipes/pagination.html) + */ +export type GroupConnection = { + __typename?: 'GroupConnection' + /** The list of nodes that match the query, wrapped in an edge to provide a cursor string for each */ + edges?: Maybe>> + /** + * You can request the `nodes` directly to avoid the extra wrapping that `NodeEdge` has, + * if you know you will not need to paginate the results. + */ + nodes?: Maybe>> + /** Information to help a client request the next or previous page */ + pageInfo: PageInfo + /** The total number of nodes that match your query */ + totalCount: Scalars['Int'] +} + +/** A connection edge in which each node is a `Group` object */ +export type GroupEdge = NodeEdge & { + __typename?: 'GroupEdge' + /** The cursor that represents this node in the paginated results */ + cursor: Scalars['ConnectionCursor'] + /** The group */ + node?: Maybe +} + +/** + * Objects implementing the NodeEdge interface will always have a node and a cursor + * that represents that node for purposes of requesting paginated results. + */ +export type NodeEdge = { + /** The cursor that represents this node in the paginated results */ + cursor: Scalars['ConnectionCursor'] + /** The node itself */ + node?: Maybe +} + +/** Represents an account group */ +export type Group = Node & { + __typename?: 'Group' + /** The group ID */ + _id: Scalars['ID'] + /** The date and time at which this group was created */ + createdAt: Scalars['DateTime'] + /** The account that created this group */ + createdBy?: Maybe + /** A free text description of this group */ + description?: Maybe + /** A unique name for the group */ + name: Scalars['String'] + /** The shop to which this group belongs */ + shop?: Maybe + /** A unique URL-safe string representing this group */ + slug: Scalars['String'] + /** The date and time at which this group was last updated */ + updatedAt: Scalars['DateTime'] + /** A list of the account permissions implied by membership in this group */ + permissions?: Maybe>> +} + +/** Represents a single user account */ +export type Account = Node & { + __typename?: 'Account' + /** The account ID */ + _id: Scalars['ID'] + /** A list of physical or mailing addresses associated with this account */ + addressBook?: Maybe + /** A list of shops this user can administer with the admin UI */ + adminUIShops?: Maybe>> + /** Bio to display on profile */ + bio?: Maybe + /** The date and time at which this account was created */ + createdAt: Scalars['DateTime'] + /** The preferred currency used by this account */ + currency?: Maybe + /** A list of email records associated with this account */ + emailRecords?: Maybe>> + /** The first name of the person this account represents, if known */ + firstName?: Maybe + /** The preferred language used by this account */ + language?: Maybe + /** The last name of the person this account represents, if known */ + lastName?: Maybe + /** Arbitrary additional metadata about this account */ + metafields?: Maybe>> + /** The full name of the person this account represents, if known */ + name?: Maybe + /** Some note about this account */ + note?: Maybe + /** URL of picture to display on profile */ + picture?: Maybe + /** An object storing plugin-specific preferences for this account */ + preferences?: Maybe + /** The primary email address for the account. This matches the address in `emailRecords` where `provides` is `default`. */ + primaryEmailAddress: Scalars['Email'] + /** The date and time at which this account was last updated */ + updatedAt?: Maybe + /** The Identity user ID with which this account is associated */ + userId: Scalars['String'] + /** Username */ + username?: Maybe + /** A paged list of the account groups in which this account is listed */ + groups?: Maybe +} + +/** Represents a single user account */ +export type AccountAddressBookArgs = { + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe +} + +/** Represents a single user account */ +export type AccountGroupsArgs = { + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** + * Wraps a list of `Addresses`, providing pagination cursors and information. + * + * For information about what Relay-compatible connections are and how to use them, see the following articles: + * - [Relay Connection Documentation](https://facebook.github.io/relay/docs/en/graphql-server-specification.html#connections) + * - [Relay Connection Specification](https://facebook.github.io/relay/graphql/connections.htm) + * - [Using Relay-style Connections With Apollo Client](https://www.apollographql.com/docs/react/recipes/pagination.html) + */ +export type AddressConnection = { + __typename?: 'AddressConnection' + /** The list of nodes that match the query, wrapped in an edge to provide a cursor string for each */ + edges?: Maybe>> + /** + * You can request the `nodes` directly to avoid the extra wrapping that `NodeEdge` has, + * if you know you will not need to paginate the results. + */ + nodes?: Maybe>> + /** Information to help a client request the next or previous page */ + pageInfo: PageInfo + /** The total number of nodes that match your query */ + totalCount: Scalars['Int'] +} + +/** A connection edge in which each node is an `Address` object */ +export type AddressEdge = { + __typename?: 'AddressEdge' + /** The cursor that represents this node in the paginated results */ + cursor: Scalars['ConnectionCursor'] + /** The address */ + node?: Maybe
+} + +/** + * Pagination information. When requesting pages of results, you can use endCursor or startCursor + * as your before or after parameters for the query you are paging. + */ +export type PageInfo = { + __typename?: 'PageInfo' + /** When paginating forwards, the cursor to continue. */ + endCursor?: Maybe + /** When paginating forwards, are there more items? */ + hasNextPage: Scalars['Boolean'] + /** When paginating backwards, are there more items? */ + hasPreviousPage: Scalars['Boolean'] + /** When paginating backwards, the cursor to continue. */ + startCursor?: Maybe +} + +/** The fields by which you are allowed to sort any query that returns an `RoleConnection` */ +export enum RoleSortByField { + /** Role ID */ + Id = '_id', + /** Role name */ + Name = 'name', +} + +/** + * Wraps a list of `Roles`, providing pagination cursors and information. + * + * For information about what Relay-compatible connections are and how to use them, see the following articles: + * - [Relay Connection Documentation](https://facebook.github.io/relay/docs/en/graphql-server-specification.html#connections) + * - [Relay Connection Specification](https://facebook.github.io/relay/graphql/connections.htm) + * - [Using Relay-style Connections With Apollo Client](https://www.apollographql.com/docs/react/recipes/pagination.html) + */ +export type RoleConnection = { + __typename?: 'RoleConnection' + /** The list of nodes that match the query, wrapped in an edge to provide a cursor string for each */ + edges?: Maybe>> + /** + * You can request the `nodes` directly to avoid the extra wrapping that `NodeEdge` has, + * if you know you will not need to paginate the results. + */ + nodes?: Maybe>> + /** Information to help a client request the next or previous page */ + pageInfo: PageInfo + /** The total number of nodes that match your query */ + totalCount: Scalars['Int'] +} + +/** A connection edge in which each node is a `Role` object */ +export type RoleEdge = NodeEdge & { + __typename?: 'RoleEdge' + /** The cursor that represents this node in the paginated results */ + cursor: Scalars['ConnectionCursor'] + /** The role */ + node?: Maybe +} + +/** Represents a named role */ +export type Role = Node & { + __typename?: 'Role' + /** The role ID */ + _id: Scalars['ID'] + /** A unique name for the role */ + name: Scalars['String'] +} + +/** The fields by which you are allowed to sort any query that returns a `TagConnection` */ +export enum TagSortByField { + /** Tag ID */ + Id = '_id', + /** Date and time the tag was created */ + CreatedAt = 'createdAt', + /** Tag name */ + Name = 'name', + /** Tag position */ + Position = 'position', + /** Date and time the tag was last updated */ + UpdatedAt = 'updatedAt', +} + +/** + * Wraps a list of `Tags`, providing pagination cursors and information. + * + * For information about what Relay-compatible connections are and how to use them, see the following articles: + * - [Relay Connection Documentation](https://facebook.github.io/relay/docs/en/graphql-server-specification.html#connections) + * - [Relay Connection Specification](https://facebook.github.io/relay/graphql/connections.htm) + * - [Using Relay-style Connections With Apollo Client](https://www.apollographql.com/docs/react/recipes/pagination.html) + */ +export type TagConnection = { + __typename?: 'TagConnection' + /** The list of nodes that match the query, wrapped in an edge to provide a cursor string for each */ + edges?: Maybe>> + /** + * You can request the `nodes` directly to avoid the extra wrapping that `NodeEdge` has, + * if you know you will not need to paginate the results. + */ + nodes?: Maybe>> + /** Information to help a client request the next or previous page */ + pageInfo: PageInfo + /** The total number of nodes that match your query */ + totalCount: Scalars['Int'] +} + +/** A connection edge in which each node is a `Tag` object */ +export type TagEdge = NodeEdge & { + __typename?: 'TagEdge' + /** The cursor that represents this node in the paginated results */ + cursor: Scalars['ConnectionCursor'] + /** The tag */ + node?: Maybe +} + +/** Represents a single tag */ +export type Tag = Node & + Deletable & { + __typename?: 'Tag' + /** The tag ID */ + _id: Scalars['ID'] + /** The date and time at which this tag was created */ + createdAt: Scalars['DateTime'] + /** A string of the title to be displayed on a Tag Listing Page */ + displayTitle?: Maybe + /** A list of the IDs of top products in this tag */ + featuredProductIds?: Maybe>> + /** A string containing the hero image url for a Tag Listing Page */ + heroMediaUrl?: Maybe + /** + * If `true`, this object should be considered deleted. Soft deleted objects are not + * returned in query results unless you explicitly ask for them. + */ + isDeleted: Scalars['Boolean'] + /** If `true`, this tag should be shown at the top level of the tag hierarchy */ + isTopLevel: Scalars['Boolean'] + /** If `true`, this tag's Tag Listing Page should be visible to the public */ + isVisible: Scalars['Boolean'] + /** Arbitrary additional metadata about this tag */ + metafields?: Maybe>> + /** The display name for the tag. This is unique within a given shop. */ + name: Scalars['String'] + /** The tag's position relative to other tags at the same level of the tag hierarchy */ + position?: Maybe + /** The shop to which this tag belongs */ + shop: Shop + /** A unique URL-safe string representing this tag for links */ + slug?: Maybe + /** A list of the IDs of tags that have this tag as their parent in the tag hierarchy, in the user-defined order */ + subTagIds: Array> + /** The date and time at which this tag was last updated */ + updatedAt: Scalars['DateTime'] + /** A paged list of tags that have this tag as their parent in the tag hierarchy. Currently only three levels are supported. */ + subTags?: Maybe + } + +/** Represents a single tag */ +export type TagSubTagsArgs = { + after?: Maybe + before?: Maybe + first?: Maybe + last?: Maybe + offset?: Maybe + sortOrder?: Maybe + sortBy?: Maybe +} + +/** Objects implementing the Deletable support soft deletion */ +export type Deletable = { + /** + * If true, this object should be considered deleted. Soft deleted objects are not + * returned in query results unless you explicitly ask for them. + */ + isDeleted: Scalars['Boolean'] +} + +/** Represents a navigation tree containing multiple levels of navigation items */ +export type NavigationTree = Node & { + __typename?: 'NavigationTree' + /** The navigation tree ID */ + _id: Scalars['ID'] + /** The draft navigation items that make up this tree */ + draftItems?: Maybe>> + /** Whether the navigation item has unpublished changes */ + hasUnpublishedChanges?: Maybe + /** The published navigation items that make up this tree */ + items?: Maybe>> + /** The name of the tree, for operator display purposes. Assumed to be in the primary shop's language */ + name: Scalars['String'] + /** The ID of the shop this navigation tree belongs to */ + shopId: Scalars['ID'] +} + +/** Represents a navigation item and its children in a tree */ +export type NavigationTreeItem = { + __typename?: 'NavigationTreeItem' + /** Whether the navigation item should display its children */ + expanded?: Maybe + /** Whether the navigation item should be hidden from customers */ + isPrivate?: Maybe + /** Whether the navigaton item is a secondary navigation item */ + isSecondary?: Maybe + /** Whether the navigation ttem should shown in query results for customers and admins */ + isVisible?: Maybe + /** The child navigation items */ + items?: Maybe>> + /** The navigation item */ + navigationItem: NavigationItem +} + +/** Represents a single navigation item */ +export type NavigationItem = Node & { + __typename?: 'NavigationItem' + /** The navigation item ID */ + _id: Scalars['ID'] + /** The date and time at which this navigation item was created */ + createdAt: Scalars['DateTime'] + /** The published data for this navigation item */ + data?: Maybe + /** The draft/unpublished data for this navigation item */ + draftData?: Maybe + /** Whether the navigation item has unpublished changes */ + hasUnpublishedChanges?: Maybe + /** An object storing additional metadata about the navigation item (such as its related tag) */ + metadata?: Maybe + /** The ID of the shop the navigation item belongs to */ + shopId: Scalars['ID'] +} + +/** Represents the data for a navigation item */ +export type NavigationItemData = { + __typename?: 'NavigationItemData' + /** CSS class names to add to the menu item for display */ + classNames?: Maybe + /** The content for the navigation item, in one or more languages */ + content?: Maybe>> + /** The translated content for a navigation item */ + contentForLanguage?: Maybe + /** Whether the provided URL is relative or external */ + isUrlRelative?: Maybe + /** Whether the navigation item should trigger a new tab/window to open when clicked */ + shouldOpenInNewWindow?: Maybe + /** The URL for the navigation item to link to */ + url?: Maybe +} + +/** Represents the translated content for a navigation item */ +export type NavigationItemContent = { + __typename?: 'NavigationItemContent' + /** The language of the piece of navigation content */ + language: Scalars['String'] + /** The translated value, in plain text or markdown */ + value?: Maybe +} + +/** + * Wraps a list of `Shops`, providing pagination cursors and information. + * + * For information about what Relay-compatible connections are and how to use them, see the following articles: + * - [Relay Connection Documentation](https://facebook.github.io/relay/docs/en/graphql-server-specification.html#connections) + * - [Relay Connection Specification](https://facebook.github.io/relay/graphql/connections.htm) + * - [Using Relay-style Connections With Apollo Client](https://www.apollographql.com/docs/react/recipes/pagination.html) + */ +export type ShopConnection = { + __typename?: 'ShopConnection' + /** The list of nodes that match the query, wrapped in an edge to provide a cursor string for each */ + edges?: Maybe>> + /** + * You can request the `nodes` directly to avoid the extra wrapping that `NodeEdge` has, + * if you know you will not need to paginate the results. + */ + nodes?: Maybe>> + /** Information to help a client request the next or previous page */ + pageInfo: PageInfo + /** The total number of nodes that match your query */ + totalCount: Scalars['Int'] +} + +/** A connection edge in which each node is an `Shop` object */ +export type ShopEdge = NodeEdge & { + __typename?: 'ShopEdge' + /** The cursor that represents this node in the paginated results */ + cursor: Scalars['ConnectionCursor'] + /** The Shop */ + node?: Maybe +} + +/** + * App settings that are not shop specific. Plugins extend the GlobalSettings type to support + * whatever settings they need. + */ +export type GlobalSettings = { + __typename?: 'GlobalSettings' + /** A fake setting necessary until some plugin extends this with a real setting */ + doNotUse?: Maybe +} + +/** + * App settings for a specific shop. Plugins extend the ShopSettings type to support + * whatever settings they need. + */ +export type ShopSettings = { + __typename?: 'ShopSettings' + /** A fake setting necessary until some plugin extends this with a real setting */ + doNotUse?: Maybe + /** + * If there is no known inventory for a product configuration, this setting determines + * whether that product configuration can be sold and should appear to be available. + */ + canSellVariantWithoutInventory: Scalars['Boolean'] + /** + * If `false` no defined shipping rates will be used when fulfillment + * quotes are requested for a cart or order. A quick way to disable the entire + * `reaction-shipping-rates` plugin temporarily. + */ + isShippingRatesFulfillmentEnabled?: Maybe + /** The default value to use for `taxCode` property of a product */ + defaultTaxCode?: Maybe + /** + * The name of the tax service to fall back to if the primary tax service is down. + * This will match the `name` field of one of the services returned by the `taxServices` + * query. + */ + fallbackTaxServiceName?: Maybe + /** + * The name of the tax service to use for calculating taxes on carts and orders. + * This will match the `name` field of one of the services returned by the `taxServices` + * query. + */ + primaryTaxServiceName?: Maybe + /** + * Whether a navigation item added to the navigation tree should be visible only to + * admins by default. + */ + shouldNavigationTreeItemsBeAdminOnly: Scalars['Boolean'] + /** + * Whether a navigation item added to the navigation tree should be + * public API/Storefront visible by default. + */ + shouldNavigationTreeItemsBePubliclyVisible: Scalars['Boolean'] + /** + * Whether a navigation item added to the navigation tree should be a secondary + * navigation item by default. + */ + shouldNavigationTreeItemsBeSecondaryNavOnly: Scalars['Boolean'] + /** This setting controls how often the sitemaps for the shop will be rebuilt */ + sitemapRefreshPeriod: Scalars['String'] +} + +/** The details of an `Address` to be created or updated */ +export type AddressInput = { + /** The street address / first line */ + address1: Scalars['String'] + /** Optional second line */ + address2?: Maybe + /** Optionally, a name for this address (e.g. 'Home') to easily identify it in the future */ + addressName?: Maybe + /** City */ + city: Scalars['String'] + /** Optional company name, if it's a business address */ + company?: Maybe + /** Country */ + country: Scalars['String'] + /** + * The first name of a person at this address + * This is an optional field to support legacy and third party platforms + * We use fullName internally, and use first and last name fields to combine into a full name if needed + */ + firstName?: Maybe + /** The full name of a person at this address */ + fullName: Scalars['String'] + /** Is this the default address for billing purposes? */ + isBillingDefault?: Maybe + /** Is this a commercial address? */ + isCommercial?: Maybe + /** Is this the default address to use when selecting a shipping address at checkout? */ + isShippingDefault?: Maybe + /** + * The last name of a person at this address + * This is an optional field to support legacy and third party platforms + * We use fullName internally, and use first and last name fields to combine into a full name if needed + */ + lastName?: Maybe + /** Arbitrary additional metadata about this address */ + metafields?: Maybe>> + /** A phone number for someone at this address */ + phone: Scalars['String'] + /** Postal code */ + postal: Scalars['String'] + /** Region. For example, a U.S. state */ + region: Scalars['String'] +} + +/** User defined attributes. You can include only `key` and use these like tags, or also include a `value`. */ +export type MetafieldInput = { + /** Field description */ + description?: Maybe + /** Field key */ + key: Scalars['String'] + /** Field namespace */ + namespace?: Maybe + /** Field scope */ + scope?: Maybe + /** Field value */ + value?: Maybe + /** Field value type */ + valueType?: Maybe +} + +/** The response from `Query.addressValidation` */ +export type AddressValidationResults = { + __typename?: 'AddressValidationResults' + /** + * A list of suggested addresses. If the address is valid as is OR the address input is invalid OR + * the shop is not configured to validate addresses, then this will be empty. + */ + suggestedAddresses: Array> + /** + * This may have information about the ways in which the provided address input is incomplete or invalid. + * Show these errors in the address review user interface. + */ + validationErrors: Array> +} + +/** An address suggestion returned from an address validation service */ +export type SuggestedAddress = { + __typename?: 'SuggestedAddress' + /** The street address / first line */ + address1: Scalars['String'] + /** Optional second line */ + address2?: Maybe + /** City */ + city: Scalars['String'] + /** Country */ + country: Scalars['String'] + /** Postal code */ + postal: Scalars['String'] + /** Region. For example, a U.S. state */ + region: Scalars['String'] +} + +/** Details about an error that was the result of validating an address that is invalid */ +export type AddressValidationError = { + __typename?: 'AddressValidationError' + /** A longer, detailed error message suitable for showing in the user interface */ + details?: Maybe + /** An identifier of the source of this error. These are not currently standardized. As long as your client understands it, any string is fine. */ + source?: Maybe + /** A short error message suitable for showing in the user interface */ + summary: Scalars['String'] + /** The error type. These are not currently standardized. As long as your client understands it, any string is fine. */ + type: Scalars['String'] +} + +/** A single registered address validation service */ +export type AddressValidationService = { + __typename?: 'AddressValidationService' + /** Human-readable name to show operators */ + displayName: Scalars['String'] + /** Unique name to serve as a key identifying this service */ + name: Scalars['String'] + /** An optional list of all country codes that this address service supports. Null means all countries. */ + supportedCountryCodes?: Maybe>> +} + +/** The fields by which you are allowed to sort any query that returns an `AddressValidationRuleConnection` */ +export enum AddressValidationRuleSortByField { + /** AddressValidationRule ID */ + Id = '_id', + /** Date and time at which the rule was created */ + CreatedAt = 'createdAt', + /** Service name */ + ServiceName = 'serviceName', + /** Date and time at which the rule was last updated */ + UpdatedAt = 'updatedAt', +} + +/** + * Wraps a list of `AddressValidationRules`, providing pagination cursors and information. + * + * For information about what Relay-compatible connections are and how to use them, see the following articles: + * - [Relay Connection Documentation](https://facebook.github.io/relay/docs/en/graphql-server-specification.html#connections) + * - [Relay Connection Specification](https://facebook.github.io/relay/graphql/connections.htm) + * - [Using Relay-style Connections With Apollo Client](https://www.apollographql.com/docs/react/recipes/pagination.html) + */ +export type AddressValidationRuleConnection = { + __typename?: 'AddressValidationRuleConnection' + /** The list of nodes that match the query, wrapped in an edge to provide a cursor string for each */ + edges?: Maybe>> + /** + * You can request the `nodes` directly to avoid the extra wrapping that `NodeEdge` has, + * if you know you will not need to paginate the results. + */ + nodes?: Maybe>> + /** Information to help a client request the next or previous page */ + pageInfo: PageInfo + /** The total number of nodes that match your query */ + totalCount: Scalars['Int'] +} + +/** A connection edge in which each node is a `AddressValidationRule` object */ +export type AddressValidationRuleEdge = NodeEdge & { + __typename?: 'AddressValidationRuleEdge' + /** The cursor that represents this node in the paginated results */ + cursor: Scalars['ConnectionCursor'] + /** The address validation rule */ + node?: Maybe +} + +/** + * An address validation rule specifies which validation services should run for + * which countries in each shop. + */ +export type AddressValidationRule = Node & { + __typename?: 'AddressValidationRule' + /** The rule ID */ + _id: Scalars['ID'] + /** Country codes for which this service is enabled */ + countryCodes?: Maybe>> + /** The date and time at which this rule was created */ + createdAt: Scalars['DateTime'] + /** + * The name of one of the installed validation services. Use `addressValidationServices` + * query to get a list with more details about all installed services. + */ + serviceName: Scalars['String'] + /** ID of the shop to which this rule applies */ + shopId: Scalars['ID'] + /** The date and time at which this rule was last updated */ + updatedAt: Scalars['DateTime'] +} + +/** Represents Reaction System Infomation */ +export type SystemInformation = { + __typename?: 'SystemInformation' + /** Core api version */ + apiVersion: Scalars['String'] + /** Mongo version */ + mongoVersion: DatabaseInformation + /** Plugins installed with name, version information */ + plugins?: Maybe>> +} + +/** Represents Mongo Database information */ +export type DatabaseInformation = { + __typename?: 'DatabaseInformation' + /** Version of database */ + version: Scalars['String'] +} + +/** Represents Reaction Plugin */ +export type Plugin = { + __typename?: 'Plugin' + /** Name of plugin */ + name: Scalars['String'] + /** Version of plugin */ + version?: Maybe +} + +/** + * Wraps a list of Templates, providing pagination cursors and information. + * + * For information about what Relay-compatible connections are and how to use them, see the following articles: + * - [Relay Connection Documentation](https://facebook.github.io/relay/docs/en/graphql-server-specification.html#connections) + * - [Relay Connection Specification](https://facebook.github.io/relay/graphql/connections.htm) + * - [Using Relay-style Connections With Apollo Client](https://www.apollographql.com/docs/react/recipes/pagination.html) + */ +export type TemplateConnection = { + __typename?: 'TemplateConnection' + /** The list of nodes that match the query, wrapped in an edge to provide a cursor string for each */ + edges?: Maybe>> + /** + * You can request the `nodes` directly to avoid the extra wrapping that `NodeEdge` has, + * if you know you will not need to paginate the results. + */ + nodes?: Maybe>> + /** Information to help a client request the next or previous page */ + pageInfo: PageInfo + /** The total number of nodes that match your query */ + totalCount: Scalars['Int'] +} + +/** A connection edge in which each node is a `Template` object */ +export type TemplateEdge = { + __typename?: 'TemplateEdge' + /** The cursor that represents this node in the paginated results */ + cursor: Scalars['ConnectionCursor'] + /** The email template */ + node?: Maybe