diff --git a/.env.template b/.env.template index 9b45afe4b..9e42e2f31 100644 --- a/.env.template +++ b/.env.template @@ -1,3 +1,6 @@ +# Available providers: bigcommerce, shopify +COMMERCE_PROVIDER=bigcommerce + BIGCOMMERCE_STOREFRONT_API_URL= BIGCOMMERCE_STOREFRONT_API_TOKEN= BIGCOMMERCE_STORE_API_URL= @@ -5,5 +8,5 @@ BIGCOMMERCE_STORE_API_TOKEN= BIGCOMMERCE_STORE_API_CLIENT_ID= BIGCOMMERCE_CHANNEL_ID= -SHOPIFY_STORE_DOMAIN= -SHOPIFY_STOREFRONT_ACCESS_TOKEN= +NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN= +NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN= diff --git a/README.md b/README.md index 885c95e85..941b1699b 100644 --- a/README.md +++ b/README.md @@ -29,67 +29,16 @@ Next.js Commerce integrates out-of-the-box with BigCommerce and Shopify. We plan ## Considerations - `framework/commerce` contains all types, helpers and functions to be used as base to build a new **provider**. -- **Providers** live under `framework`'s root folder and they will extend Next.js Commerce types and functionality. -- **Features API** is to ensure feature parity between the UI and the Provider. The UI should update accordingly and no extra code should be bundled. All extra configuration for features will live under `features` in `commerce.config.json` and if needed it can also be accessed programatically. +- **Providers** live under `framework`'s root folder and they will extend Next.js Commerce types and functionality (`framework/commerce`). +- We have a **Features API** to ensure feature parity between the UI and the Provider. The UI should update accordingly and no extra code should be bundled. All extra configuration for features will live under `features` in `commerce.config.json` and if needed it can also be accessed programatically. - Each **provider** should add its corresponding `next.config.js` and `commerce.config.json` adding specific data related to the provider. For example in case of BigCommerce, the images CDN and additional API routes. - **Providers don't depend on anything that's specific to the application they're used in**. They only depend on `framework/commerce`, on their own framework folder and on some dependencies included in `package.json` -- We recommend that each **provider** ships with an `env.template` file and a `[readme.md](http://readme.md)` file. - -## Provider Structure - -Next.js Commerce provides a set of utilities and functions to create new providers. This is how a provider structure looks like. - -- `product` - - usePrice - - useSearch - - getProduct - - getAllProducts -- `wishlist` - - useWishlist - - useAddItem - - useRemoveItem -- `auth` - - useLogin - - useLogout - - useSignup -- `customer` - - useCustomer - - getCustomerId - - getCustomerWistlist -- `cart` - - useCart - - useAddItem - - useRemoveItem - - useUpdateItem -- `env.template` -- `provider.ts` -- `commerce.config.json` -- `next.config.js` -- `README.md` ## Configuration ### How to change providers -First, update the provider selected in `commerce.config.json`: - -```json -{ - "provider": "bigcommerce", - "features": { - "wishlist": true - } -} -``` - -Then, change the paths defined in `tsconfig.json` and update the `@framework` paths to point to the right folder provider: - -```json -"@framework": ["framework/bigcommerce"], -"@framework/*": ["framework/bigcommerce/*"] -``` - -Make sure to add the environment variables required by the new provider. +Open `.env.local` and change the value of `COMMERCE_PROVIDER` to the provider you would like to use, then set the environment variables for that provider (use `.env.template` as the base). ### Features @@ -103,7 +52,6 @@ Every provider defines the features that it supports under `framework/{provider} - You'll see a config file like this: ```json { - "provider": "bigcommerce", "features": { "wishlist": false } @@ -114,15 +62,9 @@ Every provider defines the features that it supports under `framework/{provider} ### How to create a new provider -We'd recommend to duplicate a provider folder and push your providers SDK. +Follow our docs for [Adding a new Commerce Provider](framework/commerce/new-provider.md). -If you succeeded building a provider, submit a PR so we can all enjoy it. - -## Work in progress - -We're using Github Projects to keep track of issues in progress and todo's. Here is our [Board](https://github.com/vercel/commerce/projects/1) - -People actively working on this project: @okbel & @lfades. +If you succeeded building a provider, submit a PR with a valid demo and we'll review it asap. ## Contribute @@ -132,11 +74,15 @@ Our commitment to Open Source can be found [here](https://vercel.com/oss). 2. Create a new branch `git checkout -b MY_BRANCH_NAME` 3. Install yarn: `npm install -g yarn` 4. Install the dependencies: `yarn` -5. Duplicate `.env.template` and rename it to `.env.local`. -6. Add proper store values to `.env.local`. +5. Duplicate `.env.template` and rename it to `.env.local` +6. Add proper store values to `.env.local` 7. Run `yarn dev` to build and watch for code changes -8. The development branch is `canary` (this is the branch pull requests should be made against). - On a release, `canary` branch is rebased into `master`. + +## Work in progress + +We're using Github Projects to keep track of issues in progress and todo's. Here is our [Board](https://github.com/vercel/commerce/projects/1) + +People actively working on this project: @okbel & @lfades. ## Troubleshoot diff --git a/components/cart/CartItem/CartItem.tsx b/components/cart/CartItem/CartItem.tsx index 9343d1ecf..8bb5201c0 100644 --- a/components/cart/CartItem/CartItem.tsx +++ b/components/cart/CartItem/CartItem.tsx @@ -93,15 +93,18 @@ const CartItem = ({ })} {...rest} > -
- {item.variant.image!.altText} +
+ + closeSidebarIfPresent()} + className={s.productImage} + width={150} + height={150} + src={item.variant.image!.url} + alt={item.variant.image!.altText} + unoptimized + /> +
diff --git a/components/product/ProductView/ProductView.tsx b/components/product/ProductView/ProductView.tsx index 9a75ef2b1..072f6e298 100644 --- a/components/product/ProductView/ProductView.tsx +++ b/components/product/ProductView/ProductView.tsx @@ -1,23 +1,20 @@ import cn from 'classnames' import Image from 'next/image' import { NextSeo } from 'next-seo' -import { FC, useState } from 'react' +import { FC, useEffect, useState } from 'react' import s from './ProductView.module.css' - import { Swatch, ProductSlider } from '@components/product' import { Button, Container, Text, useUI } from '@components/ui' - import type { Product } from '@commerce/types' import usePrice from '@framework/product/use-price' import { useAddItem } from '@framework/cart' - import { getVariant, SelectedOptions } from '../helpers' import WishlistButton from '@components/wishlist/WishlistButton' interface Props { - className?: string children?: any product: Product + className?: string } const ProductView: FC = ({ product }) => { @@ -29,11 +26,18 @@ const ProductView: FC = ({ product }) => { }) const { openSidebar } = useUI() const [loading, setLoading] = useState(false) - const [choices, setChoices] = useState({ - color: null, - }) + const [choices, setChoices] = useState({}) + + useEffect(() => { + // Selects the default option + product.variants[0].options?.forEach((v) => { + setChoices((choices) => ({ + ...choices, + [v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(), + })) + }) + }, []) - // Select the correct variant based on choices const variant = getVariant(product, choices) const addToCart = async () => { @@ -140,7 +144,7 @@ const ProductView: FC = ({ product }) => { ))}
- +
@@ -150,7 +154,6 @@ const ProductView: FC = ({ product }) => { className={s.button} onClick={addToCart} loading={loading} - disabled={!variant && product.options.length > 0} > Add to Cart diff --git a/components/product/helpers.ts b/components/product/helpers.ts index 029476c92..a0ceb7aa5 100644 --- a/components/product/helpers.ts +++ b/components/product/helpers.ts @@ -1,9 +1,5 @@ import type { Product } from '@commerce/types' - -export type SelectedOptions = { - size: string | null - color: string | null -} +export type SelectedOptions = Record export function getVariant(product: Product, opts: SelectedOptions) { const variant = product.variants.find((variant) => { diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md deleted file mode 100644 index 437766dd8..000000000 --- a/docs/ROADMAP.md +++ /dev/null @@ -1 +0,0 @@ -# Roadmap diff --git a/framework/bigcommerce/.env.template b/framework/bigcommerce/.env.template index 43e85c046..2b91bc095 100644 --- a/framework/bigcommerce/.env.template +++ b/framework/bigcommerce/.env.template @@ -1,3 +1,5 @@ +COMMERCE_PROVIDER=bigcommerce + BIGCOMMERCE_STOREFRONT_API_URL= BIGCOMMERCE_STOREFRONT_API_TOKEN= BIGCOMMERCE_STORE_API_URL= diff --git a/framework/bigcommerce/README.md b/framework/bigcommerce/README.md index 2609b1544..7f62a5f3f 100644 --- a/framework/bigcommerce/README.md +++ b/framework/bigcommerce/README.md @@ -1,45 +1,34 @@ -# Table of Contents +# Bigcommerce Provider -- [BigCommerce Storefront Data Hooks](#bigcommerce-storefront-data-hooks) - - [Installation](#installation) - - [General Usage](#general-usage) - - [CommerceProvider](#commerceprovider) - - [useLogin hook](#uselogin-hook) - - [useLogout](#uselogout) - - [useCustomer](#usecustomer) - - [useSignup](#usesignup) - - [usePrice](#useprice) - - [Cart Hooks](#cart-hooks) - - [useCart](#usecart) - - [useAddItem](#useadditem) - - [useUpdateItem](#useupdateitem) - - [useRemoveItem](#useremoveitem) - - [Wishlist Hooks](#wishlist-hooks) - - [Product Hooks and API](#product-hooks-and-api) - - [useSearch](#usesearch) - - [getAllProducts](#getallproducts) - - [getProduct](#getproduct) - - [More](#more) +**Demo:** https://bigcommerce.demo.vercel.store/ -# BigCommerce Storefront Data Hooks +With the deploy button below you'll be able to have a [BigCommerce](https://www.bigcommerce.com/) account and a store that works with this starter: -> This project is under active development, new features and updates will be continuously added over time +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fcommerce&project-name=commerce&repo-name=commerce&demo-title=Next.js%20Commerce&demo-description=An%20all-in-one%20starter%20kit%20for%20high-performance%20e-commerce%20sites.&demo-url=https%3A%2F%2Fdemo.vercel.store&demo-image=https%3A%2F%2Fbigcommerce-demo-asset-ksvtgfvnd.vercel.app%2Fbigcommerce.png&integration-ids=oac_MuWZiE4jtmQ2ejZQaQ7ncuDT) -UI hooks and data fetching methods built from the ground up for e-commerce applications written in React, that use BigCommerce as a headless e-commerce platform. The package provides: +If you already have a BigCommerce account and want to use your current store, then copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git): -- Code splitted hooks for data fetching using [SWR](https://swr.vercel.app/), and to handle common user actions -- Code splitted data fetching methods for initial data population and static generation of content -- Helpers to create the API endpoints that connect to the hooks, very well suited for Next.js applications - -## Installation - -To install: - -``` -yarn add storefront-data-hooks +```bash +cp framework/bigcommerce/.env.template .env.local ``` -After install, the first thing you do is: set your environment variables in `.env.local` +Then, set the environment variables in `.env.local` to match the ones from your store. + +## Contribute + +Our commitment to Open Source can be found [here](https://vercel.com/oss). + +If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues). + +## Troubleshoot + +
+I already own a BigCommerce store. What should I do? +
+First thing you do is: set your environment variables +
+
+.env.local ```sh BIGCOMMERCE_STOREFRONT_API_URL=<> @@ -50,331 +39,21 @@ BIGCOMMERCE_STORE_API_CLIENT_ID=<> BIGCOMMERCE_CHANNEL_ID=<> ``` -## General Usage +If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials. -### CommerceProvider +1. Install Vercel CLI: `npm i -g vercel` +2. Link local instance with Vercel and Github accounts (creates .vercel file): `vercel link` +3. Download your environment variables: `vercel env pull .env.local` -This component is a provider pattern component that creates commerce context for it's children. It takes config values for the locale and an optional `fetcherRef` object for data fetching. +Next, you're free to customize the starter. More updates coming soon. Stay tuned. -```jsx -... -import { CommerceProvider } from '@bigcommerce/storefront-data-hooks' +
-const App = ({ locale = 'en-US', children }) => { - return ( - - {children} - - ) -} -... -``` - -### useLogin hook - -Hook for bigcommerce user login functionality, returns `login` function to handle user login. - -```jsx -... -import useLogin from '@bigcommerce/storefront-data-hooks/use-login' - -const LoginView = () => { - const login = useLogin() - - const handleLogin = async () => { - await login({ - email, - password, - }) - } - - return ( -
- {children} -
- ) -} -... -``` - -### useLogout - -Hook to logout user. - -```jsx -... -import useLogout from '@bigcommerce/storefront-data-hooks/use-logout' - -const LogoutLink = () => { - const logout = useLogout() - return ( - logout()}> - Logout - - ) -} -``` - -### useCustomer - -Hook for getting logged in customer data, and fetching customer info. - -```jsx -... -import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer' -... - -const Profile = () => { - const { data } = useCustomer() - - if (!data) { - return null - } - - return ( -
Hello, {data.firstName}
- ) -} -``` - -### useSignup - -Hook for bigcommerce user signup, returns `signup` function to handle user signups. - -```jsx -... -import useSignup from '@bigcommerce/storefront-data-hooks/use-login' - -const SignupView = () => { - const signup = useSignup() - - const handleSignup = async () => { - await signup({ - email, - firstName, - lastName, - password, - }) - } - - return ( -
- {children} -
- ) -} -... -``` - -### usePrice - -Helper hook to format price according to commerce locale, and return discount if available. - -```jsx -import usePrice from '@bigcommerce/storefront-data-hooks/use-price' -... - const { price, discount, basePrice } = usePrice( - data && { - amount: data.cart_amount, - currencyCode: data.currency.code, - } - ) -... -``` - -## Cart Hooks - -### useCart - -Returns the current cart data for use - -```jsx -... -import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart' - -const countItem = (count: number, item: LineItem) => count + item.quantity - -const CartNumber = () => { - const { data } = useCart() - const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0 - - return itemsCount > 0 ? {itemsCount} : null -} -``` - -### useAddItem - -```jsx -... -import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item' - -const AddToCartButton = ({ productId, variantId }) => { - const addItem = useAddItem() - - const addToCart = async () => { - await addItem({ - productId, - variantId, - }) - } - - return -} -... -``` - -### useUpdateItem - -```jsx -... -import useUpdateItem from '@bigcommerce/storefront-data-hooks/cart/use-update-item' - -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 ( - - ) -} -... -``` - -### useRemoveItem - -Provided with a cartItemId, will remove an item from the cart: - -```jsx -... -import useRemoveItem from '@bigcommerce/storefront-data-hooks/cart/use-remove-item' - -const RemoveButton = ({ item }) => { - const removeItem = useRemoveItem() - - const handleRemove = async () => { - await removeItem({ id: item.id }) - } - - return -} -... -``` - -## Wishlist Hooks - -Wishlist hooks are similar to cart hooks. See the below example for how to use `useWishlist`, `useAddItem`, and `useRemoveItem`. - -```jsx -import useAddItem from '@bigcommerce/storefront-data-hooks/wishlist/use-add-item' -import useRemoveItem from '@bigcommerce/storefront-data-hooks/wishlist/use-remove-item' -import useWishlist from '@bigcommerce/storefront-data-hooks/wishlist/use-wishlist' - -const WishlistButton = ({ productId, variant }) => { - const addItem = useAddItem() - const removeItem = useRemoveItem() - const { data } = useWishlist() - const { data: customer } = useCustomer() - const itemInWishlist = data?.items?.find( - (item) => - item.product_id === productId && - item.variant_id === variant?.node.entityId - ) - - const handleWishlistChange = async (e) => { - e.preventDefault() - - if (!customer) { - return - } - - if (itemInWishlist) { - await removeItem({ id: itemInWishlist.id! }) - } else { - await addItem({ - productId, - variantId: variant?.node.entityId!, - }) - } - } - - return ( - - ) -} -``` - -## Product Hooks and API - -### useSearch - -`useSearch` handles searching the bigcommerce storefront product catalog by catalog, brand, and query string. - -```jsx -... -import useSearch from '@bigcommerce/storefront-data-hooks/products/use-search' - -const SearchPage = ({ searchString, category, brand, sortStr }) => { - const { data } = useSearch({ - search: searchString || '', - categoryId: category?.entityId, - brandId: brand?.entityId, - sort: sortStr || '', - }) - - return ( - - {data.products.map(({ node }) => ( - - ))} - - ) -} -``` - -### getAllProducts - -API function to retrieve a product list. - -```js -import { getConfig } from '@bigcommerce/storefront-data-hooks/api' -import getAllProducts from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products' - -const { products } = await getAllProducts({ - variables: { field: 'featuredProducts', first: 6 }, - config, - preview, -}) -``` - -### getProduct - -API product to retrieve a single product when provided with the product -slug string. - -```js -import { getConfig } from '@bigcommerce/storefront-data-hooks/api' -import getProduct from '@bigcommerce/storefront-data-hooks/api/operations/get-product' - -const { product } = await getProduct({ - variables: { slug }, - config, - preview, -}) -``` - -## More - -Feel free to read through the source for more usage, and check the commerce vercel demo and commerce repo for usage examples: ([demo.vercel.store](https://demo.vercel.store/)) ([repo](https://github.com/vercel/commerce)) +
+BigCommerce shows a Coming Soon page and requests a Preview Code +
+After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard. +
+
+BigCommerce team has been notified and they plan to add more detailed about this subject. +
diff --git a/framework/commerce/README.md b/framework/commerce/README.md new file mode 100644 index 000000000..ecdebb8c0 --- /dev/null +++ b/framework/commerce/README.md @@ -0,0 +1,334 @@ +# Commerce Framework + +- [Commerce Framework](#commerce-framework) + - [Commerce Hooks](#commerce-hooks) + - [CommerceProvider](#commerceprovider) + - [Authentication Hooks](#authentication-hooks) + - [useSignup](#usesignup) + - [useLogin](#uselogin) + - [useLogout](#uselogout) + - [Customer Hooks](#customer-hooks) + - [useCustomer](#usecustomer) + - [Product Hooks](#product-hooks) + - [usePrice](#useprice) + - [useSearch](#usesearch) + - [Cart Hooks](#cart-hooks) + - [useCart](#usecart) + - [useAddItem](#useadditem) + - [useUpdateItem](#useupdateitem) + - [useRemoveItem](#useremoveitem) + - [Wishlist Hooks](#wishlist-hooks) + - [Commerce API](#commerce-api) + - [More](#more) + +The commerce framework ships multiple hooks and a Node.js API, both using an underlying headless e-commerce platform, which we call commerce providers. + +The core features are: + +- Code splitted hooks for data fetching using [SWR](https://swr.vercel.app/), and to handle common user actions +- A Node.js API for initial data population, static generation of content and for creating the API endpoints that connect to the hooks, if required. + +> 👩‍🔬 If you would like to contribute a new provider, check the docs for [Adding a new Commerce Provider](./new-provider.md). + +> 🚧 The core commerce framework is under active development, new features and updates will be continuously added over time. Breaking changes are expected while we finish the API. + +## Commerce Hooks + +A commerce hook is a [React hook](https://reactjs.org/docs/hooks-intro.html) that's connected to a commerce provider. They focus on user actions and data fetching of data that wasn't statically generated. + +Data fetching hooks use [SWR](https://swr.vercel.app/) underneath and you're welcome to use any of its [return values](https://swr.vercel.app/docs/options#return-values) and [options](https://swr.vercel.app/docs/options#options). For example, using the `useCustomer` hook: + +```jsx +const { data, isLoading, error } = useCustomer({ + swrOptions: { + revalidateOnFocus: true, + }, +}) +``` + +### CommerceProvider + +This component adds the provider config and handlers to the context of your React tree for it's children. You can optionally pass the `locale` to it: + +```jsx +import { CommerceProvider } from '@framework' + +const App = ({ locale = 'en-US', children }) => { + return {children} +} +``` + +## Authentication Hooks + +### useSignup + +Returns a _signup_ function that can be used to sign up the current visitor: + +```jsx +import useSignup from '@framework/auth/use-signup' + +const SignupView = () => { + const signup = useSignup() + + const handleSignup = async () => { + await signup({ + email, + firstName, + lastName, + password, + }) + } + + return
{children}
+} +``` + +### useLogin + +Returns a _login_ function that can be used to sign in the current visitor into an existing customer: + +```jsx +import useLogin from '@framework/auth/use-login' + +const LoginView = () => { + const login = useLogin() + const handleLogin = async () => { + await login({ + email, + password, + }) + } + + return
{children}
+} +``` + +### useLogout + +Returns a _logout_ function that signs out the current customer when called. + +```jsx +import useLogout from '@framework/auth/use-logout' + +const LogoutButton = () => { + const logout = useLogout() + return ( + + ) +} +``` + +## Customer Hooks + +### useCustomer + +Fetches and returns the data of the signed in customer: + +```jsx +import useCustomer from '@framework/customer/use-customer' + +const Profile = () => { + const { data, isLoading, error } = useCustomer() + + if (isLoading) return

Loading...

+ if (error) return

{error.message}

+ if (!data) return null + + return
Hello, {data.firstName}
+} +``` + +## Product Hooks + +### usePrice + +Helper hook to format price according to the commerce locale and currency code. It also handles discounts: + +```jsx +import useCart from '@framework/cart/use-cart' +import usePrice from '@framework/product/use-price' + +// ... +const { data } = useCart() +const { price, discount, basePrice } = usePrice( + data && { + amount: data.subtotalPrice, + currencyCode: data.currency.code, + // If `baseAmount` is used, a discount will be calculated + // baseAmount: number, + } +) +// ... +``` + +### useSearch + +Fetches and returns the products that match a set of filters: + +```jsx +import useSearch from '@framework/product/use-search' + +const SearchPage = ({ searchString, category, brand, sortStr }) => { + const { data } = useSearch({ + search: searchString || '', + categoryId: category?.entityId, + brandId: brand?.entityId, + sort: sortStr, + }) + + return ( + + {data.products.map((product) => ( + + ))} + + ) +} +``` + +## Cart Hooks + +### useCart + +Fetches and returns the data of the current cart: + +```jsx +import useCart from '@framework/cart/use-cart' + +const CartTotal = () => { + const { data, isLoading, isEmpty, error } = useCart() + + if (isLoading) return

Loading...

+ if (error) return

{error.message}

+ if (isEmpty) return

The cart is empty

+ + return

The cart total is {data.totalPrice}

+} +``` + +### useAddItem + +Returns a function that adds a new item to the cart when called, if this is the first item it will create the cart: + +```jsx +import { useAddItem } from '@framework/cart' + +const AddToCartButton = ({ productId, variantId }) => { + const addItem = useAddItem() + + const addToCart = async () => { + await addItem({ + productId, + variantId, + }) + } + + return +} +``` + +### useUpdateItem + +Returns a function that updates a current item in the cart when called, usually the quantity. + +```jsx +import { useUpdateItem } from '@framework/cart' + +const CartItemQuantity = ({ item }) => { + const [quantity, setQuantity] = useState(item.quantity) + const updateItem = useUpdateItem({ item }) + + const updateQuantity = async (e) => { + const val = e.target.value + + setQuantity(val) + await updateItem({ quantity: val }) + } + + return ( + + ) +} +``` + +If the `quantity` is lower than 1 the item will be removed from the cart. + +### useRemoveItem + +Returns a function that removes an item in the cart when called: + +```jsx +import { useRemoveItem } from '@framework/cart' + +const RemoveButton = ({ item }) => { + const removeItem = useRemoveItem() + const handleRemove = async () => { + await removeItem(item) + } + + return +} +``` + +## Wishlist Hooks + +Wishlist hooks work just like [cart hooks](#cart-hooks). Feel free to check how those work first. + +The example below shows how to use the `useWishlist`, `useAddItem` and `useRemoveItem` hooks: + +```jsx +import { useWishlist, useAddItem, useRemoveItem } from '@framework/wishlist' + +const WishlistButton = ({ productId, variant }) => { + const addItem = useAddItem() + const removeItem = useRemoveItem() + const { data, isLoading, isEmpty, error } = useWishlist() + + if (isLoading) return

Loading...

+ if (error) return

{error.message}

+ if (isEmpty) return

The wihslist is empty

+ + const { data: customer } = useCustomer() + const itemInWishlist = data?.items?.find( + (item) => item.product_id === productId && item.variant_id === variant.id + ) + + const handleWishlistChange = async (e) => { + e.preventDefault() + if (!customer) return + + if (itemInWishlist) { + await removeItem({ id: itemInWishlist.id }) + } else { + await addItem({ + productId, + variantId: variant.id, + }) + } + } + + return ( + + ) +} +``` + +## Commerce API + +While commerce hooks focus on client side data fetching and interactions, the commerce API focuses on static content generation for pages and API endpoints in a Node.js context. + +> The commerce API is currently going through a refactor in https://github.com/vercel/commerce/pull/252 - We'll update the docs once the API is released. + +## More + +Feel free to read through the source for more usage, and check the commerce vercel demo and commerce repo for usage examples: ([demo.vercel.store](https://demo.vercel.store/)) ([repo](https://github.com/vercel/commerce)) diff --git a/framework/commerce/config.js b/framework/commerce/config.js new file mode 100644 index 000000000..ffc76ba2a --- /dev/null +++ b/framework/commerce/config.js @@ -0,0 +1,66 @@ +/** + * This file is expected to be used in next.config.js only + */ + +const path = require('path') +const fs = require('fs') +const merge = require('deepmerge') +const prettier = require('prettier') + +const PROVIDERS = ['bigcommerce', 'reactioncommerce', 'shopify'] + +function getProviderName() { + return ( + process.env.COMMERCE_PROVIDER || + (process.env.BIGCOMMERCE_STOREFRONT_API_URL + ? 'bigcommerce' + : process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN + ? 'shopify' + : null) + ) +} + +function withCommerceConfig(nextConfig = {}) { + const commerce = nextConfig.commerce || {} + const name = commerce.provider || getProviderName() + + if (!name) { + throw new Error( + `The commerce provider is missing, please add a valid provider name or its environment variables` + ) + } + if (!PROVIDERS.includes(name)) { + throw new Error( + `The commerce provider "${name}" can't be found, please use one of "${PROVIDERS.join( + ', ' + )}"` + ) + } + + const commerceNextConfig = require(path.join('../', name, 'next.config')) + const config = merge(commerceNextConfig, nextConfig) + + config.env = config.env || {} + + Object.entries(config.commerce.features).forEach(([k, v]) => { + if (v) config.env[`COMMERCE_${k.toUpperCase()}_ENABLED`] = true + }) + + // Update paths in `tsconfig.json` to point to the selected provider + if (config.commerce.updateTSConfig !== false) { + const tsconfigPath = path.join(process.cwd(), 'tsconfig.json') + const tsconfig = require(tsconfigPath) + + tsconfig.compilerOptions.paths['@framework'] = [`framework/${name}`] + tsconfig.compilerOptions.paths['@framework/*'] = [`framework/${name}/*`] + + fs.writeFileSync( + tsconfigPath, + prettier.format(JSON.stringify(tsconfig), { parser: 'json' }) + ) + } + + return config +} + +module.exports = { withCommerceConfig, getProviderName } diff --git a/framework/commerce/new-provider.md b/framework/commerce/new-provider.md new file mode 100644 index 000000000..4051c0f01 --- /dev/null +++ b/framework/commerce/new-provider.md @@ -0,0 +1,239 @@ +# Adding a new Commerce Provider + +A commerce provider is a headless e-commerce platform that integrates with the [Commerce Framework](./README.md). Right now we have the following providers: + +- BigCommerce ([framework/bigcommerce](../bigcommerce)) +- Shopify ([framework/shopify](../shopify)) + +Adding a commerce provider means adding a new folder in `framework` with a folder structure like the next one: + +- `api` + - index.ts +- `product` + - usePrice + - useSearch + - getProduct + - getAllProducts +- `wishlist` + - useWishlist + - useAddItem + - useRemoveItem +- `auth` + - useLogin + - useLogout + - useSignup +- `customer` + - useCustomer + - getCustomerId + - getCustomerWistlist +- `cart` + - useCart + - useAddItem + - useRemoveItem + - useUpdateItem +- `env.template` +- `index.ts` +- `provider.ts` +- `commerce.config.json` +- `next.config.js` +- `README.md` + +`provider.ts` exports a provider object with handlers for the [Commerce Hooks](./README.md#commerce-hooks) and `api/index.ts` exports a Node.js provider for the [Commerce API](./README.md#commerce-api) + +> **Important:** We use TypeScript for every provider and expect its usage for every new one. + +The app imports from the provider directly instead of the core commerce folder (`framework/commerce`), but all providers are interchangeable and to achieve it every provider always has to implement the core types and helpers. + +The provider folder should only depend on `framework/commerce` and dependencies in the main `package.json`. In the future we'll move the `framework` folder to a package that can be shared easily for multiple apps. + +## Adding the provider hooks + +Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks: + +```tsx +import type { ReactNode } from 'react' +import { + CommerceConfig, + CommerceProvider as CoreCommerceProvider, + useCommerce as useCoreCommerce, +} from '@commerce' +import { bigcommerceProvider, BigcommerceProvider } from './provider' + +export { bigcommerceProvider } +export type { BigcommerceProvider } + +export const bigcommerceConfig: CommerceConfig = { + locale: 'en-us', + cartCookie: 'bc_cartId', +} + +export type BigcommerceConfig = Partial + +export type BigcommerceProps = { + children?: ReactNode + locale: string +} & BigcommerceConfig + +export function CommerceProvider({ children, ...config }: BigcommerceProps) { + return ( + + {children} + + ) +} + +export const useCommerce = () => useCoreCommerce() +``` + +The exported types and components extend from the core ones exported by `@commerce`, which refers to `framework/commerce`. + +The `bigcommerceProvider` object looks like this: + +```tsx +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 useWishlist } from './wishlist/use-wishlist' +import { handler as useWishlistAddItem } from './wishlist/use-add-item' +import { handler as useWishlistRemoveItem } from './wishlist/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 bigcommerceProvider = { + locale: 'en-us', + cartCookie: 'bc_cartId', + fetcher, + cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, + wishlist: { + useWishlist, + useAddItem: useWishlistAddItem, + useRemoveItem: useWishlistRemoveItem, + }, + customer: { useCustomer }, + products: { useSearch }, + auth: { useLogin, useLogout, useSignup }, +} + +export type BigcommerceProvider = typeof bigcommerceProvider +``` + +The provider object, in this case `bigcommerceProvider`, has to match the `Provider` type defined in [framework/commerce](./index.ts). + +A hook handler, like `useCart`, looks like this: + +```tsx +import { useMemo } from 'react' +import { SWRHook } from '@commerce/utils/types' +import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart' +import { normalizeCart } from '../lib/normalize' +import type { Cart } from '../types' + +export default useCart as UseCart + +export const handler: SWRHook< + Cart | null, + {}, + FetchCartInput, + { isEmpty?: boolean } +> = { + fetchOptions: { + url: '/api/bigcommerce/cart', + method: 'GET', + }, + async fetcher({ input: { cartId }, options, fetch }) { + const data = cartId ? await fetch(options) : null + return data && normalizeCart(data) + }, + useHook: ({ useData }) => (input) => { + const response = useData({ + swrOptions: { revalidateOnFocus: false, ...input?.swrOptions }, + }) + + return useMemo( + () => + Object.create(response, { + isEmpty: { + get() { + return (response.data?.lineItems.length ?? 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, +} +``` + +In the case of data fetching hooks like `useCart` each handler has to implement the `SWRHook` type that's defined in the core types. For mutations it's the `MutationHook`, e.g for `useAddItem`: + +```tsx +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import { CommerceError } from '@commerce/utils/errors' +import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item' +import { normalizeCart } from '../lib/normalize' +import type { + Cart, + BigcommerceCart, + CartItemBody, + AddCartItemBody, +} from '../types' +import useCart from './use-cart' + +export default useAddItem as UseAddItem + +export const handler: MutationHook = { + fetchOptions: { + url: '/api/bigcommerce/cart', + method: 'POST', + }, + async fetcher({ input: item, options, fetch }) { + 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 normalizeCart(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] + ) + }, +} +``` + +## Adding the Node.js provider API + +TODO + +> The commerce API is currently going through a refactor in https://github.com/vercel/commerce/pull/252 - We'll update the docs once the API is released. diff --git a/framework/commerce/types.ts b/framework/commerce/types.ts index a398070ac..86361fd9f 100644 --- a/framework/commerce/types.ts +++ b/framework/commerce/types.ts @@ -163,6 +163,7 @@ interface Entity { export interface Product extends Entity { name: string description: string + descriptionHtml?: string slug?: string path?: string images: ProductImage[] diff --git a/framework/commerce/with-config.js b/framework/commerce/with-config.js deleted file mode 100644 index bb34d5ac8..000000000 --- a/framework/commerce/with-config.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * This file is expected to be used in next.config.js only - */ - -const merge = require('deepmerge') - -const PROVIDERS = ['bigcommerce', 'shopify', 'reactioncommerce'] - -function getProviderName() { - // TODO: OSOT. - return process.env.BIGCOMMERCE_STOREFRONT_API_URL ? 'bigcommerce' : null -} - -module.exports = (nextConfig = {}) => { - const commerce = nextConfig.commerce || {} - const name = commerce.provider || getProviderName() - - if (!name) { - throw new Error( - `The commerce provider is missing, please add a valid provider name or its environment variables` - ) - } - if (!PROVIDERS.includes(name)) { - throw new Error( - `The commerce provider "${name}" can't be found, please use one of "${PROVIDERS.join( - ', ' - )}"` - ) - } - - const commerceNextConfig = require(`../${name}/next.config`) - const config = merge(commerceNextConfig, nextConfig) - - config.env = config.env || {} - - Object.entries(config.commerce.features).forEach(([k, v]) => { - if (v) config.env[`COMMERCE_${k.toUpperCase()}_ENABLED`] = true - }) - - return config -} diff --git a/framework/reactioncommerce/api/cart/handlers/add-item.ts b/framework/reactioncommerce/api/cart/handlers/add-item.ts index c8ccb36af..8d17b8458 100644 --- a/framework/reactioncommerce/api/cart/handlers/add-item.ts +++ b/framework/reactioncommerce/api/cart/handlers/add-item.ts @@ -1,21 +1,18 @@ import type { CartHandlers } from '..' import { addCartItemsMutation, - checkoutCreateMutation, + createCartMutation, } from '@framework/utils/mutations' import getCartCookie from '@framework/api/utils/get-cart-cookie' +import reconcileCarts from '@framework/api/utils/reconcile-carts' import { REACTION_ANONYMOUS_CART_TOKEN_COOKIE, REACTION_CART_ID_COOKIE, + REACTION_CUSTOMER_TOKEN_COOKIE, } from '@framework/const' const addItem: CartHandlers['addItem'] = async ({ - req: { - cookies: { - [REACTION_ANONYMOUS_CART_TOKEN_COOKIE]: anonymousCartToken, - [REACTION_CART_ID_COOKIE]: cartId, - }, - }, + req: { cookies }, res, body: { item }, config, @@ -23,6 +20,20 @@ const addItem: CartHandlers['addItem'] = async ({ console.log('add-item API', item.productId) console.log('variantId', item.variantId) + const { + [REACTION_ANONYMOUS_CART_TOKEN_COOKIE]: anonymousCartToken, + [REACTION_CUSTOMER_TOKEN_COOKIE]: reactionCustomerToken, + } = cookies + + let { [REACTION_CART_ID_COOKIE]: cartId } = cookies + + if (!cartId) { + return res.status(400).json({ + data: null, + errors: [{ message: 'Missing cartId cookie' }], + }) + } + if (!item) { return res.status(400).json({ data: null, @@ -32,7 +43,7 @@ const addItem: CartHandlers['addItem'] = async ({ if (!item.quantity) item.quantity = 1 if (cartId === config.dummyEmptyCartId) { - const createdCart = await config.fetch(checkoutCreateMutation, { + const createdCart = await config.fetch(createCartMutation, { variables: { input: { shopId: config.shopId, @@ -54,20 +65,58 @@ const addItem: CartHandlers['addItem'] = async ({ console.log('created cart', createdCart.data.createCart.cart) res.setHeader('Set-Cookie', [ - getCartCookie(config.cartCookie, createdCart.data.createCart.token, 999), + getCartCookie( + config.anonymousCartTokenCookie, + 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, { + } + + const anonymousTokenParam = {} + const authorizationHeaderParam = {} + + if (anonymousCartToken) { + anonymousTokenParam.cartToken = anonymousCartToken + } + + if (reactionCustomerToken) { + authorizationHeaderParam[ + 'Authorization' + ] = `Bearer ${reactionCustomerToken}` + } + + if (anonymousCartToken && reactionCustomerToken) { + console.log('reconciliating carts')( + ({ _id: cartId } = await reconcileCarts({ + config, + cartId, + anonymousCartToken, + reactionCustomerToken, + })) + ) + + // Clear the anonymous cart token cookie and update cart ID cookie + res.setHeader('Set-Cookie', [ + getCartCookie(config.anonymousCartTokenCookie), + getCartCookie(config.cartIdCookie, cartId, 999), + ]) + } + + const updatedCart = await config.fetch( + addCartItemsMutation, + { variables: { input: { cartId, - cartToken: anonymousCartToken, + ...anonymousTokenParam, items: [ { productConfiguration: { @@ -80,14 +129,17 @@ const addItem: CartHandlers['addItem'] = async ({ ], }, }, - }) + }, + { + headers: { + ...authorizationHeaderParam, + }, + } + ) - console.log('updatedCart', updatedCart) + console.log('updatedCart', updatedCart) - return res.status(200).json(updatedCart.data) - } - - res.status(200) + return res.status(200).json(updatedCart.data) } export default addItem diff --git a/framework/reactioncommerce/api/cart/handlers/get-cart.ts b/framework/reactioncommerce/api/cart/handlers/get-cart.ts index 0e6eb5c36..a6f2e58e7 100644 --- a/framework/reactioncommerce/api/cart/handlers/get-cart.ts +++ b/framework/reactioncommerce/api/cart/handlers/get-cart.ts @@ -1,34 +1,47 @@ import type { Cart } from '../../../types' import type { CartHandlers } from '../' import getAnomymousCartQuery from '@framework/utils/queries/get-anonymous-cart' +import accountCartByAccountIdQuery from '@framework/utils/queries/account-cart-by-account-id' import getCartCookie from '@framework/api/utils/get-cart-cookie' +import reconcileCarts from '@framework/api/utils/reconcile-carts' +import getViewerId from '@framework/customer/get-viewer-id' import { REACTION_ANONYMOUS_CART_TOKEN_COOKIE, REACTION_CART_ID_COOKIE, + REACTION_CUSTOMER_TOKEN_COOKIE, } from '@framework/const.ts' import { normalizeCart } from '@framework/utils' // Return current cart info -const getCart: CartHandlers['getCart'] = async ({ - req: { +const getCart: CartHandlers['getCart'] = async ({ req, res, config }) => { + const { cookies: { [REACTION_ANONYMOUS_CART_TOKEN_COOKIE]: anonymousCartToken, [REACTION_CART_ID_COOKIE]: cartId, + [REACTION_CUSTOMER_TOKEN_COOKIE]: reactionCustomerToken, }, - }, - res, - config, -}) => { + } = req + let normalizedCart - console.log('get-cart API') - console.log('anonymousCartToken', anonymousCartToken) - console.log('cartId', cartId) - console.log('shopId', config.shopId) + if (cartId && anonymousCartToken && reactionCustomerToken) { + const rawReconciledCart = await reconcileCarts({ + config, + cartId, + anonymousCartToken, + reactionCustomerToken, + }) - if (cartId && anonymousCartToken) { + normalizedCart = normalizeCart(rawReconciledCart) + + // Clear the anonymous cart token cookie and update cart ID cookie + res.setHeader('Set-Cookie', [ + getCartCookie(config.anonymousCartTokenCookie), + getCartCookie(config.cartIdCookie, normalizedCart.id, 999), + ]) + } else if (cartId && anonymousCartToken) { const { - data: { cart: rawCart }, + data: { cart: rawAnonymousCart }, } = await config.fetch(getAnomymousCartQuery, { variables: { cartId, @@ -36,11 +49,43 @@ const getCart: CartHandlers['getCart'] = async ({ }, }) - normalizedCart = normalizeCart(rawCart) + normalizedCart = normalizeCart(rawAnonymousCart) + } else if (reactionCustomerToken && !anonymousCartToken) { + const accountId = await getViewerId({ + customerToken: reactionCustomerToken, + config, + }) + + const { + data: { cart: rawAccountCart }, + } = await config.fetch( + accountCartByAccountIdQuery, + { + variables: { + accountId, + shopId: config.shopId, + }, + }, + { + headers: { + Authorization: `Bearer ${reactionCustomerToken}`, + }, + } + ) + + normalizedCart = normalizeCart(rawAccountCart) + + if (cartId !== normalizedCart.id) { + res.setHeader( + 'Set-Cookie', + getCartCookie(config.cartIdCookie, rawAccountCart._id, 999) + ) + } } else { + // If there's no cart for now, store a dummy cart ID to keep Next Commerce happy res.setHeader( 'Set-Cookie', - getCartCookie(config.cartCookie, config.dummyEmptyCartId, 999) + getCartCookie(config.cartIdCookie, config.dummyEmptyCartId, 999) ) } diff --git a/framework/reactioncommerce/api/cart/index.ts b/framework/reactioncommerce/api/cart/index.ts index 7a2197ce7..0e8e02507 100644 --- a/framework/reactioncommerce/api/cart/index.ts +++ b/framework/reactioncommerce/api/cart/index.ts @@ -29,7 +29,7 @@ const cartApi: ReactionCommerceApiHandler = async ( if (!isAllowedMethod(req, res, METHODS)) return const { cookies } = req - const cartId = cookies[config.cartCookie] + const cartId = cookies[config.anonymousCartTokenCookie] try { // Return current cart info diff --git a/framework/reactioncommerce/api/checkout/index.ts b/framework/reactioncommerce/api/checkout/index.ts index de2cb835c..ea9b101e1 100644 --- a/framework/reactioncommerce/api/checkout/index.ts +++ b/framework/reactioncommerce/api/checkout/index.ts @@ -1,50 +1 @@ -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, {}, {}) +export default function () {} diff --git a/framework/reactioncommerce/api/index.ts b/framework/reactioncommerce/api/index.ts index 52ce27bf9..3a9c97376 100644 --- a/framework/reactioncommerce/api/index.ts +++ b/framework/reactioncommerce/api/index.ts @@ -12,16 +12,18 @@ import { if (!API_URL) { throw new Error( - `The environment variable NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN is missing and it's required to access your store` + `The environment variable API_URL is missing and it's required to access your store` ) } import fetchGraphqlApi from './utils/fetch-graphql-api' -export interface ReactionCommerceConfig extends CommerceAPIConfig { +export interface ReactionCommerceConfig extends Partial { shopId: string cartIdCookie: string - dummyEmptyCartId: string + dummyEmptyCartId?: string + anonymousCartTokenCookie?: string + anonymousCartTokenCookieMaxAge?: number } export class Config { @@ -46,11 +48,12 @@ export class Config { const config = new Config({ locale: 'en-US', commerceUrl: API_URL, - apiToken: '', cartCookie: REACTION_ANONYMOUS_CART_TOKEN_COOKIE, cartIdCookie: REACTION_CART_ID_COOKIE, dummyEmptyCartId: REACTION_EMPTY_DUMMY_CART_ID, cartCookieMaxAge: REACTION_COOKIE_EXPIRE, + anonymousCartTokenCookie: REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + anonymousCartTokenCookieMaxAge: REACTION_COOKIE_EXPIRE, fetch: fetchGraphqlApi, customerCookie: REACTION_CUSTOMER_TOKEN_COOKIE, shopId: SHOP_ID, diff --git a/framework/reactioncommerce/api/utils/create-api-handler.ts b/framework/reactioncommerce/api/utils/create-api-handler.ts index 92dd11af8..b01e04664 100644 --- a/framework/reactioncommerce/api/utils/create-api-handler.ts +++ b/framework/reactioncommerce/api/utils/create-api-handler.ts @@ -39,8 +39,6 @@ export default function createApiHandler< handlers: H, defaultOptions: Options ) { - console.log('next api handler', defaultOptions) - return function getApiHandler({ config, operations, diff --git a/framework/reactioncommerce/api/utils/fetch-graphql-api.ts b/framework/reactioncommerce/api/utils/fetch-graphql-api.ts index dfbcf0343..6de009907 100644 --- a/framework/reactioncommerce/api/utils/fetch-graphql-api.ts +++ b/framework/reactioncommerce/api/utils/fetch-graphql-api.ts @@ -1,6 +1,5 @@ import type { GraphQLFetcher } from '@commerce/api' import fetch from './fetch' - import { API_URL } from '../../const' import { getError } from '../../utils/handle-fetch-response' diff --git a/framework/reactioncommerce/api/utils/reconcile-carts.ts b/framework/reactioncommerce/api/utils/reconcile-carts.ts new file mode 100644 index 000000000..6bebd686d --- /dev/null +++ b/framework/reactioncommerce/api/utils/reconcile-carts.ts @@ -0,0 +1,34 @@ +import reconcileCartsMutation from '@framework/utils/mutations/reconcile-carts' + +async function reconcileCarts({ + config, + cartId, + anonymousCartToken, + reactionCustomerToken, +}) { + const { + data: { + reconcileCarts: { cart: rawReconciledCart }, + }, + } = await config.fetch( + reconcileCartsMutation, + { + variables: { + input: { + anonymousCartId: cartId, + cartToken: anonymousCartToken, + shopId: config.shopId, + }, + }, + }, + { + headers: { + Authorization: `Bearer ${reactionCustomerToken}`, + }, + } + ) + + return rawReconciledCart +} + +export default reconcileCarts diff --git a/framework/reactioncommerce/auth/index.ts b/framework/reactioncommerce/auth/index.ts new file mode 100644 index 000000000..36e757a89 --- /dev/null +++ b/framework/reactioncommerce/auth/index.ts @@ -0,0 +1,3 @@ +export { default as useLogin } from './use-login' +export { default as useLogout } from './use-logout' +export { default as useSignup } from './use-signup' diff --git a/framework/reactioncommerce/auth/use-login.tsx b/framework/reactioncommerce/auth/use-login.tsx index 188dd54a2..fb1fd22fe 100644 --- a/framework/reactioncommerce/auth/use-login.tsx +++ b/framework/reactioncommerce/auth/use-login.tsx @@ -2,12 +2,12 @@ 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 authenticateMutation from '../utils/mutations/authenticate' import { CustomerAccessTokenCreateInput, CustomerUserError, Mutation, - MutationCheckoutCreateArgs, + MutationAuthenticateArgs, } from '../schema' import useLogin, { UseLogin } from '@commerce/auth/use-login' import { setCustomerToken } from '../utils' @@ -25,7 +25,7 @@ const getErrorMessage = ({ code, message }: CustomerUserError) => { export const handler: MutationHook = { fetchOptions: { - query: createCustomerAccessTokenMutation, + query: authenticateMutation, }, async fetcher({ input: { email, password }, options, fetch }) { if (!(email && password)) { @@ -35,25 +35,19 @@ export const handler: MutationHook = { }) } - const { customerAccessTokenCreate } = await fetch< - Mutation, - MutationCheckoutCreateArgs - >({ + console.log('querying API') + + const { authenticate } = await fetch({ ...options, variables: { - input: { email, password }, + serviceName: 'password', + params: { user: { email }, password }, }, }) - const errors = customerAccessTokenCreate?.customerUserErrors + const accessToken = authenticate?.tokens?.accessToken - if (errors && errors.length) { - throw new ValidationError({ - message: getErrorMessage(errors[0]), - }) - } - const customerAccessToken = customerAccessTokenCreate?.customerAccessToken - const accessToken = customerAccessToken?.accessToken + console.log('accessToken', accessToken) if (accessToken) { setCustomerToken(accessToken) diff --git a/framework/reactioncommerce/auth/use-logout.tsx b/framework/reactioncommerce/auth/use-logout.tsx index 81a3b8cdd..a4ea1d3d0 100644 --- a/framework/reactioncommerce/auth/use-logout.tsx +++ b/framework/reactioncommerce/auth/use-logout.tsx @@ -2,21 +2,18 @@ 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 logoutMutation from '../utils/mutations/logout' import { getCustomerToken, setCustomerToken } from '../utils/customer-token' export default useLogout as UseLogout export const handler: MutationHook = { fetchOptions: { - query: customerAccessTokenDeleteMutation, + query: logoutMutation, }, async fetcher({ options, fetch }) { await fetch({ ...options, - variables: { - customerAccessToken: getCustomerToken(), - }, }) setCustomerToken(null) return null diff --git a/framework/reactioncommerce/auth/use-signup.tsx b/framework/reactioncommerce/auth/use-signup.tsx index 7f66448d3..72ee60767 100644 --- a/framework/reactioncommerce/auth/use-signup.tsx +++ b/framework/reactioncommerce/auth/use-signup.tsx @@ -2,62 +2,45 @@ 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 { setCustomerToken } from '@framework/utils' +import { createUserMutation } from '@framework/utils/mutations' import useCustomer from '../customer/use-customer' -import { CustomerCreateInput } from '../schema' - -import { - customerCreateMutation, - customerAccessTokenCreateMutation, -} from '../utils/mutations' -import handleLogin from '../utils/handle-login' +import { CreateUserInput } from '../schema' export default useSignup as UseSignup export const handler: MutationHook< null, {}, - CustomerCreateInput, - CustomerCreateInput + CreateUserInput, + CreateUserInput > = { fetchOptions: { - query: customerCreateMutation, + query: createUserMutation, }, - async fetcher({ - input: { firstName, lastName, email, password }, - options, - fetch, - }) { - if (!(firstName && lastName && email && password)) { + 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 signup', + message: 'An email and password are required to sign up', }) } - const data = await fetch({ + const { createUser } = 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 + const accessToken = createUser?.loginResult?.tokens?.accessToken + + if (accessToken) { + setCustomerToken(accessToken) + } + + return createUser }, useHook: ({ fetch }) => () => { const { revalidate } = useCustomer() diff --git a/framework/reactioncommerce/cart/use-remove-item.tsx b/framework/reactioncommerce/cart/use-remove-item.tsx index de0ecd64f..a7e062179 100644 --- a/framework/reactioncommerce/cart/use-remove-item.tsx +++ b/framework/reactioncommerce/cart/use-remove-item.tsx @@ -16,7 +16,7 @@ import useCart from './use-cart' import { removeCartItemsMutation, getAnonymousCartToken, - getCartId, + getAnonymousCartId, normalizeCart, } from '../utils' import { Cart, LineItem } from '../types' @@ -49,7 +49,7 @@ export const handler = { ...options, variables: { input: { - cartId: getCartId(), + cartId: getAnonymousCartId(), cartToken: getAnonymousCartToken(), cartItemIds: [itemId], }, diff --git a/framework/reactioncommerce/cart/use-update-item.tsx b/framework/reactioncommerce/cart/use-update-item.tsx index 5a6a1b9f9..a84f2fef9 100644 --- a/framework/reactioncommerce/cart/use-update-item.tsx +++ b/framework/reactioncommerce/cart/use-update-item.tsx @@ -15,7 +15,7 @@ import { handler as removeItemHandler } from './use-remove-item' import type { Cart, LineItem, UpdateCartItemBody } from '../types' import { getAnonymousCartToken, - getCartId, + getAnonymousCartId, updateCartItemsQuantityMutation, normalizeCart, } from '../utils' @@ -59,7 +59,7 @@ export const handler = { ...options, variables: { updateCartItemsQuantityInput: { - cartId: getCartId(), + cartId: getAnonymousCartId(), cartToken: getAnonymousCartToken(), items: [ { diff --git a/framework/reactioncommerce/cart/utils/checkout-create.ts b/framework/reactioncommerce/cart/utils/create-cart.ts similarity index 54% rename from framework/reactioncommerce/cart/utils/checkout-create.ts rename to framework/reactioncommerce/cart/utils/create-cart.ts index f072b5992..57c95789d 100644 --- a/framework/reactioncommerce/cart/utils/checkout-create.ts +++ b/framework/reactioncommerce/cart/utils/create-cart.ts @@ -1,15 +1,13 @@ import { REACTION_ANONYMOUS_CART_TOKEN_COOKIE, - SHOPIFY_CHECKOUT_URL_COOKIE, - SHOPIFY_COOKIE_EXPIRE, + REACTION_COOKIE_EXPIRE, } from '../../const' - -import checkoutCreateMutation from '../../utils/mutations/checkout-create' +import createCartMutation from '../../utils/mutations/create-cart' import Cookies from 'js-cookie' -export const checkoutCreate = async (fetch: any) => { +export const createCart = async (fetch: any) => { const data = await fetch({ - query: checkoutCreateMutation, + query: createCartMutation, variables: { input: { shopId, @@ -22,13 +20,12 @@ export const checkoutCreate = async (fetch: any) => { if (checkoutId) { const options = { - expires: SHOPIFY_COOKIE_EXPIRE, + expires: REACTION_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 +export default createCart diff --git a/framework/reactioncommerce/cart/utils/index.ts b/framework/reactioncommerce/cart/utils/index.ts index 20d04955d..08ce71ec6 100644 --- a/framework/reactioncommerce/cart/utils/index.ts +++ b/framework/reactioncommerce/cart/utils/index.ts @@ -1,2 +1,2 @@ export { default as checkoutToCart } from './checkout-to-cart' -export { default as checkoutCreate } from './checkout-create' +export { default as checkoutCreate } from './create-cart' diff --git a/framework/reactioncommerce/commerce.config.json b/framework/reactioncommerce/commerce.config.json index 5cbc67209..ce78b1b10 100644 --- a/framework/reactioncommerce/commerce.config.json +++ b/framework/reactioncommerce/commerce.config.json @@ -1,6 +1,7 @@ { "provider": "reactioncommerce", "features": { - "wishlist": false + "wishlist": false, + "customCheckout": true } } diff --git a/framework/reactioncommerce/customer/get-customer-id.ts b/framework/reactioncommerce/customer/get-customer-id.ts deleted file mode 100644 index 931615691..000000000 --- a/framework/reactioncommerce/customer/get-customer-id.ts +++ /dev/null @@ -1,24 +0,0 @@ -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/get-viewer-id.ts b/framework/reactioncommerce/customer/get-viewer-id.ts new file mode 100644 index 000000000..5e3a5fabe --- /dev/null +++ b/framework/reactioncommerce/customer/get-viewer-id.ts @@ -0,0 +1,26 @@ +import { getConfig, ReactionCommerceConfig } from '../api' +import getViewerIdQuery from '../utils/queries/get-viewer-id-query' + +async function getViewerId({ + customerToken: customerAccessToken, + config, +}: { + customerToken: string + config?: ReactionCommerceConfig +}): Promise { + config = getConfig(config) + + const { data } = await config.fetch( + getViewerIdQuery, + {}, + { + headers: { + Authorization: `Bearer ${customerAccessToken}`, + }, + } + ) + + return data.viewer?._id +} + +export default getViewerId diff --git a/framework/reactioncommerce/customer/use-customer.tsx b/framework/reactioncommerce/customer/use-customer.tsx index 113dd1a9d..d7cfd4679 100644 --- a/framework/reactioncommerce/customer/use-customer.tsx +++ b/framework/reactioncommerce/customer/use-customer.tsx @@ -1,7 +1,7 @@ import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' import { Customer } from '@commerce/types' import { SWRHook } from '@commerce/utils/types' -import { viewerQuery } from '../utils' +import { viewerQuery, normalizeCustomer } from '../utils' export default useCustomer as UseCustomer @@ -13,7 +13,7 @@ export const handler: SWRHook = { const data = await fetch({ ...options, }) - return data.viewer ?? null + return normalizeCustomer(data.viewer) ?? null }, useHook: ({ useData }) => (input) => { return useData({ diff --git a/framework/reactioncommerce/fetcher.ts b/framework/reactioncommerce/fetcher.ts index b4c57097b..0ea43543d 100644 --- a/framework/reactioncommerce/fetcher.ts +++ b/framework/reactioncommerce/fetcher.ts @@ -2,6 +2,7 @@ import { FetcherError } from '@commerce/utils/errors' import type { Fetcher } from '@commerce/utils/types' import { handleFetchResponse } from './utils' import { API_URL } from './const' +import { getCustomerToken } from './utils' async function getText(res: Response) { try { @@ -28,12 +29,20 @@ const fetcher: Fetcher = async ({ }) => { // if no URL is passed but we have a `query` param, we assume it's GraphQL if (!url && query) { + const customerToken = getCustomerToken() + const authorizationHeader = {} + + if (customerToken) { + authorizationHeader['Authorization'] = `Bearer ${customerToken}` + } + return handleFetchResponse( await fetch(API_URL, { method: 'POST', body: JSON.stringify({ query, variables }), headers: { 'Content-Type': 'application/json', + ...authorizationHeader, }, }) ) diff --git a/framework/reactioncommerce/index.tsx b/framework/reactioncommerce/index.tsx index 9aa24f2dc..bc046619b 100644 --- a/framework/reactioncommerce/index.tsx +++ b/framework/reactioncommerce/index.tsx @@ -19,7 +19,7 @@ type ReactionConfig = CommerceConfig & { export const reactionCommerceConfig: ReactionConfig = { locale: 'en-us', - cartCookie: REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + anonymousCartTokenCookie: REACTION_ANONYMOUS_CART_TOKEN_COOKIE, shopId: SHOP_ID, } diff --git a/framework/reactioncommerce/provider.ts b/framework/reactioncommerce/provider.ts index 13b7af197..30d6b7c7f 100644 --- a/framework/reactioncommerce/provider.ts +++ b/framework/reactioncommerce/provider.ts @@ -1,7 +1,6 @@ import { REACTION_ANONYMOUS_CART_TOKEN_COOKIE, REACTION_CART_ID_COOKIE, - STORE_DOMAIN, } from './const' import { handler as useCart } from './cart/use-cart' @@ -20,9 +19,8 @@ import fetcher from './fetcher' export const reactionCommerceProvider = { locale: 'en-us', - cartCookie: REACTION_ANONYMOUS_CART_TOKEN_COOKIE, + anonymousCartTokenCookie: REACTION_ANONYMOUS_CART_TOKEN_COOKIE, cartIdCookie: REACTION_CART_ID_COOKIE, - storeDomain: STORE_DOMAIN, fetcher, cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, customer: { useCustomer }, diff --git a/framework/reactioncommerce/schema.d.ts b/framework/reactioncommerce/schema.d.ts index 945444080..21b174606 100644 --- a/framework/reactioncommerce/schema.d.ts +++ b/framework/reactioncommerce/schema.d.ts @@ -13,8 +13,6 @@ export type Scalars = { Boolean: boolean Int: number Float: number - /** A string email address */ - Email: any /** * * An opaque string that identifies a particular result within a connection, @@ -29,910 +27,14 @@ export type Scalars = { * */ 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>> + /** 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 + /** A string email address */ + Email: any + /** An object with any fields */ + JSONObject: any } /** Represents a single user account */ @@ -1000,6 +102,246 @@ export type AccountGroupsArgs = { sortBy?: Maybe } +/** + * Wraps a list of `Accounts`, 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 AccountConnection = { + __typename?: 'AccountConnection' + /** 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 `Account` object */ +export type AccountEdge = NodeEdge & { + __typename?: 'AccountEdge' + /** The cursor that represents this node in the paginated results */ + cursor: Scalars['ConnectionCursor'] + /** The account */ + node?: Maybe +} + +/** The fields by which you are allowed to sort any query that returns an `AccountConnection` */ +export enum AccountSortByField { + /** Account ID */ + Id = '_id', + /** Date and time at which this account was created */ + CreatedAt = 'createdAt', + /** Date and time at which this account was last updated */ + UpdatedAt = 'updatedAt', +} + +/** Defines a new Address and the account to which it should be added */ +export type AddAccountAddressBookEntryInput = { + /** The account ID */ + accountId: Scalars['ID'] + /** The address to add */ + address: AddressInput + /** An optional string identifying the mutation call, which will be returned in the response payload */ + clientMutationId?: Maybe +} + +/** The response from the `addAccountAddressBookEntry` mutation */ +export type AddAccountAddressBookEntryPayload = { + __typename?: 'AddAccountAddressBookEntryPayload' + /** The added address */ + address?: Maybe
+ /** The added address edge */ + addressEdge?: Maybe + /** The same string you sent with the mutation params, for matching mutation calls with their responses */ + clientMutationId?: Maybe +} + +/** Defines a new Email and the account to which it should be added */ +export type AddAccountEmailRecordInput = { + /** The account ID, which defaults to the viewer account */ + accountId?: Maybe + /** An optional string identifying the mutation call, which will be returned in the response payload */ + clientMutationId?: Maybe + /** The email address to add */ + email: Scalars['Email'] +} + +/** The response from the `addAccountEmailRecord` mutation */ +export type AddAccountEmailRecordPayload = { + __typename?: 'AddAccountEmailRecordPayload' + /** The account, with updated `emailRecords` */ + account?: Maybe + /** The same string you sent with the mutation params, for matching mutation calls with their responses */ + clientMutationId?: Maybe +} + +/** Defines a group and account that should be linked */ +export type AddAccountToGroupInput = { + /** The account ID */ + accountId: Scalars['ID'] + /** An optional string identifying the mutation call, which will be returned in the response payload */ + clientMutationId?: Maybe + /** The group ID */ + groupId: Scalars['ID'] +} + +/** The response from the `addAccountToGroup` mutation */ +export type AddAccountToGroupPayload = { + __typename?: 'AddAccountToGroupPayload' + /** The same string you sent with the mutation params, for matching mutation calls with their responses */ + clientMutationId?: Maybe + /** The updated group */ + group?: Maybe +} + +/** Input for the `addCartItems` mutation */ +export type AddCartItemsInput = { + /** The cart ID */ + cartId: Scalars['ID'] + /** If this cart is anonymous, provide the `cartToken` that was returned in the `CreateCartPayload` */ + cartToken?: Maybe + /** An optional string identifying the mutation call, which will be returned in the response payload */ + clientMutationId?: Maybe + /** Array of items to be added to the cart */ + items: Array> +} + +/** The payload returned from the `addCartItems` mutation call */ +export type AddCartItemsPayload = { + __typename?: 'AddCartItemsPayload' + /** + * The modified cart. You should check `incorrectPriceFailures` and `minOrderQuantityFailures` for + * information necessary to display errors to the shopper. Some items may not have been added. + */ + cart?: Maybe + /** The same string you sent with the mutation params, for matching mutation calls with their responses */ + clientMutationId?: Maybe + /** + * Clients should check to see if any items failed to be added due to the price not matching the current price. + * In general, a user interface should display the correct current prices to the shopper, confirm that they still + * want to add the items, and then call `createCart` or `addCartItems` to do so. + * + * Note that this field will always exist but may be an empty array if there were no failures of this type. + */ + incorrectPriceFailures: Array> + /** + * Clients should check to see if any items failed to be added due to quantity being below the minimum order + * quantity defined for the product variant. In general, a user interface should display the minimum order + * quantity to the shopper and allow them to add that quantity or greater. + * + * Note that this field will always exist but may be an empty array if there were no failures of this type. + */ + minOrderQuantityFailures: Array> +} + +/** Input for the addOrderFulfillmentGroup mutation */ +export type AddOrderFulfillmentGroupInput = { + /** An optional string identifying the mutation call, which will be returned in the response payload */ + clientMutationId?: Maybe + /** The order fulfillment group input, used to build the new group */ + fulfillmentGroup: OrderFulfillmentGroupExistingOrderInput + /** Optional list of order item IDs that should be moved from an existing group to the new group */ + moveItemIds?: Maybe>> + /** ID of the order that has the item you want to add the group to */ + orderId: Scalars['ID'] +} + +/** Response payload for the addOrderFulfillmentGroup mutation */ +export type AddOrderFulfillmentGroupPayload = { + __typename?: 'AddOrderFulfillmentGroupPayload' + /** The same string you sent with the mutation params, for matching mutation calls with their responses */ + clientMutationId?: Maybe + /** ID of the added fulfillment group */ + newFulfillmentGroupId: Scalars['ID'] + /** The updated order */ + order: Order +} + +/** Input for `addTag` mutation */ +export type AddTagInput = { + /** An optional string identifying the mutation call, which will be returned in the response payload */ + clientMutationId?: Maybe + /** Title to display to customers */ + displayTitle?: Maybe + /** Hero media URL */ + heroMediaUrl?: Maybe + /** Whether the tag is visible */ + isVisible: Scalars['Boolean'] + /** Tag metafields */ + metafields?: Maybe>> + /** Unique name of the tag */ + name: Scalars['String'] + /** The shop that owns the tag */ + shopId: Scalars['ID'] + /** The tag slug. If left blank, the name will be slugified and saved as the slug */ + slug?: Maybe +} + +/** Response payload for `addTag` mutation */ +export type AddTagPayload = { + __typename?: 'AddTagPayload' + /** The same string you sent with the mutation params, for matching mutation calls with their responses */ + clientMutationId?: Maybe + /** The shop that owns the tag */ + shopId: Scalars['ID'] + /** The newly-created tag */ + tag: Tag +} + +/** 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'] +} + /** * Wraps a list of `Addresses`, providing pagination cursors and information. * @@ -1032,351 +374,6 @@ export type AddressEdge = { 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 */ @@ -1421,20 +418,25 @@ export type AddressInput = { 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 +/** A list of the possible types of `Address` */ +export enum AddressType { + /** Address can be used for payment transactions and invoicing */ + Billing = 'billing', + /** Address can be used as a mailing address for sending physical items */ + Shipping = 'shipping', +} + +/** 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'] } /** The response from `Query.addressValidation` */ @@ -1452,57 +454,27 @@ export type AddressValidationResults = { 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', +/** + * 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'] } /** @@ -1537,265 +509,401 @@ export type AddressValidationRuleEdge = NodeEdge & { 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