Merge branch 'outgrow-reaction-commerce-provider' of github.com:outgrow/commerce into fix-types

This commit is contained in:
cristiancc 2021-04-28 14:24:05 -05:00
commit 9d485ba134
101 changed files with 19588 additions and 19511 deletions

View File

@ -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=

View File

@ -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

View File

@ -93,15 +93,18 @@ const CartItem = ({
})}
{...rest}
>
<div className="w-16 h-16 bg-violet relative overflow-hidden">
<Image
className={s.productImage}
width={150}
height={150}
src={item.variant.image!.url}
alt={item.variant.image!.altText}
unoptimized
/>
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer">
<Link href={`/product/${item.path}`}>
<Image
onClick={() => closeSidebarIfPresent()}
className={s.productImage}
width={150}
height={150}
src={item.variant.image!.url}
alt={item.variant.image!.altText}
unoptimized
/>
</Link>
</div>
<div className="flex-1 flex flex-col text-base">
<Link href={`/product/${item.path}`}>

View File

@ -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<Props> = ({ product }) => {
@ -29,11 +26,18 @@ const ProductView: FC<Props> = ({ product }) => {
})
const { openSidebar } = useUI()
const [loading, setLoading] = useState(false)
const [choices, setChoices] = useState<SelectedOptions>({
color: null,
})
const [choices, setChoices] = useState<SelectedOptions>({})
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<Props> = ({ product }) => {
))}
<div className="pb-14 break-words w-full max-w-xl">
<Text html={product.description} />
<Text html={product.descriptionHtml || product.description} />
</div>
</section>
<div>
@ -150,7 +154,6 @@ const ProductView: FC<Props> = ({ product }) => {
className={s.button}
onClick={addToCart}
loading={loading}
disabled={!variant && product.options.length > 0}
>
Add to Cart
</Button>

View File

@ -1,9 +1,5 @@
import type { Product } from '@commerce/types'
export type SelectedOptions = {
size: string | null
color: string | null
}
export type SelectedOptions = Record<string, string | null>
export function getVariant(product: Product, opts: SelectedOptions) {
const variant = product.variants.find((variant) => {

View File

@ -1 +0,0 @@
# Roadmap

View File

@ -1,3 +1,5 @@
COMMERCE_PROVIDER=bigcommerce
BIGCOMMERCE_STOREFRONT_API_URL=
BIGCOMMERCE_STOREFRONT_API_TOKEN=
BIGCOMMERCE_STORE_API_URL=

View File

@ -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: <b>set your environment variables</b> 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
<details>
<summary>I already own a BigCommerce store. What should I do?</summary>
<br>
First thing you do is: <b>set your environment variables</b>
<br>
<br>
.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'
</details>
const App = ({ locale = 'en-US', children }) => {
return (
<CommerceProvider locale={locale}>
{children}
</CommerceProvider>
)
}
...
```
### 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 (
<form onSubmit={handleLogin}>
{children}
</form>
)
}
...
```
### useLogout
Hook to logout user.
```jsx
...
import useLogout from '@bigcommerce/storefront-data-hooks/use-logout'
const LogoutLink = () => {
const logout = useLogout()
return (
<a onClick={() => logout()}>
Logout
</a>
)
}
```
### 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 (
<div>Hello, {data.firstName}</div>
)
}
```
### 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 (
<form onSubmit={handleSignup}>
{children}
</form>
)
}
...
```
### 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 ? <span>{itemsCount}</span> : 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 <button onClick={addToCart}>Add To Cart</button>
}
...
```
### 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 (
<input
type="number"
max={99}
min={0}
value={quantity}
onChange={updateQuantity}
/>
)
}
...
```
### 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 <button onClick={handleRemove}>Remove</button>
}
...
```
## 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 (
<button onClick={handleWishlistChange}>
<Heart fill={itemInWishlist ? 'var(--pink)' : 'none'} />
</button>
)
}
```
## 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 (
<Grid layout="normal">
{data.products.map(({ node }) => (
<ProductCard key={node.path} product={node} />
))}
</Grid>
)
}
```
### 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))
<details>
<summary>BigCommerce shows a Coming Soon page and requests a Preview Code</summary>
<br>
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
<br>
<br>
BigCommerce team has been notified and they plan to add more detailed about this subject.
</details>

View File

@ -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 <CommerceProvider locale={locale}>{children}</CommerceProvider>
}
```
## 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 <form onSubmit={handleSignup}>{children}</form>
}
```
### 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 <form onSubmit={handleLogin}>{children}</form>
}
```
### 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 (
<button type="button" onClick={() => logout()}>
Logout
</button>
)
}
```
## 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 <p>Loading...</p>
if (error) return <p>{error.message}</p>
if (!data) return null
return <div>Hello, {data.firstName}</div>
}
```
## 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 (
<Grid layout="normal">
{data.products.map((product) => (
<ProductCard key={product.path} product={product} />
))}
</Grid>
)
}
```
## 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 <p>Loading...</p>
if (error) return <p>{error.message}</p>
if (isEmpty) return <p>The cart is empty</p>
return <p>The cart total is {data.totalPrice}</p>
}
```
### 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 <button onClick={addToCart}>Add To Cart</button>
}
```
### 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 (
<input
type="number"
max={99}
min={0}
value={quantity}
onChange={updateQuantity}
/>
)
}
```
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 <button onClick={handleRemove}>Remove</button>
}
```
## 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 <p>Loading...</p>
if (error) return <p>{error.message}</p>
if (isEmpty) return <p>The wihslist is empty</p>
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 (
<button onClick={handleWishlistChange}>
<Heart fill={itemInWishlist ? 'var(--pink)' : 'none'} />
</button>
)
}
```
## 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))

View File

@ -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 }

View File

@ -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<CommerceConfig>
export type BigcommerceProps = {
children?: ReactNode
locale: string
} & BigcommerceConfig
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
return (
<CoreCommerceProvider
provider={bigcommerceProvider}
config={{ ...bigcommerceConfig, ...config }}
>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
```
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<typeof handler>
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<typeof handler>
export const handler: MutationHook<Cart, {}, CartItemBody> = {
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<BigcommerceCart, AddCartItemBody>({
...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.

View File

@ -163,6 +163,7 @@ interface Entity {
export interface Product extends Entity {
name: string
description: string
descriptionHtml?: string
slug?: string
path?: string
images: ProductImage[]

View File

@ -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
}

View File

@ -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 = <any>{}
const authorizationHeaderParam = <any>{}
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

View File

@ -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)
)
}

View File

@ -29,7 +29,7 @@ const cartApi: ReactionCommerceApiHandler<Cart, CartHandlers> = 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

View File

@ -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<any> = 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 () {}

View File

@ -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<CommerceAPIConfig> {
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,

View File

@ -39,8 +39,6 @@ export default function createApiHandler<
handlers: H,
defaultOptions: Options
) {
console.log('next api handler', defaultOptions)
return function getApiHandler({
config,
operations,

View File

@ -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'

View File

@ -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

View File

@ -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'

View File

@ -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<null, {}, CustomerAccessTokenCreateInput> = {
fetchOptions: {
query: createCustomerAccessTokenMutation,
query: authenticateMutation,
},
async fetcher({ input: { email, password }, options, fetch }) {
if (!(email && password)) {
@ -35,25 +35,19 @@ export const handler: MutationHook<null, {}, CustomerAccessTokenCreateInput> = {
})
}
const { customerAccessTokenCreate } = await fetch<
Mutation,
MutationCheckoutCreateArgs
>({
console.log('querying API')
const { authenticate } = await fetch<Mutation, MutationAuthenticateArgs>({
...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)

View File

@ -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<typeof handler>
export const handler: MutationHook<null> = {
fetchOptions: {
query: customerAccessTokenDeleteMutation,
query: logoutMutation,
},
async fetcher({ options, fetch }) {
await fetch({
...options,
variables: {
customerAccessToken: getCustomerToken(),
},
})
setCustomerToken(null)
return null

View File

@ -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<typeof handler>
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()

View File

@ -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],
},

View File

@ -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: [
{

View File

@ -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

View File

@ -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'

View File

@ -1,6 +1,7 @@
{
"provider": "reactioncommerce",
"features": {
"wishlist": false
"wishlist": false,
"customCheckout": true
}
}

View File

@ -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<number | undefined> {
config = getConfig(config)
const { data } = await config.fetch(getCustomerIdQuery, {
variables: {
customerAccesToken:
customerAccesToken || Cookies.get(config.customerCookie),
},
})
return data.customer?.id
}
export default getCustomerId

View File

@ -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<number | undefined> {
config = getConfig(config)
const { data } = await config.fetch(
getViewerIdQuery,
{},
{
headers: {
Authorization: `Bearer ${customerAccessToken}`,
},
}
)
return data.viewer?._id
}
export default getViewerId

View File

@ -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<typeof handler>
@ -13,7 +13,7 @@ export const handler: SWRHook<Customer | null> = {
const data = await fetch<any | null>({
...options,
})
return data.viewer ?? null
return normalizeCustomer(data.viewer) ?? null
},
useHook: ({ useData }) => (input) => {
return useData({

View File

@ -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,
},
})
)

View File

@ -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,
}

View File

@ -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 },

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,4 @@
import * as Core from '@commerce/types'
import { CheckoutLineItem } from './schema'
export type ShopifyCheckout = {
id: string
webUrl: string
lineItems: CheckoutLineItem[]
}
export interface Cart extends Core.Cart {
id: string

View File

@ -1,7 +1,7 @@
import { FetcherError } from '@commerce/utils/errors'
export function getError(errors: any[], status: number) {
errors = errors ?? [{ message: 'Failed to fetch Shopify API' }]
errors = errors ?? [{ message: 'Failed to fetch Reaction Commerce API' }]
return new FetcherError({ errors, status })
}

View File

@ -1,39 +0,0 @@
import { ValidationError } from '@commerce/utils/errors'
import { setCustomerToken } from './customer-token'
const getErrorMessage = ({
code,
message,
}: {
code: string
message: string
}) => {
switch (code) {
case 'UNIDENTIFIED_CUSTOMER':
message = 'Cannot find an account that matches the provided credentials'
break
}
return message
}
const handleLogin = (data: any) => {
const response = data.customerAccessTokenCreate
const errors = response?.customerUserErrors
if (errors && errors.length) {
throw new ValidationError({
message: getErrorMessage(errors[0]),
})
}
const customerAccessToken = response?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
if (accessToken) {
setCustomerToken(accessToken)
}
return customerAccessToken
}
export default handleLogin

View File

@ -4,7 +4,7 @@ export { default as getSortVariables } from './get-sort-variables'
export { default as getVendors } from './get-vendors'
export { default as getCategories } from './get-categories'
export { default as getAnonymousCartToken } from './get-anonymous-cart-token'
export { default as getCartId } from './get-cart-id'
export { default as getAnonymousCartId } from './get-cart-id'
export * from './queries'
export * from './mutations'
export * from './normalize'

View File

@ -0,0 +1,15 @@
const authenticateMutation = /* GraphQL */ `
mutation authenticate(
$serviceName: String!
$params: AuthenticateParamsInput!
) {
authenticate(serviceName: $serviceName, params: $params) {
sessionId
tokens {
refreshToken
accessToken
}
}
}
`
export default authenticateMutation

View File

@ -4,7 +4,7 @@ import {
minOrderQuantityFailureDetailsFragment,
} from '@framework/utils/queries/get-checkout-query'
const checkoutCreateMutation = /* GraphQL */ `
const createCartMutation = /* GraphQL */ `
mutation createCartMutation($input: CreateCartInput!) {
createCart(input: $input) {
cart {
@ -21,4 +21,4 @@ const checkoutCreateMutation = /* GraphQL */ `
}
}
`
export default checkoutCreateMutation
export default createCartMutation

View File

@ -0,0 +1,13 @@
const createUserMutation = /* GraphQL */ `
mutation createUser($input: CreateUserInput!) {
createUser(user: $input) {
loginResult {
tokens {
refreshToken
accessToken
}
}
}
}
`
export default createUserMutation

View File

@ -1,16 +0,0 @@
const customerAccessTokenCreateMutation = /* GraphQL */ `
mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) {
customerAccessTokenCreate(input: $input) {
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerAccessTokenCreateMutation

View File

@ -1,14 +0,0 @@
const customerAccessTokenDeleteMutation = /* GraphQL */ `
mutation customerAccessTokenDelete($customerAccessToken: String!) {
customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
deletedAccessToken
deletedCustomerAccessTokenId
userErrors {
field
message
}
}
}
`
export default customerAccessTokenDeleteMutation

View File

@ -1,15 +0,0 @@
const customerCreateMutation = /* GraphQL */ `
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
customerUserErrors {
code
field
message
}
customer {
id
}
}
}
`
export default customerCreateMutation

View File

@ -1,7 +1,7 @@
export { default as addCartItemsMutation } from './add-cart-items'
export { default as customerCreateMutation } from './customer-create'
export { default as checkoutCreateMutation } from './checkout-create'
export { default as customerAccessTokenCreateMutation } from './customer-access-token-create'
export { default as customerAccessTokenDeleteMutation } from './customer-access-token-delete'
export { default as createUserMutation } from './create-user'
export { default as createCartMutation } from './create-cart'
export { default as authenticateMutation } from './authenticate'
export { default as logoutMutation } from './logout'
export { default as removeCartItemsMutation } from './remove-cart-items'
export { default as updateCartItemsQuantityMutation } from './update-cart-items-quantity'

View File

@ -0,0 +1,7 @@
const logoutMutation = /* GraphQL */ `
mutation logout {
logout
}
`
export default logoutMutation

View File

@ -0,0 +1,13 @@
import { cartPayloadFragment } from '@framework/utils/queries/get-checkout-query'
const reconcileCartsMutation = `
mutation reconcileCartsMutation($input: ReconcileCartsInput!) {
reconcileCarts(input: $input) {
cart {
${cartPayloadFragment}
}
}
}
`
export default reconcileCartsMutation

View File

@ -1,6 +1,7 @@
import { Product } from '@commerce/types'
import { Product, Customer } from '@commerce/types'
import {
Account,
Cart as ReactionCart,
ProductPricingInfo,
CatalogProductVariant,
@ -207,3 +208,11 @@ function normalizeLineItem({
],
}
}
export function normalizeCustomer(viewer: Account): Customer {
return {
firstName: viewer.firstName ?? '',
lastName: viewer.lastName ?? '',
email: viewer.primaryEmailAddress,
}
}

View File

@ -0,0 +1,11 @@
import { cartQueryFragment } from '@framework/utils/queries/get-checkout-query'
const accountCartByAccountIdQuery = `
query accountCartByAccountIdQuery($accountId: ID!, $shopId: ID!, $itemsAfterCursor: ConnectionCursor) {
cart: accountCartByAccountId(accountId: $accountId, shopId: $shopId) {
${cartQueryFragment}
}
}
`
export default accountCartByAccountIdQuery

View File

@ -1,8 +0,0 @@
export const viewerQuery = /* GraphQL */ `
query getCustomerId($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
id
}
}
`
export default viewerQuery

View File

@ -0,0 +1,8 @@
export const viewerIdQuery = /* GraphQL */ `
query viewer {
viewer {
_id
}
}
`
export default viewerIdQuery

View File

@ -1,5 +1,5 @@
// TODO: replace this hook and other wishlist hooks with a handler, or remove them if
// Shopify doesn't have a wishlist
// Reaction Commerce doesn't have a wishlist
import { HookFetcher } from '@commerce/utils/types'
import { Product } from '../schema'

View File

@ -1,2 +1,4 @@
SHOPIFY_STORE_DOMAIN=
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
COMMERCE_PROVIDER=shopify
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=

View File

@ -1,57 +1,28 @@
## Table of Contents
## Shopify Provider
- [Getting Started](#getting-started)
- [Modifications](#modifications)
- [Adding item to Cart](#adding-item-to-cart)
- [Proceed to Checkout](#proceed-to-checkout)
- [General Usage](#general-usage)
- [CommerceProvider](#commerceprovider)
- [useCommerce](#usecommerce)
- [Hooks](#hooks)
- [usePrice](#useprice)
- [useAddItem](#useadditem)
- [useRemoveItem](#useremoveitem)
- [useUpdateItem](#useupdateitem)
- [APIs](#apis)
- [getProduct](#getproduct)
- [getAllProducts](#getallproducts)
- [getAllCollections](#getallcollections)
- [getAllPages](#getallpages)
**Demo:** https://shopify.demo.vercel.store/
# Shopify Storefront Data Hooks
Before getting starter, a [Shopify](https://www.shopify.com/) account and store is required before using the provider.
Collection of hooks and data fetching functions to integrate Shopify in a React application. Designed to work with [Next.js Commerce](https://demo.vercel.store/).
Next, copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
## Getting Started
1. Install dependencies:
```
yarn install shopify-buy
yarn install -D @types/shopify-buy
```bash
cp framework/shopify/.env.template .env.local
```
3. Environment variables need to be set:
Then, set the environment variables in `.env.local` to match the ones from your store.
```
SHOPIFY_STORE_DOMAIN=
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
```
## Contribute
4. Point the framework to `shopify` by updating `tsconfig.json`:
Our commitment to Open Source can be found [here](https://vercel.com/oss).
```
"@framework/*": ["framework/shopify/*"],
"@framework": ["framework/shopify"]
```
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).
### Modifications
## Modifications
These modifications are temporarily until contributions are made to remove them.
#### Adding item to Cart
### Adding item to Cart
```js
// components/product/ProductView/ProductView.tsx
@ -72,7 +43,7 @@ const ProductView: FC<Props> = ({ product }) => {
}
```
#### Proceed to Checkout
### Proceed to Checkout
```js
// components/cart/CartSidebarView/CartSidebarView.tsx
@ -88,114 +59,6 @@ const CartSidebarView: FC = () => {
}
```
## General Usage
### CommerceProvider
Provider component that creates the commerce context for children.
```js
import { CommerceProvider } from '@framework'
const App = ({ children }) => {
return <CommerceProvider locale={locale}>{children}</CommerceProvider>
}
export default App
```
### useCommerce
Returns the configs that are defined in the nearest `CommerceProvider`. Also provides access to Shopify's `checkout` and `shop`.
```js
import { useCommerce } from 'nextjs-commerce-shopify'
const { checkout, shop } = useCommerce()
```
- `checkout`: The information required to checkout items and pay ([Documentation](https://shopify.dev/docs/storefront-api/reference/checkouts/checkout)).
- `shop`: Represents a collection of the general settings and information about the shop ([Documentation](https://shopify.dev/docs/storefront-api/reference/online-store/shop/index)).
## Hooks
### usePrice
Display the product variant price according to currency and locale.
```js
import usePrice from '@framework/product/use-price'
const { price } = usePrice({
amount,
})
```
Takes in either `amount` or `variant`:
- `amount`: A price value for a particular item if the amount is known.
- `variant`: A shopify product variant. Price will be extracted from the variant.
### useAddItem
```js
import { useAddItem } from '@framework/cart'
const AddToCartButton = ({ variantId, quantity }) => {
const addItem = useAddItem()
const addToCart = async () => {
await addItem({
variantId,
})
}
return <button onClick={addToCart}>Add To Cart</button>
}
```
### useRemoveItem
```js
import { useRemoveItem } from '@framework/cart'
const RemoveButton = ({ item }) => {
const removeItem = useRemoveItem()
const handleRemove = async () => {
await removeItem({ id: item.id })
}
return <button onClick={handleRemove}>Remove</button>
}
```
### useUpdateItem
```js
import { useUpdateItem } from '@framework/cart'
const CartItem = ({ item }) => {
const [quantity, setQuantity] = useState(item.quantity)
const updateItem = useUpdateItem(item)
const updateQuantity = async (e) => {
const val = e.target.value
await updateItem({ quantity: val })
}
return (
<input
type="number"
max={99}
min={0}
value={quantity}
onChange={updateQuantity}
/>
)
}
```
## APIs
Collections of APIs to fetch data from a Shopify store.

View File

@ -5,7 +5,6 @@ import {
API_TOKEN,
SHOPIFY_CHECKOUT_ID_COOKIE,
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
SHOPIFY_COOKIE_EXPIRE,
} from '../const'
if (!API_URL) {
@ -48,7 +47,7 @@ const config = new Config({
commerceUrl: API_URL,
apiToken: API_TOKEN!,
cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE,
cartCookieMaxAge: SHOPIFY_COOKIE_EXPIRE,
cartCookieMaxAge: 60 * 60 * 24 * 30,
fetch: fetchGraphqlApi,
customerCookie: SHOPIFY_CUSTOMER_TOKEN_COOKIE,
})

View File

@ -1,21 +0,0 @@
import Client from 'shopify-buy'
import { ShopifyConfig } from '../index'
type Options = {
config: ShopifyConfig
}
const getAllCollections = async (options: Options) => {
const { config } = options
const client = Client.buildClient({
storefrontAccessToken: config.apiToken,
domain: config.commerceUrl,
})
const res = await client.collection.fetchAllWithProducts()
return JSON.parse(JSON.stringify(res))
}
export default getAllCollections

View File

@ -1,25 +0,0 @@
import { Page } from '../../schema'
import { ShopifyConfig, getConfig } from '..'
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
export type PageVariables = {
id: string
}
async function getPage({
url,
variables,
config,
preview,
}: {
url?: string
variables: PageVariables
config?: ShopifyConfig
preview?: boolean
}): Promise<GetPageResult> {
config = getConfig(config)
return {}
}
export default getPage

View File

@ -10,7 +10,7 @@ import {
MutationCheckoutCreateArgs,
} from '../schema'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
import { setCustomerToken } from '../utils'
import { setCustomerToken, throwUserErrors } from '../utils'
export default useLogin as UseLogin<typeof handler>
@ -45,13 +45,8 @@ export const handler: MutationHook<null, {}, CustomerAccessTokenCreateInput> = {
},
})
const errors = customerAccessTokenCreate?.customerUserErrors
throwUserErrors(customerAccessTokenCreate?.customerUserErrors)
if (errors && errors.length) {
throw new ValidationError({
message: getErrorMessage(errors[0]),
})
}
const customerAccessToken = customerAccessTokenCreate?.customerAccessToken
const accessToken = customerAccessToken?.accessToken

View File

@ -1,15 +1,16 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import { CommerceError, ValidationError } from '@commerce/utils/errors'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
import useCustomer from '../customer/use-customer'
import { CustomerCreateInput } from '../schema'
import {
customerCreateMutation,
customerAccessTokenCreateMutation,
} from '../utils/mutations'
import handleLogin from '../utils/handle-login'
CustomerCreateInput,
Mutation,
MutationCustomerCreateArgs,
} from '../schema'
import { customerCreateMutation } from '../utils/mutations'
import { handleAutomaticLogin, throwUserErrors } from '../utils'
export default useSignup as UseSignup<typeof handler>
@ -33,7 +34,11 @@ export const handler: MutationHook<
'A first name, last name, email and password are required to signup',
})
}
const data = await fetch({
const { customerCreate } = await fetch<
Mutation,
MutationCustomerCreateArgs
>({
...options,
variables: {
input: {
@ -45,19 +50,10 @@ export const handler: MutationHook<
},
})
try {
const loginData = await fetch({
query: customerAccessTokenCreateMutation,
variables: {
input: {
email,
password,
},
},
})
handleLogin(loginData)
} catch (error) {}
return data
throwUserErrors(customerCreate?.customerUserErrors)
await handleAutomaticLogin(fetch, { email, password })
return null
},
useHook: ({ fetch }) => () => {
const { revalidate } = useCustomer()

View File

@ -1,3 +1,4 @@
export { default as useCart } from './use-cart'
export { default as useAddItem } from './use-add-item'
export { default as useUpdateItem } from './use-update-item'
export { default as useRemoveItem } from './use-remove-item'

View File

@ -1,12 +1,15 @@
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 useCart from './use-cart'
import {
checkoutLineItemAddMutation,
getCheckoutId,
checkoutToCart,
} from '../utils'
import { Cart, CartItemBody } from '../types'
import { checkoutLineItemAddMutation, getCheckoutId } from '../utils'
import { checkoutToCart } from './utils'
import { Mutation, MutationCheckoutLineItemsAddArgs } from '../schema'
import { useCallback } from 'react'
export default useAddItem as UseAddItem<typeof handler>
@ -40,8 +43,7 @@ export const handler: MutationHook<Cart, {}, CartItemBody> = {
},
})
// TODO: Fix this Cart type here
return checkoutToCart(checkoutLineItemsAdd) as any
return checkoutToCart(checkoutLineItemsAdd)
},
useHook: ({ fetch }) => () => {
const { mutate } = useCart()

View File

@ -6,7 +6,7 @@ import useCommerceCart, {
import { Cart } from '../types'
import { SWRHook } from '@commerce/utils/types'
import { checkoutCreate, checkoutToCart } from './utils'
import { checkoutCreate, checkoutToCart } from '../utils'
import getCheckoutQuery from '../utils/queries/get-checkout-query'
export default useCommerceCart as UseCart<typeof handler>
@ -22,11 +22,12 @@ export const handler: SWRHook<
},
async fetcher({ input: { cartId: checkoutId }, options, fetch }) {
let checkout
if (checkoutId) {
const data = await fetch({
...options,
variables: {
checkoutId,
checkoutId: checkoutId,
},
})
checkout = data.node
@ -36,8 +37,7 @@ export const handler: SWRHook<
checkout = await checkoutCreate(fetch)
}
// TODO: Fix this type
return checkoutToCart({ checkout } as any)
return checkoutToCart({ checkout })
},
useHook: ({ useData }) => (input) => {
const response = useData({

View File

@ -1,23 +1,22 @@
import { useCallback } from 'react'
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import { RemoveCartItemBody } from '@commerce/types'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, {
RemoveItemInput as RemoveItemInputBase,
UseRemoveItem,
} from '@commerce/cart/use-remove-item'
import useCart from './use-cart'
import { checkoutLineItemRemoveMutation, getCheckoutId } from '../utils'
import { checkoutToCart } from './utils'
import {
checkoutLineItemRemoveMutation,
getCheckoutId,
checkoutToCart,
} from '../utils'
import { Cart, LineItem } from '../types'
import { Mutation, MutationCheckoutLineItemsRemoveArgs } from '../schema'
import { RemoveCartItemBody } from '@commerce/types'
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemInput<T>) => Promise<Cart | null>

View File

@ -13,7 +13,7 @@ import useUpdateItem, {
import useCart from './use-cart'
import { handler as removeItemHandler } from './use-remove-item'
import type { Cart, LineItem, UpdateCartItemBody } from '../types'
import { checkoutToCart } from './utils'
import { checkoutToCart } from '../utils'
import { getCheckoutId, checkoutLineItemUpdateMutation } from '../utils'
import { Mutation, MutationCheckoutLineItemsUpdateArgs } from '../schema'

View File

@ -1,42 +0,0 @@
import { Cart } from '../../types'
import { CommerceError, ValidationError } from '@commerce/utils/errors'
import {
CheckoutLineItemsAddPayload,
CheckoutLineItemsRemovePayload,
CheckoutLineItemsUpdatePayload,
Maybe,
} from '../../schema'
import { normalizeCart } from '../../utils'
export type CheckoutPayload =
| CheckoutLineItemsAddPayload
| CheckoutLineItemsUpdatePayload
| CheckoutLineItemsRemovePayload
const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => {
if (!checkoutPayload) {
throw new CommerceError({
message: 'Invalid response from Shopify',
})
}
const checkout = checkoutPayload?.checkout
const userErrors = checkoutPayload?.userErrors
if (userErrors && userErrors.length) {
throw new ValidationError({
message: userErrors[0].message,
})
}
if (!checkout) {
throw new CommerceError({
message: 'Invalid response from Shopify',
})
}
return normalizeCart(checkout)
}
export default checkoutToCart

View File

@ -1,31 +0,0 @@
import { HookFetcherFn } from '@commerce/utils/types'
import { Cart } from '@commerce/types'
import { checkoutCreate, checkoutToCart } from '.'
import { FetchCartInput } from '@commerce/cart/use-cart'
const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
options,
input: { cartId: checkoutId },
fetch,
}) => {
let checkout
if (checkoutId) {
const data = await fetch({
...options,
variables: {
checkoutId,
},
})
checkout = data.node
}
if (checkout?.completedAt || !checkoutId) {
checkout = await checkoutCreate(fetch)
}
// TODO: Fix this type
return checkoutToCart({ checkout } as any)
}
export default fetcher

View File

@ -1,2 +0,0 @@
export { default as checkoutToCart } from './checkout-to-cart'
export { default as checkoutCreate } from './checkout-create'

View File

@ -2,9 +2,14 @@ import { Fetcher } from '@commerce/utils/types'
import { API_TOKEN, API_URL } from './const'
import { handleFetchResponse } from './utils'
const fetcher: Fetcher = async ({ method = 'POST', variables, query }) => {
const fetcher: Fetcher = async ({
url = API_URL,
method = 'POST',
variables,
query,
}) => {
return handleFetchResponse(
await fetch(API_URL, {
await fetch(url, {
method,
body: JSON.stringify({ query, variables }),
headers: {

View File

@ -28,8 +28,7 @@ export type ShopifyProps = {
export function CommerceProvider({ children, ...config }: ShopifyProps) {
return (
<CoreCommerceProvider
// TODO: Fix this type
provider={shopifyProvider as any}
provider={shopifyProvider}
config={{ ...shopifyConfig, ...config }}
>
{children}

View File

@ -21,11 +21,10 @@ const getProduct = async (options: {
const { data }: GraphQLFetcherResult = await config.fetch(getProductQuery, {
variables,
})
const { productByHandle: product } = data
const { productByHandle } = data
return {
product: product ? normalizeProduct(product) : null,
product: productByHandle ? normalizeProduct(productByHandle) : null,
}
}

View File

@ -48,7 +48,8 @@ export const handler: SWRHook<
edges = data.node?.products?.edges ?? []
if (brandId) {
edges = edges.filter(
({ node: { vendor } }: ProductEdge) => vendor === brandId
({ node: { vendor } }: ProductEdge) =>
vendor.replace(/\s+/g, '-').toLowerCase() === brandId
)
}
} else {

View File

@ -1,4 +1,4 @@
import { SHOPIFY_CHECKOUT_ID_COOKIE, STORE_DOMAIN } from './const'
import { SHOPIFY_CHECKOUT_ID_COOKIE } from './const'
import { handler as useCart } from './cart/use-cart'
import { handler as useAddItem } from './cart/use-add-item'
@ -17,15 +17,11 @@ import fetcher from './fetcher'
export const shopifyProvider = {
locale: 'en-us',
cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE,
storeDomain: STORE_DOMAIN,
fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer },
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
features: {
wishlist: false,
},
}
export type ShopifyProvider = typeof shopifyProvider

View File

@ -7,13 +7,11 @@ export type ShopifyCheckout = {
lineItems: CheckoutLineItem[]
}
export interface Cart extends Core.Cart {
id: string
export type Cart = Core.Cart & {
lineItems: LineItem[]
}
export interface LineItem extends Core.LineItem {
options: any[]
options?: any[]
}
/**

View File

@ -1,13 +1,17 @@
import Cookies from 'js-cookie'
import {
SHOPIFY_CHECKOUT_ID_COOKIE,
SHOPIFY_CHECKOUT_URL_COOKIE,
SHOPIFY_COOKIE_EXPIRE,
} from '../../const'
} from '../const'
import checkoutCreateMutation from '../../utils/mutations/checkout-create'
import Cookies from 'js-cookie'
import checkoutCreateMutation from './mutations/checkout-create'
import { CheckoutCreatePayload } from '../schema'
export const checkoutCreate = async (fetch: any) => {
export const checkoutCreate = async (
fetch: any
): Promise<CheckoutCreatePayload> => {
const data = await fetch({
query: checkoutCreateMutation,
})
@ -20,7 +24,7 @@ export const checkoutCreate = async (fetch: any) => {
expires: SHOPIFY_COOKIE_EXPIRE,
}
Cookies.set(SHOPIFY_CHECKOUT_ID_COOKIE, checkoutId, options)
Cookies.set(SHOPIFY_CHECKOUT_URL_COOKIE, checkout?.webUrl, options)
Cookies.set(SHOPIFY_CHECKOUT_URL_COOKIE, checkout.webUrl, options)
}
return checkout

View File

@ -0,0 +1,48 @@
import { Cart } from '../types'
import { CommerceError } from '@commerce/utils/errors'
import {
CheckoutLineItemsAddPayload,
CheckoutLineItemsRemovePayload,
CheckoutLineItemsUpdatePayload,
CheckoutCreatePayload,
CheckoutUserError,
Checkout,
Maybe,
} from '../schema'
import { normalizeCart } from './normalize'
import throwUserErrors from './throw-user-errors'
export type CheckoutQuery = {
checkout: Checkout
checkoutUserErrors?: Array<CheckoutUserError>
}
export type CheckoutPayload =
| CheckoutLineItemsAddPayload
| CheckoutLineItemsUpdatePayload
| CheckoutLineItemsRemovePayload
| CheckoutCreatePayload
| CheckoutQuery
const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => {
if (!checkoutPayload) {
throw new CommerceError({
message: 'Missing checkout payload from response',
})
}
const checkout = checkoutPayload?.checkout
throwUserErrors(checkoutPayload?.checkoutUserErrors)
if (!checkout) {
throw new CommerceError({
message: 'Missing checkout object from response',
})
}
return normalizeCart(checkout)
}
export default checkoutToCart

View File

@ -1,4 +1,4 @@
const getSortVariables = (sort?: string, isCategory = false) => {
const getSortVariables = (sort?: string, isCategory: boolean = false) => {
let output = {}
switch (sort) {
case 'price-asc':

View File

@ -2,13 +2,14 @@ import { ShopifyConfig } from '../api'
import fetchAllProducts from '../api/utils/fetch-all-products'
import getAllProductVendors from './queries/get-all-product-vendors-query'
export type BrandNode = {
export type Brand = {
entityId: string
name: string
path: string
}
export type BrandEdge = {
node: BrandNode
node: Brand
}
export type Brands = BrandEdge[]
@ -24,13 +25,16 @@ const getVendors = async (config: ShopifyConfig): Promise<BrandEdge[]> => {
let vendorsStrings = vendors.map(({ node: { vendor } }) => vendor)
return [...new Set(vendorsStrings)].map((v) => ({
node: {
entityId: v,
name: v,
path: `brands/${v}`,
},
}))
return [...new Set(vendorsStrings)].map((v) => {
const id = v.replace(/\s+/g, '-').toLowerCase()
return {
node: {
entityId: id,
name: v,
path: `brands/${id}`,
},
}
})
}
export default getVendors

View File

@ -0,0 +1,30 @@
import { FetcherOptions } from '@commerce/utils/types'
import throwUserErrors from './throw-user-errors'
import {
MutationCustomerActivateArgs,
MutationCustomerActivateByUrlArgs,
} from '../schema'
import { Mutation } from '../schema'
import { customerActivateByUrlMutation } from './mutations'
const handleAccountActivation = async (
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
input: MutationCustomerActivateByUrlArgs
) => {
try {
const { customerActivateByUrl } = await fetch<
Mutation,
MutationCustomerActivateArgs
>({
query: customerActivateByUrlMutation,
variables: {
input,
},
})
throwUserErrors(customerActivateByUrl?.customerUserErrors)
} catch (error) {}
}
export default handleAccountActivation

View File

@ -1,30 +1,12 @@
import { ValidationError } from '@commerce/utils/errors'
import { FetcherOptions } from '@commerce/utils/types'
import { CustomerAccessTokenCreateInput } from '../schema'
import { setCustomerToken } from './customer-token'
const getErrorMessage = ({
code,
message,
}: {
code: string
message: string
}) => {
switch (code) {
case 'UNIDENTIFIED_CUSTOMER':
message = 'Cannot find an account that matches the provided credentials'
break
}
return message
}
import { customerAccessTokenCreateMutation } from './mutations'
import throwUserErrors from './throw-user-errors'
const handleLogin = (data: any) => {
const response = data.customerAccessTokenCreate
const errors = response?.customerUserErrors
if (errors && errors.length) {
throw new ValidationError({
message: getErrorMessage(errors[0]),
})
}
throwUserErrors(response?.customerUserErrors)
const customerAccessToken = response?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
@ -36,4 +18,19 @@ const handleLogin = (data: any) => {
return customerAccessToken
}
export const handleAutomaticLogin = async (
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
input: CustomerAccessTokenCreateInput
) => {
try {
const loginData = await fetch({
query: customerAccessTokenCreateMutation,
variables: {
input,
},
})
handleLogin(loginData)
} catch (error) {}
}
export default handleLogin

View File

@ -4,6 +4,11 @@ export { default as getSortVariables } from './get-sort-variables'
export { default as getVendors } from './get-vendors'
export { default as getCategories } from './get-categories'
export { default as getCheckoutId } from './get-checkout-id'
export { default as checkoutCreate } from './checkout-create'
export { default as checkoutToCart } from './checkout-to-cart'
export { default as handleLogin, handleAutomaticLogin } from './handle-login'
export { default as handleAccountActivation } from './handle-account-activation'
export { default as throwUserErrors } from './throw-user-errors'
export * from './queries'
export * from './mutations'
export * from './normalize'

View File

@ -3,9 +3,10 @@ import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutCreateMutation = /* GraphQL */ `
mutation {
checkoutCreate(input: {}) {
userErrors {
message
checkoutUserErrors {
code
field
message
}
checkout {
${checkoutDetailsFragment}

View File

@ -3,9 +3,10 @@ import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutLineItemAddMutation = /* GraphQL */ `
mutation($checkoutId: ID!, $lineItems: [CheckoutLineItemInput!]!) {
checkoutLineItemsAdd(checkoutId: $checkoutId, lineItems: $lineItems) {
userErrors {
message
checkoutUserErrors {
code
field
message
}
checkout {
${checkoutDetailsFragment}

View File

@ -6,9 +6,10 @@ const checkoutLineItemRemoveMutation = /* GraphQL */ `
checkoutId: $checkoutId
lineItemIds: $lineItemIds
) {
userErrors {
message
checkoutUserErrors {
code
field
message
}
checkout {
${checkoutDetailsFragment}

View File

@ -3,9 +3,10 @@ import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutLineItemUpdateMutation = /* GraphQL */ `
mutation($checkoutId: ID!, $lineItems: [CheckoutLineItemUpdateInput!]!) {
checkoutLineItemsUpdate(checkoutId: $checkoutId, lineItems: $lineItems) {
userErrors {
message
checkoutUserErrors {
code
field
message
}
checkout {
${checkoutDetailsFragment}

View File

@ -0,0 +1,19 @@
const customerActivateByUrlMutation = /* GraphQL */ `
mutation customerActivateByUrl($activationUrl: URL!, $password: String!) {
customerActivateByUrl(activationUrl: $activationUrl, password: $password) {
customer {
id
}
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerActivateByUrlMutation

View File

@ -0,0 +1,19 @@
const customerActivateMutation = /* GraphQL */ `
mutation customerActivate($id: ID!, $input: CustomerActivateInput!) {
customerActivate(id: $id, input: $input) {
customer {
id
}
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerActivateMutation

View File

@ -5,3 +5,5 @@ export { default as checkoutLineItemUpdateMutation } from './checkout-line-item-
export { default as checkoutLineItemRemoveMutation } from './checkout-line-item-remove'
export { default as customerAccessTokenCreateMutation } from './customer-access-token-create'
export { default as customerAccessTokenDeleteMutation } from './customer-access-token-delete'
export { default as customerActivateMutation } from './customer-activate'
export { default as customerActivateByUrlMutation } from './customer-activate-by-url'

View File

@ -33,7 +33,7 @@ const normalizeProductOption = ({
let output: any = {
label: value,
}
if (displayName === 'Color') {
if (displayName.match(/colou?r/gi)) {
output = {
...output,
hexColors: [value],
@ -54,21 +54,24 @@ const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
return edges?.map(
({
node: { id, selectedOptions, sku, title, priceV2, compareAtPriceV2 },
}) => ({
id,
name: title,
sku: sku ?? id,
price: +priceV2.amount,
listPrice: +compareAtPriceV2?.amount,
requiresShipping: true,
options: selectedOptions.map(({ name, value }: SelectedOption) =>
normalizeProductOption({
id,
name,
values: [value],
})
),
})
}) => {
return {
id,
name: title,
sku: sku ?? id,
price: +priceV2.amount,
listPrice: +compareAtPriceV2?.amount,
requiresShipping: true,
options: selectedOptions.map(({ name, value }: SelectedOption) => {
const options = normalizeProductOption({
id,
name,
values: [value],
})
return options
}),
}
}
)
}
@ -80,6 +83,7 @@ export function normalizeProduct(productNode: ShopifyProduct): Product {
images,
variants,
description,
descriptionHtml,
handle,
priceRange,
options,
@ -90,13 +94,18 @@ export function normalizeProduct(productNode: ShopifyProduct): Product {
id,
name,
vendor,
description,
path: `/${handle}`,
slug: handle?.replace(/^\/+|\/+$/g, ''),
price: money(priceRange?.minVariantPrice),
images: normalizeProductImages(images),
variants: variants ? normalizeProductVariants(variants) : [],
options: options ? options.map((o) => normalizeProductOption(o)) : [],
options: options
? options
.filter((o) => o.name !== 'Title') // By default Shopify adds a 'Title' name when there's only one option. We don't need it. https://community.shopify.com/c/Shopify-APIs-SDKs/Adding-new-product-variant-is-automatically-adding-quot-Default/td-p/358095
.map((o) => normalizeProductOption(o))
: [],
...(description && { description }),
...(descriptionHtml && { descriptionHtml }),
...rest,
}
@ -122,7 +131,7 @@ export function normalizeCart(checkout: Checkout): Cart {
}
function normalizeLineItem({
node: { id, title, variant, quantity },
node: { id, title, variant, quantity, ...rest },
}: CheckoutLineItemEdge): LineItem {
return {
id,
@ -135,18 +144,22 @@ function normalizeLineItem({
sku: variant?.sku ?? '',
name: variant?.title!,
image: {
url: variant?.image?.originalSrc,
url: variant?.image?.originalSrc ?? '/product-img-placeholder.svg',
},
requiresShipping: variant?.requiresShipping ?? false,
price: variant?.priceV2?.amount,
listPrice: variant?.compareAtPriceV2?.amount,
},
path: '',
path: String(variant?.product?.handle),
discounts: [],
options: [
{
value: variant?.title,
},
],
options:
// By default Shopify adds a default variant with default names, we're removing it. https://community.shopify.com/c/Shopify-APIs-SDKs/Adding-new-product-variant-is-automatically-adding-quot-Default/td-p/358095
variant?.title == 'Default Title'
? []
: [
{
value: variant?.title,
},
],
}
}

View File

@ -9,7 +9,6 @@ edges {
title
vendor
handle
description
priceRange {
minVariantPrice {
amount

View File

@ -43,6 +43,9 @@ export const checkoutDetailsFragment = `
amount
currencyCode
}
product {
handle
}
}
quantity
}

View File

@ -1,13 +0,0 @@
export const getCheckoutIdFromStorage = (token: string) => {
if (window && window.sessionStorage) {
return window.sessionStorage.getItem(token)
}
return null
}
export const setCheckoutIdInStorage = (token: string, id: string | number) => {
if (window && window.sessionStorage) {
return window.sessionStorage.setItem(token, id + '')
}
}

View File

@ -0,0 +1,38 @@
import { ValidationError } from '@commerce/utils/errors'
import {
CheckoutErrorCode,
CheckoutUserError,
CustomerErrorCode,
CustomerUserError,
} from '../schema'
export type UserErrors = Array<CheckoutUserError | CustomerUserError>
export type UserErrorCode =
| CustomerErrorCode
| CheckoutErrorCode
| null
| undefined
const getCustomMessage = (code: UserErrorCode, message: string) => {
switch (code) {
case 'UNIDENTIFIED_CUSTOMER':
message = 'Cannot find an account that matches the provided credentials'
break
}
return message
}
export const throwUserErrors = (errors?: UserErrors) => {
if (errors && errors.length) {
throw new ValidationError({
errors: errors.map(({ code, message }) => ({
code: code ?? 'validation_error',
message: getCustomMessage(code, message),
})),
})
}
}
export default throwUserErrors

View File

@ -1,11 +1,18 @@
const commerce = require('./commerce.config.json')
const withCommerceConfig = require('./framework/commerce/with-config')
const {
withCommerceConfig,
getProviderName,
} = require('./framework/commerce/config')
const isBC = commerce.provider === 'bigcommerce'
const isShopify = commerce.provider === 'shopify'
const isRC = commerce.provider === 'reactioncommerce'
const provider = commerce.provider || getProviderName()
const isBC = provider === 'bigcommerce'
const isShopify = provider === 'shopify'
const isRC = provider === 'reactioncommerce'
module.exports = withCommerceConfig({
env: {
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
},
commerce,
i18n: {
locales: ['en-US', 'es'],
@ -13,7 +20,7 @@ module.exports = withCommerceConfig({
},
rewrites() {
return [
(isBC || isShopify || isRC) && {
(isBC || isShopify) && {
source: '/checkout',
destination: '/api/bigcommerce/checkout',
},
@ -40,3 +47,6 @@ module.exports = withCommerceConfig({
].filter((x) => x)
},
})
// Don't delete this console log, useful to see the commerce config in Vercel deployments
console.log('next.config.js', JSON.stringify(module.exports, null, 2))

View File

@ -33,7 +33,7 @@
"lodash.debounce": "^4.0.8",
"lodash.random": "^3.2.0",
"lodash.throttle": "^4.1.1",
"next": "^10.0.7",
"next": "^10.0.9-canary.5",
"next-seo": "^4.11.0",
"next-themes": "^0.0.4",
"postcss": "^8.2.6",
@ -74,9 +74,6 @@
"prettier": "^2.2.1",
"typescript": "^4.0.3"
},
"resolutions": {
"webpack": "5.11.1"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"

View File

@ -25,8 +25,7 @@ export async function getStaticProps({
const pageItem = pages.find((p) => (p.url ? getSlug(p.url) === slug : false))
const data =
pageItem &&
// TODO: Shopify - Fix this type
(await getPage({ variables: { id: pageItem.id! } as any, config, preview }))
(await getPage({ variables: { id: pageItem.id! }, config, preview }))
const page = data?.page
if (!page) {

View File

@ -77,10 +77,8 @@ export default function Search({
const { data } = useSearch({
search: typeof q === 'string' ? q : '',
// TODO: Shopify - Fix this type
categoryId: activeCategory?.entityId as any,
// TODO: Shopify - Fix this type
brandId: (activeBrand as any)?.entityId,
categoryId: activeCategory?.entityId,
brandId: activeBrand?.entityId,
sort: typeof sort === 'string' ? sort : '',
shopId,
})

Some files were not shown because too many files have changed in this diff Show More