}
-export type CommerceAPI<
- P extends APIProvider = APIProvider
-> = CommerceAPICore & AllOperations
+export type CommerceAPI
=
+ CommerceAPICore
& AllOperations
export class CommerceAPICore
{
constructor(readonly provider: P) {}
@@ -134,17 +133,17 @@ export function getEndpoint<
}
}
-export const createEndpoint = >(
- endpoint: API['endpoint']
-) => (
- commerce: CommerceAPI
,
- context?: Partial & {
- config?: P['config']
- options?: API['schema']['endpoint']['options']
+export const createEndpoint =
+ >(endpoint: API['endpoint']) =>
+ (
+ commerce: CommerceAPI
,
+ context?: Partial & {
+ config?: P['config']
+ options?: API['schema']['endpoint']['options']
+ }
+ ): NextApiHandler => {
+ return getEndpoint(commerce, { ...endpoint, ...context })
}
-): NextApiHandler => {
- return getEndpoint(commerce, { ...endpoint, ...context })
-}
export interface CommerceAPIConfig {
locale?: string
diff --git a/framework/commerce/config.js b/framework/commerce/config.js
index 48f0d526b..b31ba06e0 100644
--- a/framework/commerce/config.js
+++ b/framework/commerce/config.js
@@ -7,7 +7,7 @@ const fs = require('fs')
const merge = require('deepmerge')
const prettier = require('prettier')
-const PROVIDERS = ['bigcommerce', 'shopify', 'swell', 'vendure']
+const PROVIDERS = ['bigcommerce', 'shopify', 'swell', 'vendure', 'local']
function getProviderName() {
return (
@@ -18,7 +18,7 @@ function getProviderName() {
? 'shopify'
: process.env.NEXT_PUBLIC_SWELL_STORE_ID
? 'swell'
- : null)
+ : 'local')
)
}
@@ -40,7 +40,7 @@ function withCommerceConfig(nextConfig = {}) {
}
const commerceNextConfig = require(path.join('../', name, 'next.config'))
- const config = merge(commerceNextConfig, nextConfig)
+ const config = merge(nextConfig, commerceNextConfig)
config.env = config.env || {}
@@ -50,27 +50,11 @@ function withCommerceConfig(nextConfig = {}) {
// 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}/*`]
-
- // When running for production it may be useful to exclude the other providers
- // from TS checking
- if (process.env.VERCEL) {
- const exclude = tsconfig.exclude.filter(
- (item) => !item.startsWith('framework/')
- )
-
- tsconfig.exclude = PROVIDERS.reduce((exclude, current) => {
- if (current !== name) exclude.push(`framework/${current}`)
- return exclude
- }, exclude)
- }
+ const staticTsconfigPath = path.join(process.cwd(), 'tsconfig.json')
+ const tsconfig = require('../../tsconfig.js')
fs.writeFileSync(
- tsconfigPath,
+ staticTsconfigPath,
prettier.format(JSON.stringify(tsconfig), { parser: 'json' })
)
}
diff --git a/framework/commerce/new-provider.md b/framework/commerce/new-provider.md
index 511704af6..8c2feeab2 100644
--- a/framework/commerce/new-provider.md
+++ b/framework/commerce/new-provider.md
@@ -3,6 +3,7 @@
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))
+- Saleor ([framework/saleor](../saleor))
- Shopify ([framework/shopify](../shopify))
Adding a commerce provider means adding a new folder in `framework` with a folder structure like the next one:
@@ -57,7 +58,8 @@ import {
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
} from '@commerce'
-import { bigcommerceProvider, BigcommerceProvider } from './provider'
+import { bigcommerceProvider } from './provider'
+import type { BigcommerceProvider } from './provider'
export { bigcommerceProvider }
export type { BigcommerceProvider }
@@ -156,24 +158,26 @@ export const handler: SWRHook<
const data = cartId ? await fetch(options) : null
return data && normalizeCart(data)
},
- useHook: ({ useData }) => (input) => {
- const response = useData({
- swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
- })
+ 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
+ return useMemo(
+ () =>
+ Object.create(response, {
+ isEmpty: {
+ get() {
+ return (response.data?.lineItems.length ?? 0) <= 0
+ },
+ enumerable: true,
},
- enumerable: true,
- },
- }),
- [response]
- )
- },
+ }),
+ [response]
+ )
+ },
}
```
@@ -217,18 +221,20 @@ export const handler: MutationHook = {
return normalizeCart(data)
},
- useHook: ({ fetch }) => () => {
- const { mutate } = useCart()
+ useHook:
+ ({ fetch }) =>
+ () => {
+ const { mutate } = useCart()
- return useCallback(
- async function addItem(input) {
- const data = await fetch({ input })
- await mutate(data, false)
- return data
- },
- [fetch, mutate]
- )
- },
+ return useCallback(
+ async function addItem(input) {
+ const data = await fetch({ input })
+ await mutate(data, false)
+ return data
+ },
+ [fetch, mutate]
+ )
+ },
}
```
diff --git a/framework/commerce/types/cart.ts b/framework/commerce/types/cart.ts
index 7826f9b2d..e4af878de 100644
--- a/framework/commerce/types/cart.ts
+++ b/framework/commerce/types/cart.ts
@@ -165,15 +165,13 @@ export type AddItemHandler = AddItemHook & {
body: { cartId: string }
}
-export type UpdateItemHandler<
- T extends CartTypes = CartTypes
-> = UpdateItemHook & {
- data: T['cart']
- body: { cartId: string }
-}
+export type UpdateItemHandler =
+ UpdateItemHook & {
+ data: T['cart']
+ body: { cartId: string }
+ }
-export type RemoveItemHandler<
- T extends CartTypes = CartTypes
-> = RemoveItemHook & {
- body: { cartId: string }
-}
+export type RemoveItemHandler =
+ RemoveItemHook & {
+ body: { cartId: string }
+ }
diff --git a/framework/commerce/utils/define-property.ts b/framework/commerce/utils/define-property.ts
index e89735226..875aaaa82 100644
--- a/framework/commerce/utils/define-property.ts
+++ b/framework/commerce/utils/define-property.ts
@@ -11,18 +11,16 @@ type InferValue = Desc extends {
? Record
: never
-type DefineProperty<
- Prop extends PropertyKey,
- Desc extends PropertyDescriptor
-> = Desc extends { writable: any; set(val: any): any }
- ? never
- : Desc extends { writable: any; get(): any }
- ? never
- : Desc extends { writable: false }
- ? Readonly>
- : Desc extends { writable: true }
- ? InferValue
- : Readonly>
+type DefineProperty =
+ Desc extends { writable: any; set(val: any): any }
+ ? never
+ : Desc extends { writable: any; get(): any }
+ ? never
+ : Desc extends { writable: false }
+ ? Readonly>
+ : Desc extends { writable: true }
+ ? InferValue
+ : Readonly>
export default function defineProperty<
Obj extends object,
diff --git a/framework/local/.env.template b/framework/local/.env.template
new file mode 100644
index 000000000..873eea7e7
--- /dev/null
+++ b/framework/local/.env.template
@@ -0,0 +1 @@
+COMMERCE_PROVIDER=local
\ No newline at end of file
diff --git a/framework/local/README.md b/framework/local/README.md
new file mode 100644
index 000000000..a3bc1db32
--- /dev/null
+++ b/framework/local/README.md
@@ -0,0 +1 @@
+# Next.js Local Provider
diff --git a/framework/local/api/endpoints/cart/index.ts b/framework/local/api/endpoints/cart/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/cart/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/api/endpoints/catalog/index.ts b/framework/local/api/endpoints/catalog/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/catalog/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/api/endpoints/catalog/products.ts b/framework/local/api/endpoints/catalog/products.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/catalog/products.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/api/endpoints/checkout/index.ts b/framework/local/api/endpoints/checkout/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/checkout/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/api/endpoints/customer/index.ts b/framework/local/api/endpoints/customer/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/customer/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/api/endpoints/login/index.ts b/framework/local/api/endpoints/login/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/login/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/api/endpoints/logout/index.ts b/framework/local/api/endpoints/logout/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/logout/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/api/endpoints/signup/index.ts b/framework/local/api/endpoints/signup/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/signup/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/api/endpoints/wishlist/index.tsx b/framework/local/api/endpoints/wishlist/index.tsx
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/local/api/endpoints/wishlist/index.tsx
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/local/api/index.ts b/framework/local/api/index.ts
new file mode 100644
index 000000000..9f403d2b3
--- /dev/null
+++ b/framework/local/api/index.ts
@@ -0,0 +1,42 @@
+import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api'
+import { getCommerceApi as commerceApi } from '@commerce/api'
+import createFetcher from './utils/fetch-local'
+
+import getAllPages from './operations/get-all-pages'
+import getPage from './operations/get-page'
+import getSiteInfo from './operations/get-site-info'
+import getCustomerWishlist from './operations/get-customer-wishlist'
+import getAllProductPaths from './operations/get-all-product-paths'
+import getAllProducts from './operations/get-all-products'
+import getProduct from './operations/get-product'
+
+export interface LocalConfig extends CommerceAPIConfig {}
+const config: LocalConfig = {
+ commerceUrl: '',
+ apiToken: '',
+ cartCookie: '',
+ customerCookie: '',
+ cartCookieMaxAge: 2592000,
+ fetch: createFetcher(() => getCommerceApi().getConfig()),
+}
+
+const operations = {
+ getAllPages,
+ getPage,
+ getSiteInfo,
+ getCustomerWishlist,
+ getAllProductPaths,
+ getAllProducts,
+ getProduct,
+}
+
+export const provider = { config, operations }
+
+export type Provider = typeof provider
+export type LocalAPI = CommerceAPI
+
+export function getCommerceApi
(
+ customProvider: P = provider as any
+): LocalAPI
{
+ return commerceApi(customProvider as any)
+}
diff --git a/framework/local/api/operations/get-all-pages.ts b/framework/local/api/operations/get-all-pages.ts
new file mode 100644
index 000000000..b258fe70a
--- /dev/null
+++ b/framework/local/api/operations/get-all-pages.ts
@@ -0,0 +1,19 @@
+export type Page = { url: string }
+export type GetAllPagesResult = { pages: Page[] }
+import type { LocalConfig } from '../index'
+
+export default function getAllPagesOperation() {
+ function getAllPages({
+ config,
+ preview,
+ }: {
+ url?: string
+ config?: Partial
+ preview?: boolean
+ }): Promise {
+ return Promise.resolve({
+ pages: [],
+ })
+ }
+ return getAllPages
+}
diff --git a/framework/local/api/operations/get-all-product-paths.ts b/framework/local/api/operations/get-all-product-paths.ts
new file mode 100644
index 000000000..fff24e791
--- /dev/null
+++ b/framework/local/api/operations/get-all-product-paths.ts
@@ -0,0 +1,15 @@
+import data from '../../data.json'
+
+export type GetAllProductPathsResult = {
+ products: Array<{ path: string }>
+}
+
+export default function getAllProductPathsOperation() {
+ function getAllProductPaths(): Promise {
+ return Promise.resolve({
+ products: data.products.map(({ path }) => ({ path })),
+ })
+ }
+
+ return getAllProductPaths
+}
diff --git a/framework/local/api/operations/get-all-products.ts b/framework/local/api/operations/get-all-products.ts
new file mode 100644
index 000000000..abb1c02ae
--- /dev/null
+++ b/framework/local/api/operations/get-all-products.ts
@@ -0,0 +1,25 @@
+import { Product } from '@commerce/types/product'
+import { GetAllProductsOperation } from '@commerce/types/product'
+import type { OperationContext } from '@commerce/api/operations'
+import type { LocalConfig, Provider } from '../index'
+import data from '../../data.json'
+
+export default function getAllProductsOperation({
+ commerce,
+}: OperationContext) {
+ async function getAllProducts({
+ query = '',
+ variables,
+ config,
+ }: {
+ query?: string
+ variables?: T['variables']
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise<{ products: Product[] | any[] }> {
+ return Promise.resolve({
+ products: data.products,
+ })
+ }
+ return getAllProducts
+}
diff --git a/framework/local/api/operations/get-customer-wishlist.ts b/framework/local/api/operations/get-customer-wishlist.ts
new file mode 100644
index 000000000..8c34b9e87
--- /dev/null
+++ b/framework/local/api/operations/get-customer-wishlist.ts
@@ -0,0 +1,6 @@
+export default function getCustomerWishlistOperation() {
+ function getCustomerWishlist(): any {
+ return { wishlist: {} }
+ }
+ return getCustomerWishlist
+}
diff --git a/framework/local/api/operations/get-page.ts b/framework/local/api/operations/get-page.ts
new file mode 100644
index 000000000..ef2867fcc
--- /dev/null
+++ b/framework/local/api/operations/get-page.ts
@@ -0,0 +1,12 @@
+export type Page = any
+export type GetPageResult = { page?: Page }
+export type PageVariables = {
+ id: number
+}
+
+export default function getPageOperation() {
+ function getPage(): Promise {
+ return Promise.resolve({})
+ }
+ return getPage
+}
diff --git a/framework/local/api/operations/get-product.ts b/framework/local/api/operations/get-product.ts
new file mode 100644
index 000000000..7a47d08c2
--- /dev/null
+++ b/framework/local/api/operations/get-product.ts
@@ -0,0 +1,26 @@
+import type { LocalConfig } from '../index'
+import { Product } from '@commerce/types/product'
+import { GetProductOperation } from '@commerce/types/product'
+import data from '../../data.json'
+import type { OperationContext } from '@commerce/api/operations'
+
+export default function getProductOperation({
+ commerce,
+}: OperationContext) {
+ async function getProduct({
+ query = '',
+ variables,
+ config,
+ }: {
+ query?: string
+ variables?: T['variables']
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ return Promise.resolve({
+ product: data.products[0],
+ })
+ }
+
+ return getProduct
+}
diff --git a/framework/local/api/operations/get-site-info.ts b/framework/local/api/operations/get-site-info.ts
new file mode 100644
index 000000000..180a6e9b4
--- /dev/null
+++ b/framework/local/api/operations/get-site-info.ts
@@ -0,0 +1,30 @@
+import { OperationContext } from '@commerce/api/operations'
+import { Category } from '@commerce/types/site'
+import { LocalConfig } from '../index'
+
+export type GetSiteInfoResult<
+ T extends { categories: any[]; brands: any[] } = {
+ categories: Category[]
+ brands: any[]
+ }
+> = T
+
+export default function getSiteInfoOperation({}: OperationContext) {
+ function getSiteInfo({
+ query,
+ variables,
+ config: cfg,
+ }: {
+ query?: string
+ variables?: any
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ return Promise.resolve({
+ categories: [],
+ brands: [],
+ })
+ }
+
+ return getSiteInfo
+}
diff --git a/framework/local/api/operations/index.ts b/framework/local/api/operations/index.ts
new file mode 100644
index 000000000..086fdf83a
--- /dev/null
+++ b/framework/local/api/operations/index.ts
@@ -0,0 +1,6 @@
+export { default as getPage } from './get-page'
+export { default as getSiteInfo } from './get-site-info'
+export { default as getAllPages } from './get-all-pages'
+export { default as getProduct } from './get-product'
+export { default as getAllProducts } from './get-all-products'
+export { default as getAllProductPaths } from './get-all-product-paths'
diff --git a/framework/local/api/utils/fetch-local.ts b/framework/local/api/utils/fetch-local.ts
new file mode 100644
index 000000000..aa85cf27b
--- /dev/null
+++ b/framework/local/api/utils/fetch-local.ts
@@ -0,0 +1,34 @@
+import { FetcherError } from '@commerce/utils/errors'
+import type { GraphQLFetcher } from '@commerce/api'
+import type { LocalConfig } from '../index'
+import fetch from './fetch'
+
+const fetchGraphqlApi: (getConfig: () => LocalConfig) => GraphQLFetcher =
+ (getConfig) =>
+ async (query: string, { variables, preview } = {}, fetchOptions) => {
+ const config = getConfig()
+ const res = await fetch(config.commerceUrl, {
+ ...fetchOptions,
+ method: 'POST',
+ headers: {
+ ...fetchOptions?.headers,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query,
+ variables,
+ }),
+ })
+
+ const json = await res.json()
+ if (json.errors) {
+ throw new FetcherError({
+ errors: json.errors ?? [{ message: 'Failed to fetch for API' }],
+ status: res.status,
+ })
+ }
+
+ return { data: json.data, res }
+ }
+
+export default fetchGraphqlApi
diff --git a/framework/local/api/utils/fetch.ts b/framework/local/api/utils/fetch.ts
new file mode 100644
index 000000000..9d9fff3ed
--- /dev/null
+++ b/framework/local/api/utils/fetch.ts
@@ -0,0 +1,3 @@
+import zeitFetch from '@vercel/fetch'
+
+export default zeitFetch()
diff --git a/framework/local/auth/index.ts b/framework/local/auth/index.ts
new file mode 100644
index 000000000..36e757a89
--- /dev/null
+++ b/framework/local/auth/index.ts
@@ -0,0 +1,3 @@
+export { default as useLogin } from './use-login'
+export { default as useLogout } from './use-logout'
+export { default as useSignup } from './use-signup'
diff --git a/framework/local/auth/use-login.tsx b/framework/local/auth/use-login.tsx
new file mode 100644
index 000000000..28351dc7f
--- /dev/null
+++ b/framework/local/auth/use-login.tsx
@@ -0,0 +1,16 @@
+import { MutationHook } from '@commerce/utils/types'
+import useLogin, { UseLogin } from '@commerce/auth/use-login'
+
+export default useLogin as UseLogin
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher() {
+ return null
+ },
+ useHook: () => () => {
+ return async function () {}
+ },
+}
diff --git a/framework/local/auth/use-logout.tsx b/framework/local/auth/use-logout.tsx
new file mode 100644
index 000000000..9b3fc3e44
--- /dev/null
+++ b/framework/local/auth/use-logout.tsx
@@ -0,0 +1,17 @@
+import { MutationHook } from '@commerce/utils/types'
+import useLogout, { UseLogout } from '@commerce/auth/use-logout'
+
+export default useLogout as UseLogout
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher() {
+ return null
+ },
+ useHook:
+ ({ fetch }) =>
+ () =>
+ async () => {},
+}
diff --git a/framework/local/auth/use-signup.tsx b/framework/local/auth/use-signup.tsx
new file mode 100644
index 000000000..e9ad13458
--- /dev/null
+++ b/framework/local/auth/use-signup.tsx
@@ -0,0 +1,19 @@
+import { useCallback } from 'react'
+import useCustomer from '../customer/use-customer'
+import { MutationHook } from '@commerce/utils/types'
+import useSignup, { UseSignup } from '@commerce/auth/use-signup'
+
+export default useSignup as UseSignup
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher() {
+ return null
+ },
+ useHook:
+ ({ fetch }) =>
+ () =>
+ () => {},
+}
diff --git a/framework/local/cart/index.ts b/framework/local/cart/index.ts
new file mode 100644
index 000000000..3b8ba990e
--- /dev/null
+++ b/framework/local/cart/index.ts
@@ -0,0 +1,4 @@
+export { default as useCart } from './use-cart'
+export { default as useAddItem } from './use-add-item'
+export { default as useRemoveItem } from './use-remove-item'
+export { default as useUpdateItem } from './use-update-item'
diff --git a/framework/local/cart/use-add-item.tsx b/framework/local/cart/use-add-item.tsx
new file mode 100644
index 000000000..7f3d1061f
--- /dev/null
+++ b/framework/local/cart/use-add-item.tsx
@@ -0,0 +1,17 @@
+import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
+import { MutationHook } from '@commerce/utils/types'
+
+export default useAddItem as UseAddItem
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook:
+ ({ fetch }) =>
+ () => {
+ return async function addItem() {
+ return {}
+ }
+ },
+}
diff --git a/framework/local/cart/use-cart.tsx b/framework/local/cart/use-cart.tsx
new file mode 100644
index 000000000..b3e509a21
--- /dev/null
+++ b/framework/local/cart/use-cart.tsx
@@ -0,0 +1,42 @@
+import { useMemo } from 'react'
+import { SWRHook } from '@commerce/utils/types'
+import useCart, { UseCart } from '@commerce/cart/use-cart'
+
+export default useCart as UseCart
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher() {
+ return {
+ id: '',
+ createdAt: '',
+ currency: { code: '' },
+ taxesIncluded: '',
+ lineItems: [],
+ lineItemsSubtotalPrice: '',
+ subtotalPrice: 0,
+ totalPrice: 0,
+ }
+ },
+ useHook:
+ ({ useData }) =>
+ (input) => {
+ return useMemo(
+ () =>
+ Object.create(
+ {},
+ {
+ isEmpty: {
+ get() {
+ return true
+ },
+ enumerable: true,
+ },
+ }
+ ),
+ []
+ )
+ },
+}
diff --git a/framework/local/cart/use-remove-item.tsx b/framework/local/cart/use-remove-item.tsx
new file mode 100644
index 000000000..b4ed583b8
--- /dev/null
+++ b/framework/local/cart/use-remove-item.tsx
@@ -0,0 +1,18 @@
+import { MutationHook } from '@commerce/utils/types'
+import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
+
+export default useRemoveItem as UseRemoveItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook:
+ ({ fetch }) =>
+ () => {
+ return async function removeItem(input) {
+ return {}
+ }
+ },
+}
diff --git a/framework/local/cart/use-update-item.tsx b/framework/local/cart/use-update-item.tsx
new file mode 100644
index 000000000..06d703f70
--- /dev/null
+++ b/framework/local/cart/use-update-item.tsx
@@ -0,0 +1,18 @@
+import { MutationHook } from '@commerce/utils/types'
+import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
+
+export default useUpdateItem as UseUpdateItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook:
+ ({ fetch }) =>
+ () => {
+ return async function addItem() {
+ return {}
+ }
+ },
+}
diff --git a/framework/local/commerce.config.json b/framework/local/commerce.config.json
new file mode 100644
index 000000000..5a0376a31
--- /dev/null
+++ b/framework/local/commerce.config.json
@@ -0,0 +1,6 @@
+{
+ "provider": "local",
+ "features": {
+ "wishlist": false
+ }
+}
diff --git a/framework/local/customer/index.ts b/framework/local/customer/index.ts
new file mode 100644
index 000000000..6c903ecc5
--- /dev/null
+++ b/framework/local/customer/index.ts
@@ -0,0 +1 @@
+export { default as useCustomer } from './use-customer'
diff --git a/framework/local/customer/use-customer.tsx b/framework/local/customer/use-customer.tsx
new file mode 100644
index 000000000..41757cd0d
--- /dev/null
+++ b/framework/local/customer/use-customer.tsx
@@ -0,0 +1,15 @@
+import { SWRHook } from '@commerce/utils/types'
+import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
+
+export default useCustomer as UseCustomer
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook: () => () => {
+ return async function addItem() {
+ return {}
+ }
+ },
+}
diff --git a/framework/local/data.json b/framework/local/data.json
new file mode 100644
index 000000000..6b25e6aa5
--- /dev/null
+++ b/framework/local/data.json
@@ -0,0 +1,241 @@
+{
+ "products": [
+ {
+ "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjA=",
+ "name": "New Short Sleeve T-Shirt",
+ "vendor": "Next.js",
+ "path": "/new-short-sleeve-t-shirt",
+ "slug": "new-short-sleeve-t-shirt",
+ "price": { "value": 25, "currencyCode": "USD" },
+ "descriptionHtml": "Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.
",
+ "images": [
+ {
+ "url": "/assets/drop-shirt-0.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-1.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-2.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-3.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ }
+ ],
+ "variants": [
+ {
+ "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjAss=",
+ "options": [
+ {
+ "__typename": "MultipleChoiceOption",
+ "id": "asd",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "XL"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "options": [
+ {
+ "id": "option-color",
+ "displayName": "Color",
+ "values": [
+ {
+ "label": "color",
+ "hexColors": ["#222"]
+ }
+ ]
+ },
+ {
+ "id": "option-size",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "S"
+ },
+ {
+ "label": "M"
+ },
+ {
+ "label": "L"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjA=",
+ "name": "New Short Sleeve T-Shirt",
+ "vendor": "Next.js",
+ "path": "/new-short-sleeve-t-shirt",
+ "slug": "new-short-sleeve-t-shirt",
+ "price": { "value": 25, "currencyCode": "USD" },
+ "descriptionHtml": "Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.
",
+ "images": [
+ {
+ "url": "/assets/drop-shirt-0.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-1.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-2.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-3.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ }
+ ],
+ "variants": [
+ {
+ "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjAss=",
+ "options": [
+ {
+ "__typename": "MultipleChoiceOption",
+ "id": "asd",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "XL"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "options": [
+ {
+ "id": "option-color",
+ "displayName": "Color",
+ "values": [
+ {
+ "label": "color",
+ "hexColors": ["#222"]
+ }
+ ]
+ },
+ {
+ "id": "option-size",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "S"
+ },
+ {
+ "label": "M"
+ },
+ {
+ "label": "L"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjA=",
+ "name": "New Short Sleeve T-Shirt",
+ "vendor": "Next.js",
+ "path": "/new-short-sleeve-t-shirt",
+ "slug": "new-short-sleeve-t-shirt",
+ "price": { "value": 25, "currencyCode": "USD" },
+ "descriptionHtml": "Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.
",
+ "images": [
+ {
+ "url": "/assets/drop-shirt-0.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-1.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-2.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-3.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ }
+ ],
+ "variants": [
+ {
+ "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjAss=",
+ "options": [
+ {
+ "__typename": "MultipleChoiceOption",
+ "id": "asd",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "XL"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "options": [
+ {
+ "id": "option-color",
+ "displayName": "Color",
+ "values": [
+ {
+ "label": "color",
+ "hexColors": ["#222"]
+ }
+ ]
+ },
+ {
+ "id": "option-size",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "S"
+ },
+ {
+ "label": "M"
+ },
+ {
+ "label": "L"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/framework/local/fetcher.ts b/framework/local/fetcher.ts
new file mode 100644
index 000000000..69943d1df
--- /dev/null
+++ b/framework/local/fetcher.ts
@@ -0,0 +1,11 @@
+import { Fetcher } from '@commerce/utils/types'
+
+export const fetcher: Fetcher = async () => {
+ console.log('FETCHER')
+ const res = await fetch('./data.json')
+ if (res.ok) {
+ const { data } = await res.json()
+ return data
+ }
+ throw res
+}
diff --git a/framework/local/index.tsx b/framework/local/index.tsx
new file mode 100644
index 000000000..2ec304f63
--- /dev/null
+++ b/framework/local/index.tsx
@@ -0,0 +1,32 @@
+import * as React from 'react'
+import { ReactNode } from 'react'
+import { localProvider } from './provider'
+import {
+ CommerceConfig,
+ CommerceProvider as CoreCommerceProvider,
+ useCommerce as useCoreCommerce,
+} from '@commerce'
+
+export const localConfig: CommerceConfig = {
+ locale: 'en-us',
+ cartCookie: 'session',
+}
+
+export function CommerceProvider({
+ children,
+ ...config
+}: {
+ children?: ReactNode
+ locale: string
+} & Partial) {
+ return (
+
+ {children}
+
+ )
+}
+
+export const useCommerce = () => useCoreCommerce()
diff --git a/framework/local/next.config.js b/framework/local/next.config.js
new file mode 100644
index 000000000..ce46b706f
--- /dev/null
+++ b/framework/local/next.config.js
@@ -0,0 +1,8 @@
+const commerce = require('./commerce.config.json')
+
+module.exports = {
+ commerce,
+ images: {
+ domains: ['localhost'],
+ },
+}
diff --git a/framework/local/product/index.ts b/framework/local/product/index.ts
new file mode 100644
index 000000000..426a3edcd
--- /dev/null
+++ b/framework/local/product/index.ts
@@ -0,0 +1,2 @@
+export { default as usePrice } from './use-price'
+export { default as useSearch } from './use-search'
diff --git a/framework/local/product/use-price.tsx b/framework/local/product/use-price.tsx
new file mode 100644
index 000000000..0174faf5e
--- /dev/null
+++ b/framework/local/product/use-price.tsx
@@ -0,0 +1,2 @@
+export * from '@commerce/product/use-price'
+export { default } from '@commerce/product/use-price'
diff --git a/framework/local/product/use-search.tsx b/framework/local/product/use-search.tsx
new file mode 100644
index 000000000..30e699537
--- /dev/null
+++ b/framework/local/product/use-search.tsx
@@ -0,0 +1,17 @@
+import { SWRHook } from '@commerce/utils/types'
+import useSearch, { UseSearch } from '@commerce/product/use-search'
+export default useSearch as UseSearch
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook: () => () => {
+ return {
+ data: {
+ products: [],
+ },
+ }
+ },
+}
diff --git a/framework/local/provider.ts b/framework/local/provider.ts
new file mode 100644
index 000000000..e6a2b0a21
--- /dev/null
+++ b/framework/local/provider.ts
@@ -0,0 +1,21 @@
+import { fetcher } from './fetcher'
+import { handler as useCart } from './cart/use-cart'
+import { handler as useAddItem } from './cart/use-add-item'
+import { handler as useUpdateItem } from './cart/use-update-item'
+import { handler as useRemoveItem } from './cart/use-remove-item'
+import { handler as useCustomer } from './customer/use-customer'
+import { handler as useSearch } from './product/use-search'
+import { handler as useLogin } from './auth/use-login'
+import { handler as useLogout } from './auth/use-logout'
+import { handler as useSignup } from './auth/use-signup'
+
+export type Provider = typeof localProvider
+export const localProvider = {
+ locale: 'en-us',
+ cartCookie: 'session',
+ fetcher: fetcher,
+ cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
+ customer: { useCustomer },
+ products: { useSearch },
+ auth: { useLogin, useLogout, useSignup },
+}
diff --git a/framework/local/wishlist/use-add-item.tsx b/framework/local/wishlist/use-add-item.tsx
new file mode 100644
index 000000000..75f067c3a
--- /dev/null
+++ b/framework/local/wishlist/use-add-item.tsx
@@ -0,0 +1,13 @@
+import { useCallback } from 'react'
+
+export function emptyHook() {
+ const useEmptyHook = async (options = {}) => {
+ return useCallback(async function () {
+ return Promise.resolve()
+ }, [])
+ }
+
+ return useEmptyHook
+}
+
+export default emptyHook
diff --git a/framework/local/wishlist/use-remove-item.tsx b/framework/local/wishlist/use-remove-item.tsx
new file mode 100644
index 000000000..a2d3a8a05
--- /dev/null
+++ b/framework/local/wishlist/use-remove-item.tsx
@@ -0,0 +1,17 @@
+import { useCallback } from 'react'
+
+type Options = {
+ includeProducts?: boolean
+}
+
+export function emptyHook(options?: Options) {
+ const useEmptyHook = async ({ id }: { id: string | number }) => {
+ return useCallback(async function () {
+ return Promise.resolve()
+ }, [])
+ }
+
+ return useEmptyHook
+}
+
+export default emptyHook
diff --git a/framework/local/wishlist/use-wishlist.tsx b/framework/local/wishlist/use-wishlist.tsx
new file mode 100644
index 000000000..9fe0e758f
--- /dev/null
+++ b/framework/local/wishlist/use-wishlist.tsx
@@ -0,0 +1,43 @@
+import { HookFetcher } from '@commerce/utils/types'
+import type { Product } from '@commerce/types/product'
+
+const defaultOpts = {}
+
+export type Wishlist = {
+ items: [
+ {
+ product_id: number
+ variant_id: number
+ id: number
+ product: Product
+ }
+ ]
+}
+
+export interface UseWishlistOptions {
+ includeProducts?: boolean
+}
+
+export interface UseWishlistInput extends UseWishlistOptions {
+ customerId?: number
+}
+
+export const fetcher: HookFetcher = () => {
+ return null
+}
+
+export function extendHook(
+ customFetcher: typeof fetcher,
+ // swrOptions?: SwrOptions
+ swrOptions?: any
+) {
+ const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
+ return { data: null }
+ }
+
+ useWishlist.extend = extendHook
+
+ return useWishlist
+}
+
+export default extendHook(fetcher)
diff --git a/framework/saleor/.env.template b/framework/saleor/.env.template
new file mode 100644
index 000000000..715fec861
--- /dev/null
+++ b/framework/saleor/.env.template
@@ -0,0 +1,4 @@
+COMMERCE_PROVIDER=saleor
+
+NEXT_SALEOR_API_URL=
+NEXT_SALEOR_CHANNEL=
diff --git a/framework/saleor/README.md b/framework/saleor/README.md
new file mode 100644
index 000000000..1684ff6bc
--- /dev/null
+++ b/framework/saleor/README.md
@@ -0,0 +1,19 @@
+## Saleor Provider
+
+**Demo:** TBD
+
+Before getting starter, a [Saleor](https://saleor.io/) account and store is required before using the provider.
+
+Next, copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
+
+```bash
+cp framework/saleor/.env.template .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).
diff --git a/framework/swell/api/customers/login.ts b/framework/saleor/api/cart.ts
similarity index 100%
rename from framework/swell/api/customers/login.ts
rename to framework/saleor/api/cart.ts
diff --git a/framework/saleor/api/catalog/products.ts b/framework/saleor/api/catalog/products.ts
new file mode 100644
index 000000000..ea9b101e1
--- /dev/null
+++ b/framework/saleor/api/catalog/products.ts
@@ -0,0 +1 @@
+export default function () {}
diff --git a/framework/saleor/api/checkout.ts b/framework/saleor/api/checkout.ts
new file mode 100644
index 000000000..ea9b101e1
--- /dev/null
+++ b/framework/saleor/api/checkout.ts
@@ -0,0 +1 @@
+export default function () {}
diff --git a/framework/saleor/api/customers/index.ts b/framework/saleor/api/customers/index.ts
new file mode 100644
index 000000000..ea9b101e1
--- /dev/null
+++ b/framework/saleor/api/customers/index.ts
@@ -0,0 +1 @@
+export default function () {}
diff --git a/framework/saleor/api/customers/login.ts b/framework/saleor/api/customers/login.ts
new file mode 100644
index 000000000..ea9b101e1
--- /dev/null
+++ b/framework/saleor/api/customers/login.ts
@@ -0,0 +1 @@
+export default function () {}
diff --git a/framework/saleor/api/customers/logout.ts b/framework/saleor/api/customers/logout.ts
new file mode 100644
index 000000000..ea9b101e1
--- /dev/null
+++ b/framework/saleor/api/customers/logout.ts
@@ -0,0 +1 @@
+export default function () {}
diff --git a/framework/saleor/api/customers/signup.ts b/framework/saleor/api/customers/signup.ts
new file mode 100644
index 000000000..ea9b101e1
--- /dev/null
+++ b/framework/saleor/api/customers/signup.ts
@@ -0,0 +1 @@
+export default function () {}
diff --git a/framework/saleor/api/endpoints/cart.ts b/framework/saleor/api/endpoints/cart.ts
new file mode 100644
index 000000000..d09c976c3
--- /dev/null
+++ b/framework/saleor/api/endpoints/cart.ts
@@ -0,0 +1 @@
+export default function (_commerce: any) {}
diff --git a/framework/saleor/api/endpoints/catalog/products.ts b/framework/saleor/api/endpoints/catalog/products.ts
new file mode 100644
index 000000000..d09c976c3
--- /dev/null
+++ b/framework/saleor/api/endpoints/catalog/products.ts
@@ -0,0 +1 @@
+export default function (_commerce: any) {}
diff --git a/framework/saleor/api/endpoints/checkout/index.ts b/framework/saleor/api/endpoints/checkout/index.ts
new file mode 100644
index 000000000..f15672435
--- /dev/null
+++ b/framework/saleor/api/endpoints/checkout/index.ts
@@ -0,0 +1,57 @@
+import { CommerceAPI, GetAPISchema, createEndpoint } from '@commerce/api'
+import checkoutEndpoint from '@commerce/api/endpoints/checkout'
+import { CheckoutSchema } from '@commerce/types/checkout'
+
+export type CheckoutAPI = GetAPISchema
+
+export type CheckoutEndpoint = CheckoutAPI['endpoint']
+
+const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
+ req,
+ res,
+ config,
+}) => {
+ try {
+ const html = `
+
+
+
+
+
+ Checkout
+
+
+
+
+
Checkout not yet implemented :(
+
+ See #64
+
+
+
+
+ `
+
+ res.status(200)
+ res.setHeader('Content-Type', 'text/html')
+ res.write(html)
+ res.end()
+ } catch (error) {
+ console.error(error)
+
+ const message = 'An unexpected error ocurred'
+
+ res.status(500).json({ data: null, errors: [{ message }] })
+ }
+}
+
+export const handlers: CheckoutEndpoint['handlers'] = { checkout }
+
+const checkoutApi = createEndpoint({
+ handler: checkoutEndpoint,
+ handlers,
+})
+
+export default checkoutApi
diff --git a/framework/saleor/api/endpoints/customer.ts b/framework/saleor/api/endpoints/customer.ts
new file mode 100644
index 000000000..d09c976c3
--- /dev/null
+++ b/framework/saleor/api/endpoints/customer.ts
@@ -0,0 +1 @@
+export default function (_commerce: any) {}
diff --git a/framework/saleor/api/endpoints/login.ts b/framework/saleor/api/endpoints/login.ts
new file mode 100644
index 000000000..d09c976c3
--- /dev/null
+++ b/framework/saleor/api/endpoints/login.ts
@@ -0,0 +1 @@
+export default function (_commerce: any) {}
diff --git a/framework/saleor/api/endpoints/logout.ts b/framework/saleor/api/endpoints/logout.ts
new file mode 100644
index 000000000..d09c976c3
--- /dev/null
+++ b/framework/saleor/api/endpoints/logout.ts
@@ -0,0 +1 @@
+export default function (_commerce: any) {}
diff --git a/framework/saleor/api/endpoints/signup.ts b/framework/saleor/api/endpoints/signup.ts
new file mode 100644
index 000000000..d09c976c3
--- /dev/null
+++ b/framework/saleor/api/endpoints/signup.ts
@@ -0,0 +1 @@
+export default function (_commerce: any) {}
diff --git a/framework/saleor/api/endpoints/wishlist.ts b/framework/saleor/api/endpoints/wishlist.ts
new file mode 100644
index 000000000..d09c976c3
--- /dev/null
+++ b/framework/saleor/api/endpoints/wishlist.ts
@@ -0,0 +1 @@
+export default function (_commerce: any) {}
diff --git a/framework/saleor/api/index.ts b/framework/saleor/api/index.ts
new file mode 100644
index 000000000..5ae5f3a8a
--- /dev/null
+++ b/framework/saleor/api/index.ts
@@ -0,0 +1,49 @@
+import type { CommerceAPIConfig } from '@commerce/api'
+
+import * as Const from '../const'
+
+if (!Const.API_URL) {
+ throw new Error(`The environment variable NEXT_SALEOR_API_URL is missing and it's required to access your store`)
+}
+
+if (!Const.API_CHANNEL) {
+ throw new Error(`The environment variable NEXT_SALEOR_CHANNEL is missing and it's required to access your store`)
+}
+
+import fetchGraphqlApi from './utils/fetch-graphql-api'
+
+export interface SaleorConfig extends CommerceAPIConfig {
+ storeChannel: string
+}
+
+const config: SaleorConfig = {
+ locale: 'en-US',
+ commerceUrl: Const.API_URL,
+ apiToken: Const.SALEOR_TOKEN,
+ cartCookie: Const.CHECKOUT_ID_COOKIE,
+ cartCookieMaxAge: 60 * 60 * 24 * 30,
+ fetch: fetchGraphqlApi,
+ customerCookie: '',
+ storeChannel: Const.API_CHANNEL,
+}
+
+import {
+ CommerceAPI,
+ getCommerceApi as commerceApi,
+} from '@commerce/api'
+
+import * as operations from './operations'
+
+export interface ShopifyConfig extends CommerceAPIConfig {}
+
+export const provider = { config, operations }
+
+export type Provider = typeof provider
+
+export type SaleorAPI = CommerceAPI
+
+export function getCommerceApi
(
+ customProvider: P = provider as any
+): SaleorAPI
{
+ return commerceApi(customProvider)
+}
diff --git a/framework/saleor/api/operations/get-all-pages.ts b/framework/saleor/api/operations/get-all-pages.ts
new file mode 100644
index 000000000..f3ed54e27
--- /dev/null
+++ b/framework/saleor/api/operations/get-all-pages.ts
@@ -0,0 +1,50 @@
+import type { OperationContext } from '@commerce/api/operations'
+
+import { QueryPagesArgs, PageCountableEdge } from '../../schema'
+import type { SaleorConfig, Provider } from '..'
+import * as Query from '../../utils/queries'
+
+export type Page = any
+
+ export type GetAllPagesResult<
+ T extends { pages: any[] } = { pages: Page[] }
+ > = T
+
+export default function getAllPagesOperation({
+ commerce,
+}: OperationContext) {
+
+ async function getAllPages({
+ query = Query.PageMany,
+ config,
+ variables,
+ }: {
+ url?: string
+ config?: Partial
+ variables?: QueryPagesArgs
+ preview?: boolean
+ query?: string
+ } = {}): Promise {
+ const { fetch, locale, locales = ['en-US'] } = commerce.getConfig(config)
+
+ const { data } = await fetch(query, { variables },
+ {
+ ...(locale && {
+ headers: {
+ 'Accept-Language': locale,
+ },
+ }),
+ }
+ )
+
+ const pages = data.pages?.edges?.map(({ node: { title: name, slug, ...node } }: PageCountableEdge) => ({
+ ...node,
+ url: `/${locale}/${slug}`,
+ name,
+ }))
+
+ return { pages }
+ }
+
+ return getAllPages
+}
diff --git a/framework/saleor/api/operations/get-all-product-paths.ts b/framework/saleor/api/operations/get-all-product-paths.ts
new file mode 100644
index 000000000..43ce7de94
--- /dev/null
+++ b/framework/saleor/api/operations/get-all-product-paths.ts
@@ -0,0 +1,46 @@
+import type { OperationContext } from '@commerce/api/operations'
+import {
+ GetAllProductPathsQuery,
+ GetAllProductPathsQueryVariables,
+ ProductCountableEdge,
+} from '../../schema'
+import type { ShopifyConfig, Provider, SaleorConfig } from '..'
+
+import { getAllProductsPathsQuery } from '../../utils/queries'
+import fetchAllProducts from '../utils/fetch-all-products'
+
+export type GetAllProductPathsResult = {
+ products: Array<{ path: string }>
+}
+
+export default function getAllProductPathsOperation({
+ commerce,
+}: OperationContext) {
+
+ async function getAllProductPaths({
+ query,
+ config,
+ variables,
+ }: {
+ query?: string
+ config?: SaleorConfig
+ variables?: any
+ } = {}): Promise {
+ config = commerce.getConfig(config)
+
+ const products = await fetchAllProducts({
+ config,
+ query: getAllProductsPathsQuery,
+ variables,
+ })
+
+ return {
+ products: products?.map(({ node: { slug } }: ProductCountableEdge) => ({
+ path: `/${slug}`,
+ })),
+ }
+
+ }
+
+ return getAllProductPaths
+}
diff --git a/framework/saleor/api/operations/get-all-products.ts b/framework/saleor/api/operations/get-all-products.ts
new file mode 100644
index 000000000..a1a7ce0c9
--- /dev/null
+++ b/framework/saleor/api/operations/get-all-products.ts
@@ -0,0 +1,67 @@
+import type { OperationContext } from '@commerce/api/operations'
+import { Product } from '@commerce/types/product'
+
+import { ProductCountableEdge } from '../../schema'
+import type { Provider, SaleorConfig } from '..'
+import { normalizeProduct } from '../../utils'
+
+import * as Query from '../../utils/queries'
+import { GraphQLFetcherResult } from '@commerce/api'
+
+type ReturnType = {
+ products: Product[]
+}
+
+export default function getAllProductsOperation({
+ commerce,
+}: OperationContext) {
+ async function getAllProducts({
+ query = Query.ProductMany,
+ variables,
+ config,
+ featured,
+ }: {
+ query?: string
+ variables?: any
+ config?: Partial
+ preview?: boolean
+ featured?: boolean
+ } = {}): Promise {
+ const { fetch, locale } = commerce.getConfig(config)
+
+ if (featured) {
+ variables = { ...variables, categoryId: 'Q29sbGVjdGlvbjo0' };
+ query = Query.CollectionOne
+ }
+
+
+ const { data }: GraphQLFetcherResult = await fetch(
+ query,
+ { variables },
+ {
+ ...(locale && {
+ headers: {
+ 'Accept-Language': locale,
+ },
+ }),
+ }
+ )
+
+ if (featured) {
+ const products = data.collection.products?.edges?.map(({ node: p }: ProductCountableEdge) => normalizeProduct(p)) ?? []
+
+ return {
+ products,
+ }
+ } else {
+ const products = data.products?.edges?.map(({ node: p }: ProductCountableEdge) => normalizeProduct(p)) ?? []
+
+ return {
+ products,
+ }
+ }
+
+ }
+
+ return getAllProducts
+}
diff --git a/framework/saleor/api/operations/get-page.ts b/framework/saleor/api/operations/get-page.ts
new file mode 100644
index 000000000..af2d5b8e6
--- /dev/null
+++ b/framework/saleor/api/operations/get-page.ts
@@ -0,0 +1,51 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type { Provider, SaleorConfig } from '..'
+import { QueryPageArgs } from '../../schema'
+
+import * as Query from '../../utils/queries'
+
+export type Page = any
+
+ export type GetPageResult = T
+
+export default function getPageOperation({
+ commerce,
+}: OperationContext) {
+
+ async function getPage({
+ query = Query.PageOne,
+ variables,
+ config,
+ }: {
+ query?: string
+ variables: QueryPageArgs,
+ config?: Partial
+ preview?: boolean
+ }): Promise {
+ const { fetch, locale = 'en-US' } = commerce.getConfig(config)
+
+ const {
+ data: { page },
+ } = await fetch(query, { variables },
+ {
+ ...(locale && {
+ headers: {
+ 'Accept-Language': locale,
+ },
+ }),
+ }
+ )
+
+ return {
+ page: page
+ ? {
+ ...page,
+ name: page.title,
+ url: `/${locale}/${page.slug}`,
+ }
+ : null,
+ }
+ }
+
+ return getPage
+}
diff --git a/framework/saleor/api/operations/get-product.ts b/framework/saleor/api/operations/get-product.ts
new file mode 100644
index 000000000..85fca934a
--- /dev/null
+++ b/framework/saleor/api/operations/get-product.ts
@@ -0,0 +1,46 @@
+import type { OperationContext } from '@commerce/api/operations'
+import { normalizeProduct, } from '../../utils'
+import type { Provider, SaleorConfig } from '..'
+
+import * as Query from '../../utils/queries'
+
+type Variables = {
+ slug: string
+}
+
+type ReturnType = {
+ product: any
+}
+
+export default function getProductOperation({
+ commerce,
+}: OperationContext) {
+ async function getProduct({
+ query = Query.ProductOneBySlug,
+ variables,
+ config: cfg,
+ }: {
+ query?: string
+ variables: Variables
+ config?: Partial
+ preview?: boolean
+ }): Promise {
+ const { fetch, locale } = commerce.getConfig(cfg)
+
+ const { data } = await fetch(query, { variables },
+ {
+ ...(locale && {
+ headers: {
+ 'Accept-Language': locale,
+ },
+ }),
+ }
+ )
+
+ return {
+ product: data?.product ? normalizeProduct(data.product) : null,
+ }
+ }
+
+ return getProduct
+}
diff --git a/framework/saleor/api/operations/get-site-info.ts b/framework/saleor/api/operations/get-site-info.ts
new file mode 100644
index 000000000..eca0f2246
--- /dev/null
+++ b/framework/saleor/api/operations/get-site-info.ts
@@ -0,0 +1,35 @@
+import type { OperationContext } from '@commerce/api/operations'
+import { Category } from '@commerce/types/site'
+import type { SaleorConfig, Provider } from '..'
+
+import { getCategories, getVendors } from '../../utils'
+
+interface GetSiteInfoResult {
+ categories: Category[]
+ brands: any[]
+}
+
+export default function getSiteInfoOperation({ commerce }: OperationContext) {
+ async function getSiteInfo({
+ query,
+ config,
+ variables,
+ }: {
+ query?: string
+ config?: Partial
+ preview?: boolean
+ variables?: any
+ } = {}): Promise {
+ const cfg = commerce.getConfig(config)
+
+ const categories = await getCategories(cfg)
+ const brands = await getVendors(cfg)
+
+ return {
+ categories,
+ brands,
+ }
+ }
+
+ return getSiteInfo
+}
diff --git a/framework/saleor/api/operations/index.ts b/framework/saleor/api/operations/index.ts
new file mode 100644
index 000000000..7872a20b6
--- /dev/null
+++ b/framework/saleor/api/operations/index.ts
@@ -0,0 +1,7 @@
+export { default as getAllPages } from './get-all-pages'
+export { default as getPage } from './get-page'
+export { default as getAllProducts } from './get-all-products'
+export { default as getAllProductPaths } from './get-all-product-paths'
+export { default as getProduct } from './get-product'
+export { default as getSiteInfo } from './get-site-info'
+export { default as login } from './login'
diff --git a/framework/saleor/api/operations/login.ts b/framework/saleor/api/operations/login.ts
new file mode 100644
index 000000000..ca680b82c
--- /dev/null
+++ b/framework/saleor/api/operations/login.ts
@@ -0,0 +1,42 @@
+import type { ServerResponse } from 'http'
+import type { OperationContext } from '@commerce/api/operations'
+import type { Provider, SaleorConfig } from '..'
+import {
+ throwUserErrors,
+} from '../../utils'
+
+import * as Mutation from '../../utils/mutations'
+
+export default function loginOperation({
+ commerce,
+}: OperationContext) {
+ async function login({
+ query = Mutation.SessionCreate,
+ variables,
+ config,
+ }: {
+ query?: string
+ variables: any
+ res: ServerResponse
+ config?: SaleorConfig
+ }): Promise {
+ config = commerce.getConfig(config)
+
+ const { data: { customerAccessTokenCreate } } = await config.fetch(query, { variables })
+
+ throwUserErrors(customerAccessTokenCreate?.customerUserErrors)
+
+ const customerAccessToken = customerAccessTokenCreate?.customerAccessToken
+ const accessToken = customerAccessToken?.accessToken
+
+ // if (accessToken) {
+ // setCustomerToken(accessToken)
+ // }
+
+ return {
+ result: customerAccessToken?.accessToken,
+ }
+ }
+
+ return login
+}
diff --git a/framework/saleor/api/utils/fetch-all-products.ts b/framework/saleor/api/utils/fetch-all-products.ts
new file mode 100644
index 000000000..1cfb3157c
--- /dev/null
+++ b/framework/saleor/api/utils/fetch-all-products.ts
@@ -0,0 +1,41 @@
+import { ProductCountableEdge } from '../../schema'
+import { SaleorConfig } from '..'
+
+const fetchAllProducts = async ({
+ config,
+ query,
+ variables,
+ acc = [],
+ cursor,
+}: {
+ config: SaleorConfig
+ query: string
+ acc?: ProductCountableEdge[]
+ variables?: any
+ cursor?: string
+}): Promise => {
+ const { data } = await config.fetch(query, {
+ variables: { ...variables, cursor },
+ })
+
+ const edges: ProductCountableEdge[] = data.products?.edges ?? []
+ const hasNextPage = data.products?.pageInfo?.hasNextPage
+ acc = acc.concat(edges)
+
+ if (hasNextPage) {
+ const cursor = edges.pop()?.cursor
+ if (cursor) {
+ return fetchAllProducts({
+ config,
+ query,
+ variables,
+ acc,
+ cursor,
+ })
+ }
+ }
+
+ return acc
+}
+
+export default fetchAllProducts
diff --git a/framework/saleor/api/utils/fetch-graphql-api.ts b/framework/saleor/api/utils/fetch-graphql-api.ts
new file mode 100644
index 000000000..3145409ad
--- /dev/null
+++ b/framework/saleor/api/utils/fetch-graphql-api.ts
@@ -0,0 +1,37 @@
+import type { GraphQLFetcher } from '@commerce/api'
+import fetch from './fetch'
+
+import { API_URL } from '../../const'
+import { getError } from '../../utils/handle-fetch-response'
+import { getCommerceApi } from '..'
+import { getToken } from '../../utils/index'
+
+const fetchGraphqlApi: GraphQLFetcher = async (query: string, { variables } = {}, fetchOptions) => {
+ const config = getCommerceApi().getConfig()
+ const token = getToken()
+
+ const res = await fetch(API_URL!, {
+ ...fetchOptions,
+ method: 'POST',
+ headers: {
+ ...(token && {
+ Authorization: `Bearer ${token}`,
+ }),
+ ...fetchOptions?.headers,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query,
+ variables,
+ }),
+ })
+
+ const { data, errors, status } = await res.json()
+
+ if (errors) {
+ throw getError(errors, status)
+ }
+
+ return { data, res }
+}
+export default fetchGraphqlApi
diff --git a/framework/saleor/api/utils/fetch.ts b/framework/saleor/api/utils/fetch.ts
new file mode 100644
index 000000000..0b8367102
--- /dev/null
+++ b/framework/saleor/api/utils/fetch.ts
@@ -0,0 +1,2 @@
+import zeitFetch from '@vercel/fetch'
+export default zeitFetch()
diff --git a/framework/saleor/api/utils/is-allowed-method.ts b/framework/saleor/api/utils/is-allowed-method.ts
new file mode 100644
index 000000000..cbaab2251
--- /dev/null
+++ b/framework/saleor/api/utils/is-allowed-method.ts
@@ -0,0 +1,22 @@
+import type { NextApiRequest, NextApiResponse } from 'next'
+
+export default function isAllowedMethod(req: NextApiRequest, res: NextApiResponse, allowedMethods: string[]) {
+ const methods = allowedMethods.includes('OPTIONS') ? allowedMethods : [...allowedMethods, 'OPTIONS']
+
+ if (!req.method || !methods.includes(req.method)) {
+ res.status(405)
+ res.setHeader('Allow', methods.join(', '))
+ res.end()
+ return false
+ }
+
+ if (req.method === 'OPTIONS') {
+ res.status(200)
+ res.setHeader('Allow', methods.join(', '))
+ res.setHeader('Content-Length', '0')
+ res.end()
+ return false
+ }
+
+ return true
+}
diff --git a/framework/saleor/api/wishlist.ts b/framework/saleor/api/wishlist.ts
new file mode 100644
index 000000000..ea9b101e1
--- /dev/null
+++ b/framework/saleor/api/wishlist.ts
@@ -0,0 +1 @@
+export default function () {}
diff --git a/framework/saleor/auth/use-login.tsx b/framework/saleor/auth/use-login.tsx
new file mode 100644
index 000000000..2a31c932b
--- /dev/null
+++ b/framework/saleor/auth/use-login.tsx
@@ -0,0 +1,63 @@
+import { useCallback } from 'react'
+
+import type { MutationHook } from '@commerce/utils/types'
+import { CommerceError } from '@commerce/utils/errors'
+import useCustomer from '../customer/use-customer'
+import * as mutation from '../utils/mutations'
+import { Mutation, MutationTokenCreateArgs } from '../schema'
+import useLogin, { UseLogin } from '@commerce/auth/use-login'
+import { setCSRFToken, setToken, throwUserErrors, checkoutAttach, getCheckoutId } from '../utils'
+import { LoginHook } from '@commerce/types/login'
+
+export default useLogin as UseLogin
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: mutation.SessionCreate,
+ },
+ async fetcher({ input: { email, password }, options, fetch }) {
+ if (!(email && password)) {
+ throw new CommerceError({
+ message: 'A first name, last name, email and password are required to login',
+ })
+ }
+
+ const { tokenCreate } = await fetch({
+ ...options,
+ variables: { email, password },
+ })
+
+ throwUserErrors(tokenCreate?.errors)
+
+ const { token, csrfToken } = tokenCreate!
+
+ if (token && csrfToken) {
+ setToken(token)
+ setCSRFToken(csrfToken)
+
+ const { checkoutId } = getCheckoutId()
+ checkoutAttach(fetch, {
+ variables: { checkoutId },
+ headers: {
+ Authorization: `JWT ${token}`,
+ },
+ })
+ }
+
+ return null
+ },
+ useHook:
+ ({ fetch }) =>
+ () => {
+ const { revalidate } = useCustomer()
+
+ return useCallback(
+ async function login(input) {
+ const data = await fetch({ input })
+ await revalidate()
+ return data
+ },
+ [fetch, revalidate]
+ )
+ },
+}
diff --git a/framework/saleor/auth/use-logout.tsx b/framework/saleor/auth/use-logout.tsx
new file mode 100644
index 000000000..fe75df84b
--- /dev/null
+++ b/framework/saleor/auth/use-logout.tsx
@@ -0,0 +1,41 @@
+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 * as mutation from '../utils/mutations'
+import { setCSRFToken, setToken, setCheckoutToken } from '../utils/customer-token'
+import { LogoutHook } from '@commerce/types/logout'
+
+export default useLogout as UseLogout
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: mutation.SessionDestroy,
+ },
+ async fetcher({ options, fetch }) {
+ await fetch({
+ ...options,
+ variables: {},
+ })
+
+ setToken()
+ setCSRFToken()
+ setCheckoutToken()
+
+ return null
+ },
+ useHook:
+ ({ fetch }) =>
+ () => {
+ const { mutate } = useCustomer()
+
+ return useCallback(
+ async function logout() {
+ const data = await fetch()
+ await mutate(null, false)
+ return data
+ },
+ [fetch, mutate]
+ )
+ },
+}
diff --git a/framework/saleor/auth/use-signup.tsx b/framework/saleor/auth/use-signup.tsx
new file mode 100644
index 000000000..d9e91b468
--- /dev/null
+++ b/framework/saleor/auth/use-signup.tsx
@@ -0,0 +1,56 @@
+import { useCallback } from 'react'
+import type { MutationHook } from '@commerce/utils/types'
+import { CommerceError } from '@commerce/utils/errors'
+import useSignup, { UseSignup } from '@commerce/auth/use-signup'
+import useCustomer from '../customer/use-customer'
+import { AccountRegisterInput, Mutation, MutationAccountRegisterArgs } from '../schema'
+
+import * as mutation from '../utils/mutations'
+import { handleAutomaticLogin, throwUserErrors } from '../utils'
+import { SignupHook } from '@commerce/types/signup'
+
+export default useSignup as UseSignup
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: mutation.AccountCreate,
+ },
+ 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',
+ })
+ }
+
+ const { customerCreate } = await fetch({
+ ...options,
+ variables: {
+ input: {
+ email,
+ password,
+ redirectUrl: 'https://localhost.com',
+ channel: 'default-channel'
+ },
+ },
+ })
+
+ throwUserErrors(customerCreate?.errors)
+ await handleAutomaticLogin(fetch, { email, password })
+
+ return null
+ },
+ useHook:
+ ({ fetch }) =>
+ () => {
+ const { revalidate } = useCustomer()
+
+ return useCallback(
+ async function signup(input) {
+ const data = await fetch({ input })
+ await revalidate()
+ return data
+ },
+ [fetch, revalidate]
+ )
+ },
+}
diff --git a/framework/saleor/cart/index.ts b/framework/saleor/cart/index.ts
new file mode 100644
index 000000000..f6d36b443
--- /dev/null
+++ b/framework/saleor/cart/index.ts
@@ -0,0 +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'
diff --git a/framework/saleor/cart/use-add-item.tsx b/framework/saleor/cart/use-add-item.tsx
new file mode 100644
index 000000000..3af368e70
--- /dev/null
+++ b/framework/saleor/cart/use-add-item.tsx
@@ -0,0 +1,54 @@
+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 * as mutation from '../utils/mutations'
+
+import { getCheckoutId, checkoutToCart } from '../utils'
+
+import { Mutation, MutationCheckoutLinesAddArgs } from '../schema'
+import { AddItemHook } from '@commerce/types/cart'
+
+export default useAddItem as UseAddItem
+
+export const handler: MutationHook = {
+ fetchOptions: { query: mutation.CheckoutLineAdd },
+ 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 { checkoutLinesAdd } = await fetch({
+ ...options,
+ variables: {
+ checkoutId: getCheckoutId().checkoutId,
+ lineItems: [
+ {
+ variantId: item.variantId,
+ quantity: item.quantity ?? 1,
+ },
+ ],
+ },
+ })
+
+ return checkoutToCart(checkoutLinesAdd)
+ },
+ useHook:
+ ({ fetch }) =>
+ () => {
+ const { mutate } = useCart()
+
+ return useCallback(
+ async function addItem(input) {
+ const data = await fetch({ input })
+ await mutate(data, false)
+ return data
+ },
+ [fetch, mutate]
+ )
+ },
+}
diff --git a/framework/saleor/cart/use-cart.tsx b/framework/saleor/cart/use-cart.tsx
new file mode 100644
index 000000000..ab80ea395
--- /dev/null
+++ b/framework/saleor/cart/use-cart.tsx
@@ -0,0 +1,53 @@
+import { useMemo } from 'react'
+import useCommerceCart, { UseCart } from '@commerce/cart/use-cart'
+
+import { SWRHook } from '@commerce/utils/types'
+import { checkoutCreate, checkoutToCart, getCheckoutId } from '../utils'
+import * as query from '../utils/queries'
+import { GetCartHook } from '@commerce/types/cart'
+
+export default useCommerceCart as UseCart
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: query.CheckoutOne,
+ },
+ async fetcher({ input: { cartId: checkoutId }, options, fetch }) {
+ let checkout
+
+ if (checkoutId) {
+ const checkoutId = getCheckoutId().checkoutToken
+ const data = await fetch({
+ ...options,
+ variables: { checkoutId },
+ })
+
+ checkout = data
+ }
+
+ if (checkout?.completedAt || !checkoutId) {
+ checkout = await checkoutCreate(fetch)
+ }
+
+ return checkoutToCart(checkout)
+ },
+ useHook:
+ ({ useData }) =>
+ (input) => {
+ const response = useData({
+ swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
+ })
+ return useMemo(
+ () =>
+ Object.create(response, {
+ isEmpty: {
+ get() {
+ return (response.data?.lineItems.length ?? 0) <= 0
+ },
+ enumerable: true,
+ },
+ }),
+ [response]
+ )
+ },
+}
diff --git a/framework/saleor/cart/use-remove-item.tsx b/framework/saleor/cart/use-remove-item.tsx
new file mode 100644
index 000000000..81f9c122f
--- /dev/null
+++ b/framework/saleor/cart/use-remove-item.tsx
@@ -0,0 +1,39 @@
+import { useCallback } from 'react'
+import type { MutationHookContext, HookFetcherContext, MutationHook } from '@commerce/utils/types'
+import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
+import useCart from './use-cart'
+import * as mutation from '../utils/mutations'
+import { getCheckoutId, checkoutToCart } from '../utils'
+import { Mutation, MutationCheckoutLineDeleteArgs } from '../schema'
+import { LineItem, RemoveItemHook } from '../types/cart'
+
+export default useRemoveItem as UseRemoveItem
+
+export const handler = {
+ fetchOptions: { query: mutation.CheckoutLineDelete },
+ async fetcher({ input: { itemId }, options, fetch }: HookFetcherContext) {
+ const data = await fetch({
+ ...options,
+ variables: {
+ checkoutId: getCheckoutId().checkoutId,
+ lineId: itemId,
+ },
+ })
+ return checkoutToCart(data.checkoutLineDelete)
+ },
+ useHook: ({ fetch }: MutationHookContext) => <
+ T extends LineItem | undefined = undefined
+ > () => {
+ const { mutate } = useCart()
+
+ return useCallback(
+ async function removeItem(input) {
+ const data = await fetch({ input: { itemId: input.id } })
+ await mutate(data, false)
+
+ return data
+ },
+ [fetch, mutate]
+ );
+ },
+}
diff --git a/framework/saleor/cart/use-update-item.tsx b/framework/saleor/cart/use-update-item.tsx
new file mode 100644
index 000000000..361ae5cdf
--- /dev/null
+++ b/framework/saleor/cart/use-update-item.tsx
@@ -0,0 +1,99 @@
+import { useCallback } from 'react'
+import debounce from 'lodash.debounce'
+import type { HookFetcherContext, MutationHookContext } from '@commerce/utils/types'
+import { ValidationError } from '@commerce/utils/errors'
+import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
+
+import useCart from './use-cart'
+import { handler as removeItemHandler } from './use-remove-item'
+import type { LineItem } from '../types'
+import { checkoutToCart } from '../utils'
+import { getCheckoutId } from '../utils'
+import { Mutation, MutationCheckoutLinesUpdateArgs } from '../schema'
+
+import * as mutation from '../utils/mutations'
+
+import type { UpdateItemHook } from '../types/cart'
+
+export type UpdateItemActionInput = T extends LineItem
+ ? Partial
+ : UpdateItemHook['actionInput']
+
+export default useUpdateItem as UseUpdateItem
+
+export const handler = {
+ fetchOptions: { query: mutation.CheckoutLineUpdate },
+ async fetcher({
+ input: { itemId, item },
+ options,
+ fetch
+ }: HookFetcherContext) {
+ if (Number.isInteger(item.quantity)) {
+ // Also allow the update hook to remove an item if the quantity is lower than 1
+ if (item.quantity! < 1) {
+ return removeItemHandler.fetcher({
+ options: removeItemHandler.fetchOptions,
+ input: { itemId },
+ fetch,
+ })
+ }
+ } else if (item.quantity) {
+ throw new ValidationError({
+ message: 'The item quantity has to be a valid integer',
+ })
+ }
+
+ const checkoutId = getCheckoutId().checkoutId
+ const { checkoutLinesUpdate } = await fetch({
+ ...options,
+ variables: {
+ checkoutId,
+ lineItems: [
+ {
+ variantId: item.variantId,
+ quantity: item.quantity,
+ },
+ ],
+ },
+ })
+
+ return checkoutToCart(checkoutLinesUpdate)
+ },
+ useHook: ({ fetch }: MutationHookContext) =>
+ (
+ ctx: {
+ item?: T
+ wait?: number
+ } = {}
+ ) => {
+ const { item } = ctx
+ const { mutate } = useCart() as any
+
+ return useCallback(
+ debounce(async (input: UpdateItemActionInput) => {
+ const itemId = input.id ?? item?.id
+ const productId = input.productId ?? item?.productId
+ const variantId = input.productId ?? item?.variantId
+ if (!itemId || !productId || !variantId) {
+ throw new ValidationError({
+ message: 'Invalid input used for this operation',
+ })
+ }
+
+ const data = await fetch({
+ input: {
+ item: {
+ productId,
+ variantId,
+ quantity: input.quantity,
+ },
+ itemId,
+ },
+ })
+ await mutate(data, false)
+ return data
+ }, ctx.wait ?? 500),
+ [fetch, mutate]
+ )
+ },
+}
diff --git a/framework/saleor/commerce.config.json b/framework/saleor/commerce.config.json
new file mode 100644
index 000000000..d5a1ac35d
--- /dev/null
+++ b/framework/saleor/commerce.config.json
@@ -0,0 +1,7 @@
+{
+ "provider": "saleor",
+ "features": {
+ "wishlist": false,
+ "customCheckout": true
+ }
+}
diff --git a/framework/saleor/const.ts b/framework/saleor/const.ts
new file mode 100644
index 000000000..df348770d
--- /dev/null
+++ b/framework/saleor/const.ts
@@ -0,0 +1,5 @@
+export const API_URL = process.env.NEXT_PUBLIC_SALEOR_API_URL
+export const API_CHANNEL = process.env.NEXT_PUBLIC_SALEOR_CHANNEL
+export const CHECKOUT_ID_COOKIE = 'saleor.CheckoutID'
+export const SALEOR_TOKEN = 'saleor.Token'
+export const SALEOR_CRSF_TOKEN = 'saleor.CSRFToken'
diff --git a/framework/saleor/customer/index.ts b/framework/saleor/customer/index.ts
new file mode 100644
index 000000000..6c903ecc5
--- /dev/null
+++ b/framework/saleor/customer/index.ts
@@ -0,0 +1 @@
+export { default as useCustomer } from './use-customer'
diff --git a/framework/saleor/customer/use-customer.tsx b/framework/saleor/customer/use-customer.tsx
new file mode 100644
index 000000000..1e0e63d5a
--- /dev/null
+++ b/framework/saleor/customer/use-customer.tsx
@@ -0,0 +1,30 @@
+import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
+import { CustomerHook } from '@commerce/types/customer'
+import { SWRHook } from '@commerce/utils/types'
+
+import * as query from '../utils/queries'
+
+export default useCustomer as UseCustomer
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: query.CustomerCurrent,
+ },
+ async fetcher({ options, fetch }) {
+ const data = await fetch({
+ ...options,
+ variables: {},
+ })
+ return data.me ?? null
+ },
+ useHook:
+ ({ useData }) =>
+ (input) => {
+ return useData({
+ swrOptions: {
+ revalidateOnFocus: false,
+ ...input?.swrOptions,
+ },
+ })
+ },
+}
diff --git a/framework/saleor/fetcher.ts b/framework/saleor/fetcher.ts
new file mode 100644
index 000000000..9d3c0bf89
--- /dev/null
+++ b/framework/saleor/fetcher.ts
@@ -0,0 +1,20 @@
+import { Fetcher } from '@commerce/utils/types'
+import { API_URL } from './const'
+import { getToken, handleFetchResponse } from './utils'
+
+const fetcher: Fetcher = async ({ url = API_URL, method = 'POST', variables, query }) => {
+ const token = getToken()
+
+ return handleFetchResponse(
+ await fetch(url!, {
+ method,
+ body: JSON.stringify({ query, variables }),
+ headers: {
+ Authorization: `JWT ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ })
+ )
+}
+
+export default fetcher
diff --git a/framework/saleor/index.tsx b/framework/saleor/index.tsx
new file mode 100644
index 000000000..5c9e61ec8
--- /dev/null
+++ b/framework/saleor/index.tsx
@@ -0,0 +1,32 @@
+import * as React from 'react'
+import { ReactNode } from 'react'
+
+import { CommerceConfig, CommerceProvider as CoreCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
+
+import { saleorProvider, SaleorProvider } from './provider'
+import * as Const from './const'
+
+export { saleorProvider }
+export type { SaleorProvider }
+
+export const saleorConfig: CommerceConfig = {
+ locale: 'en-us',
+ cartCookie: Const.CHECKOUT_ID_COOKIE,
+}
+
+export type SaleorConfig = Partial
+
+export type SaleorProps = {
+ children?: ReactNode
+ locale: string
+} & SaleorConfig
+
+export function CommerceProvider({ children, ...config }: SaleorProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export const useCommerce = () => useCoreCommerce()
diff --git a/framework/saleor/next.config.js b/framework/saleor/next.config.js
new file mode 100644
index 000000000..397a37b2a
--- /dev/null
+++ b/framework/saleor/next.config.js
@@ -0,0 +1,8 @@
+const commerce = require('./commerce.config.json')
+
+module.exports = {
+ commerce,
+ images: {
+ domains: [process.env.COMMERCE_IMAGE_HOST],
+ },
+}
diff --git a/framework/saleor/product/use-price.tsx b/framework/saleor/product/use-price.tsx
new file mode 100644
index 000000000..0174faf5e
--- /dev/null
+++ b/framework/saleor/product/use-price.tsx
@@ -0,0 +1,2 @@
+export * from '@commerce/product/use-price'
+export { default } from '@commerce/product/use-price'
diff --git a/framework/saleor/product/use-search.tsx b/framework/saleor/product/use-search.tsx
new file mode 100644
index 000000000..cc763c4b7
--- /dev/null
+++ b/framework/saleor/product/use-search.tsx
@@ -0,0 +1,74 @@
+import { SWRHook } from '@commerce/utils/types'
+import { Product } from '@commerce/types/product'
+import useSearch, { UseSearch } from '@commerce/product/use-search'
+
+import { ProductCountableEdge } from '../schema'
+import { getSearchVariables, normalizeProduct } from '../utils'
+
+import * as query from '../utils/queries'
+import { SearchProductsHook } from '@commerce/types/product'
+
+export default useSearch as UseSearch
+
+export type SearchProductsInput = {
+ search?: string
+ categoryId?: string | number
+ brandId?: string | number
+ sort?: string
+}
+
+export type SearchProductsData = {
+ products: Product[]
+ found: boolean
+}
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: query.ProductMany,
+ },
+ async fetcher({ input, options, fetch }) {
+ const { categoryId, brandId } = input
+
+ const data = await fetch({
+ query: categoryId ? query.CollectionOne : options.query,
+ method: options?.method,
+ variables: getSearchVariables(input),
+ })
+
+ let edges
+
+ if (categoryId) {
+ edges = data.collection?.products?.edges ?? []
+ // FIXME @zaiste, no `vendor` in Saleor
+ // if (brandId) {
+ // edges = edges.filter(
+ // ({ node: { vendor } }: ProductCountableEdge) =>
+ // vendor.replace(/\s+/g, '-').toLowerCase() === brandId
+ // )
+ // }
+ } else {
+ edges = data.products?.edges ?? []
+ }
+
+ return {
+ products: edges.map(({ node }: ProductCountableEdge) => normalizeProduct(node)),
+ found: !!edges.length,
+ }
+ },
+ useHook:
+ ({ useData }) =>
+ (input = {}) => {
+ return useData({
+ input: [
+ ['search', input.search],
+ ['categoryId', input.categoryId],
+ ['brandId', input.brandId],
+ ['sort', input.sort],
+ ],
+ swrOptions: {
+ revalidateOnFocus: false,
+ ...input.swrOptions,
+ },
+ })
+ },
+}
diff --git a/framework/saleor/provider.ts b/framework/saleor/provider.ts
new file mode 100644
index 000000000..2ca96475a
--- /dev/null
+++ b/framework/saleor/provider.ts
@@ -0,0 +1,26 @@
+import { handler as useCart } from './cart/use-cart'
+import { handler as useAddItem } from './cart/use-add-item'
+import { handler as useUpdateItem } from './cart/use-update-item'
+import { handler as useRemoveItem } from './cart/use-remove-item'
+
+import { handler as useCustomer } from './customer/use-customer'
+import { handler as useSearch } from './product/use-search'
+
+import { handler as useLogin } from './auth/use-login'
+import { handler as useLogout } from './auth/use-logout'
+import { handler as useSignup } from './auth/use-signup'
+
+import fetcher from './fetcher'
+
+export const saleorProvider = {
+ locale: 'en-us',
+ cartCookie: '',
+ cartCookieToken: '',
+ fetcher,
+ cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
+ customer: { useCustomer },
+ products: { useSearch },
+ auth: { useLogin, useLogout, useSignup },
+}
+
+export type SaleorProvider = typeof saleorProvider
diff --git a/framework/saleor/schema.d.ts b/framework/saleor/schema.d.ts
new file mode 100644
index 000000000..339e7269d
--- /dev/null
+++ b/framework/saleor/schema.d.ts
@@ -0,0 +1,11488 @@
+export type Maybe = T | null
+export type Exact = {
+ [K in keyof T]: T[K]
+}
+export type MakeOptional = Omit & { [SubKey in K]?: Maybe }
+export type MakeMaybe = Omit & { [SubKey in K]: Maybe }
+/** All built-in and custom scalars, mapped to their actual values */
+export type Scalars = {
+ ID: string
+ String: string
+ Boolean: boolean
+ Int: number
+ Float: number
+ /**
+ * The `Date` scalar type represents a Date
+ * value as specified by
+ * [iso8601](https://en.wikipedia.org/wiki/ISO_8601).
+ */
+ Date: any
+ /**
+ * The `DateTime` scalar type represents a DateTime
+ * value as specified by
+ * [iso8601](https://en.wikipedia.org/wiki/ISO_8601).
+ */
+ DateTime: any
+ /**
+ * The `GenericScalar` scalar type represents a generic
+ * GraphQL scalar value that could be:
+ * String, Boolean, Int, Float, List or Object.
+ */
+ GenericScalar: any
+ /**
+ * Allows use of a JSON String for input / output from the GraphQL schema.
+ *
+ * Use of this type is *not recommended* as you lose the benefits of having a defined, static
+ * schema (one of the key benefits of GraphQL).
+ */
+ JSONString: any
+ /**
+ * Positive Decimal scalar implementation.
+ *
+ * Should be used in places where value must be positive.
+ */
+ PositiveDecimal: any
+ UUID: any
+ /** Variables of this type must be set to null in mutations. They will be replaced with a filename from a following multipart part containing a binary file. See: https://github.com/jaydenseric/graphql-multipart-request-spec. */
+ Upload: any
+ WeightScalar: any
+ /** Anything */
+ _Any: any
+}
+
+/** Create a new address for the customer. */
+export type AccountAddressCreate = {
+ __typename?: 'AccountAddressCreate'
+ /** A user instance for which the address was created. */
+ user?: Maybe
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+ address?: Maybe
+}
+
+/** Delete an address of the logged-in user. */
+export type AccountAddressDelete = {
+ __typename?: 'AccountAddressDelete'
+ /** A user instance for which the address was deleted. */
+ user?: Maybe
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+ address?: Maybe
+}
+
+/** Updates an address of the logged-in user. */
+export type AccountAddressUpdate = {
+ __typename?: 'AccountAddressUpdate'
+ /** A user object for which the address was edited. */
+ user?: Maybe
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+ address?: Maybe
+}
+
+/** Remove user account. */
+export type AccountDelete = {
+ __typename?: 'AccountDelete'
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+ user?: Maybe
+}
+
+export type AccountError = {
+ __typename?: 'AccountError'
+ /** Name of a field that caused the error. A value of `null` indicates that the error isn't associated with a particular field. */
+ field?: Maybe
+ /** The error message. */
+ message?: Maybe
+ /** The error code. */
+ code: AccountErrorCode
+ /** A type of address that causes the error. */
+ addressType?: Maybe
+}
+
+/** An enumeration. */
+export enum AccountErrorCode {
+ ActivateOwnAccount = 'ACTIVATE_OWN_ACCOUNT',
+ ActivateSuperuserAccount = 'ACTIVATE_SUPERUSER_ACCOUNT',
+ DuplicatedInputItem = 'DUPLICATED_INPUT_ITEM',
+ DeactivateOwnAccount = 'DEACTIVATE_OWN_ACCOUNT',
+ DeactivateSuperuserAccount = 'DEACTIVATE_SUPERUSER_ACCOUNT',
+ DeleteNonStaffUser = 'DELETE_NON_STAFF_USER',
+ DeleteOwnAccount = 'DELETE_OWN_ACCOUNT',
+ DeleteStaffAccount = 'DELETE_STAFF_ACCOUNT',
+ DeleteSuperuserAccount = 'DELETE_SUPERUSER_ACCOUNT',
+ GraphqlError = 'GRAPHQL_ERROR',
+ Inactive = 'INACTIVE',
+ Invalid = 'INVALID',
+ InvalidPassword = 'INVALID_PASSWORD',
+ LeftNotManageablePermission = 'LEFT_NOT_MANAGEABLE_PERMISSION',
+ InvalidCredentials = 'INVALID_CREDENTIALS',
+ NotFound = 'NOT_FOUND',
+ OutOfScopeUser = 'OUT_OF_SCOPE_USER',
+ OutOfScopeGroup = 'OUT_OF_SCOPE_GROUP',
+ OutOfScopePermission = 'OUT_OF_SCOPE_PERMISSION',
+ PasswordEntirelyNumeric = 'PASSWORD_ENTIRELY_NUMERIC',
+ PasswordTooCommon = 'PASSWORD_TOO_COMMON',
+ PasswordTooShort = 'PASSWORD_TOO_SHORT',
+ PasswordTooSimilar = 'PASSWORD_TOO_SIMILAR',
+ Required = 'REQUIRED',
+ Unique = 'UNIQUE',
+ JwtSignatureExpired = 'JWT_SIGNATURE_EXPIRED',
+ JwtInvalidToken = 'JWT_INVALID_TOKEN',
+ JwtDecodeError = 'JWT_DECODE_ERROR',
+ JwtMissingToken = 'JWT_MISSING_TOKEN',
+ JwtInvalidCsrfToken = 'JWT_INVALID_CSRF_TOKEN',
+ ChannelInactive = 'CHANNEL_INACTIVE',
+ MissingChannelSlug = 'MISSING_CHANNEL_SLUG',
+}
+
+export type AccountInput = {
+ /** Given name. */
+ firstName?: Maybe
+ /** Family name. */
+ lastName?: Maybe
+ /** Billing address of the customer. */
+ defaultBillingAddress?: Maybe
+ /** Shipping address of the customer. */
+ defaultShippingAddress?: Maybe
+ /** User language code. */
+ languageCode?: Maybe
+}
+
+/** Register a new user. */
+export type AccountRegister = {
+ __typename?: 'AccountRegister'
+ /** Informs whether users need to confirm their email address. */
+ requiresConfirmation?: Maybe
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+ user?: Maybe
+}
+
+export type AccountRegisterInput = {
+ /** The email address of the user. */
+ email: Scalars['String']
+ /** Password. */
+ password: Scalars['String']
+ /** Base of frontend URL that will be needed to create confirmation URL. */
+ redirectUrl?: Maybe
+ /** User language code. */
+ languageCode?: Maybe
+ /** User public metadata. */
+ metadata?: Maybe>
+ /** Slug of a channel which will be used to notify users. Optional when only one channel exists. */
+ channel?: Maybe
+}
+
+/** Sends an email with the account removal link for the logged-in user. */
+export type AccountRequestDeletion = {
+ __typename?: 'AccountRequestDeletion'
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+}
+
+/** Sets a default address for the authenticated user. */
+export type AccountSetDefaultAddress = {
+ __typename?: 'AccountSetDefaultAddress'
+ /** An updated user instance. */
+ user?: Maybe
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+}
+
+/** Updates the account of the logged-in user. */
+export type AccountUpdate = {
+ __typename?: 'AccountUpdate'
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+ user?: Maybe
+}
+
+/** Represents user address data. */
+export type Address = Node & {
+ __typename?: 'Address'
+ /** The ID of the object. */
+ id: Scalars['ID']
+ firstName: Scalars['String']
+ lastName: Scalars['String']
+ companyName: Scalars['String']
+ streetAddress1: Scalars['String']
+ streetAddress2: Scalars['String']
+ city: Scalars['String']
+ cityArea: Scalars['String']
+ postalCode: Scalars['String']
+ /** Shop's default country. */
+ country: CountryDisplay
+ countryArea: Scalars['String']
+ phone?: Maybe
+ /** Address is user's default shipping address. */
+ isDefaultShippingAddress?: Maybe
+ /** Address is user's default billing address. */
+ isDefaultBillingAddress?: Maybe
+}
+
+/** Creates user address. */
+export type AddressCreate = {
+ __typename?: 'AddressCreate'
+ /** A user instance for which the address was created. */
+ user?: Maybe
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+ address?: Maybe
+}
+
+/** Deletes an address. */
+export type AddressDelete = {
+ __typename?: 'AddressDelete'
+ /** A user instance for which the address was deleted. */
+ user?: Maybe
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+ address?: Maybe
+}
+
+export type AddressInput = {
+ /** Given name. */
+ firstName?: Maybe
+ /** Family name. */
+ lastName?: Maybe
+ /** Company or organization. */
+ companyName?: Maybe
+ /** Address. */
+ streetAddress1?: Maybe
+ /** Address. */
+ streetAddress2?: Maybe
+ /** City. */
+ city?: Maybe
+ /** District. */
+ cityArea?: Maybe
+ /** Postal code. */
+ postalCode?: Maybe
+ /** Country. */
+ country?: Maybe
+ /** State or province. */
+ countryArea?: Maybe
+ /** Phone number. */
+ phone?: Maybe
+}
+
+/** Sets a default address for the given user. */
+export type AddressSetDefault = {
+ __typename?: 'AddressSetDefault'
+ /** An updated user instance. */
+ user?: Maybe
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+}
+
+/** An enumeration. */
+export enum AddressTypeEnum {
+ Billing = 'BILLING',
+ Shipping = 'SHIPPING',
+}
+
+/** Updates an address. */
+export type AddressUpdate = {
+ __typename?: 'AddressUpdate'
+ /** A user object for which the address was edited. */
+ user?: Maybe
+ /** @deprecated Use errors field instead. This field will be removed in Saleor 4.0. */
+ accountErrors: Array
+ errors: Array
+ address?: Maybe
+}
+
+export type AddressValidationData = {
+ __typename?: 'AddressValidationData'
+ countryCode?: Maybe