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}
>
-
-
+
+
+ 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
+[](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 (
-
- )
-}
-...
-```
-
-### 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 (
-
- )
-}
-...
-```
-
-### 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 Add To Cart
-}
-...
-```
-
-### 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 Remove
-}
-...
-```
-
-## 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
+}
+```
+
+### 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
+}
+```
+
+### 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 (
+ logout()}>
+ Logout
+
+ )
+}
+```
+
+## 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 Add To Cart
+}
+```
+
+### 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 Remove
+}
+```
+
+## 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