mirror of
https://github.com/vercel/commerce.git
synced 2025-03-14 14:42:31 +00:00
Updated Saleor Provider (#356)
* Initial work, copied from the Shopify provider * Added basis setup and type generation for the products queries * refactor: adjust the types * task: relax the Node.js constraint * fix: page/product properties * disable unknown fields * mention Saleor in the README * setup debugging for Next.js * Check nextjs-commerce bug if no images are added for a product * fix: client/server pecularities for env visibility Must prefix with `NEXT_PUBLIC_` so that the API URL is visible on the client * re: make search work with Saleor API (WIP) * task: update deps * task: move to Webpack 5.x * saleor: initial cart integration * update deps * saleor: shall the cart appear! * task: remove deprecated packages * saleor: adding/removing from the cart * saleor: preliminary signup process * saleor: fix the prices in the cart * update deps * update deps * Added the options for a variant to the product page * Mapped options to variants * Mapped options to variants * saleor: refine the auth process * saleor: remove unused code * saleor: handle customer find via refresh temporary solution * saleor: update deps * saleor: fix the session handling * saleor: fix the variants * saleor: simplify the naming for GraphQL statements * saleor: fix the type for collection * saleor: arrange the error codes * saleor: integrate collections * saleor: fix product sorting * saleor: set cookie location * saleor: update the schema * saleor: attach checkout to customer * saleor: fix the checkout flow * saleor: unify GraphQL naming approach * task: update deps * Add the env variables for saleor to the template * task: prettier * saleor: stub API for build/typescript compilation thanks @cond0r * task: temporarily disable for the `build` * saleor: refactor GraphQL queries * saleor: adjust the config * task: update dependencies * revert: Next.js to `10.0.9` * saleor: fix the checkout fetch query * task: update dependencies * saleor: adapt for displaying featured products * saleor: update the provider structure * saleor: make the home page representable * feature/cart: display the variant name (cond) Co-authored-by: Patryk Zawadzki <patrys@room-303.com> Co-authored-by: royderks <10717410+royderks@users.noreply.github.com>
This commit is contained in:
parent
685fb932db
commit
3b2bf654fe
@ -13,3 +13,6 @@ NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
||||
|
||||
NEXT_PUBLIC_SWELL_STORE_ID=
|
||||
NEXT_PUBLIC_SWELL_PUBLIC_KEY=
|
||||
|
||||
NEXT_PUBLIC_SALEOR_API_URL=
|
||||
NEXT_PUBLIC_SALEOR_CHANNEL=
|
||||
|
10
.prettierrc
10
.prettierrc
@ -2,5 +2,13 @@
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
"useTabs": false,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["framework/saleor/**/*"],
|
||||
"options": {
|
||||
"printWidth": 120
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
||||
- Swell Demo: https://swell.vercel.store/
|
||||
- BigCommerce Demo: https://bigcommerce.vercel.store/
|
||||
- Vendure Demo: https://vendure.vercel.store
|
||||
- Saleor Demo: https://saleor.vercel.store/
|
||||
|
||||
## Features
|
||||
|
||||
@ -26,7 +27,7 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
||||
|
||||
## Integrations
|
||||
|
||||
Next.js Commerce integrates out-of-the-box with BigCommerce and Shopify. We plan to support all major ecommerce backends.
|
||||
Next.js Commerce integrates out-of-the-box with BigCommerce, Shopify and Saleor. We plan to support all major ecommerce backends.
|
||||
|
||||
## Considerations
|
||||
|
||||
|
@ -127,4 +127,3 @@ a {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
.fit {
|
||||
min-height: calc(100vh - 88px);
|
||||
}
|
||||
}
|
||||
|
27
codegen.bigcommerce.json
Normal file
27
codegen.bigcommerce.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"schema": {
|
||||
"https://buybutton.store/graphql": {
|
||||
"headers": {
|
||||
"Authorization": "Bearer xzy"
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"./framework/bigcommerce/api/**/*.ts": {
|
||||
"noRequire": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"generates": {
|
||||
"./framework/bigcommerce/schema.d.ts": {
|
||||
"plugins": ["typescript", "typescript-operations"]
|
||||
},
|
||||
"./framework/bigcommerce/schema.graphql": {
|
||||
"plugins": ["schema-ast"]
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"afterAllFileWrite": ["prettier --write"]
|
||||
}
|
||||
}
|
22
codegen.json
22
codegen.json
@ -1,23 +1,29 @@
|
||||
{
|
||||
"schema": {
|
||||
"https://buybutton.store/graphql": {
|
||||
"headers": {
|
||||
"Authorization": "Bearer xzy"
|
||||
}
|
||||
}
|
||||
"https://master.staging.saleor.cloud/graphql/": {}
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"./framework/bigcommerce/api/**/*.ts": {
|
||||
"./framework/saleor/utils/queries/get-all-products-query.ts": {
|
||||
"noRequire": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"./framework/saleor/utils/queries/get-all-products-paths-query.ts": {
|
||||
"noRequire": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"./framework/saleor/utils/queries/get-products.ts": {
|
||||
"noRequire": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"generates": {
|
||||
"./framework/bigcommerce/schema.d.ts": {
|
||||
"./framework/saleor/schema.d.ts": {
|
||||
"plugins": ["typescript", "typescript-operations"]
|
||||
},
|
||||
"./framework/bigcommerce/schema.graphql": {
|
||||
"./framework/saleor/schema.graphql": {
|
||||
"plugins": ["schema-ast"]
|
||||
}
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"features": {
|
||||
"wishlist": true
|
||||
"wishlist": false,
|
||||
"customCheckout": false
|
||||
}
|
||||
}
|
||||
|
@ -108,10 +108,14 @@ const CartItem = ({
|
||||
<div className="flex-1 flex flex-col text-base">
|
||||
<Link href={`/product/${item.path}`}>
|
||||
<span
|
||||
className="font-bold text-lg cursor-pointer leading-6"
|
||||
onClick={() => closeSidebarIfPresent()}
|
||||
>
|
||||
{item.name}
|
||||
<div
|
||||
className="font-bold text-lg cursor-pointer leading-6"
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
{item.variant ? <span> {item.variant.name}</span> : ""}
|
||||
</span>
|
||||
</Link>
|
||||
{options && options.length > 0 ? (
|
||||
|
@ -49,13 +49,8 @@ const Layout: FC<Props> = ({
|
||||
children,
|
||||
pageProps: { categories = [], ...pageProps },
|
||||
}) => {
|
||||
const {
|
||||
displaySidebar,
|
||||
displayModal,
|
||||
closeSidebar,
|
||||
closeModal,
|
||||
modalView,
|
||||
} = useUI()
|
||||
const { displaySidebar, displayModal, closeSidebar, closeModal, modalView } =
|
||||
useUI()
|
||||
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
|
||||
const { locale = 'en-US' } = useRouter()
|
||||
|
||||
|
@ -10,7 +10,7 @@ interface Props {
|
||||
className?: string
|
||||
product: Product
|
||||
variant?: 'slim' | 'simple'
|
||||
imgProps?: Omit<ImageProps, 'src'>
|
||||
imgProps?: Omit<any, 'src'>
|
||||
}
|
||||
|
||||
const placeholderImg = '/product-img-placeholder.svg'
|
||||
@ -38,7 +38,7 @@ const ProductCard: FC<Props> = ({
|
||||
alt={product.name || 'Product Image'}
|
||||
height={320}
|
||||
width={320}
|
||||
layout="fixed"
|
||||
layout="fixed"
|
||||
{...imgProps}
|
||||
/>
|
||||
)}
|
||||
|
@ -32,10 +32,11 @@ const ProductView: FC<Props> = ({ product }) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Selects the default option
|
||||
product.variants[0].options?.forEach((v) => {
|
||||
const options = product.variants[0].options || []
|
||||
options.forEach((v) => {
|
||||
setChoices((choices) => ({
|
||||
...choices,
|
||||
[v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),
|
||||
[v.displayName.toLowerCase()]: v.values[0]?.label.toLowerCase(),
|
||||
}))
|
||||
})
|
||||
}, [])
|
||||
@ -126,7 +127,8 @@ const ProductView: FC<Props> = ({ product }) => {
|
||||
setChoices((choices) => {
|
||||
return {
|
||||
...choices,
|
||||
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
|
||||
[opt.displayName.toLowerCase()]:
|
||||
v.label.toLowerCase(),
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
@ -13,9 +13,8 @@ const Container: FC<Props> = ({ children, className, el = 'div', clean }) => {
|
||||
'mx-auto max-w-8xl px-6': !clean,
|
||||
})
|
||||
|
||||
let Component: React.ComponentType<
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
> = el as any
|
||||
let Component: React.ComponentType<React.HTMLAttributes<HTMLDivElement>> =
|
||||
el as any
|
||||
|
||||
return <Component className={rootClassName}>{children}</Component>
|
||||
}
|
||||
|
@ -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', 'saleor']
|
||||
|
||||
function getProviderName() {
|
||||
return (
|
||||
@ -18,6 +18,8 @@ function getProviderName() {
|
||||
? 'shopify'
|
||||
: process.env.NEXT_PUBLIC_SWELL_STORE_ID
|
||||
? 'swell'
|
||||
: process.env.NEXT_PUBLIC_SALEOR_API_URL
|
||||
? 'saleor'
|
||||
: null)
|
||||
)
|
||||
}
|
||||
|
@ -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:
|
||||
@ -156,24 +157,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 +220,20 @@ export const handler: MutationHook<Cart, {}, CartItemBody> = {
|
||||
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -11,18 +11,16 @@ type InferValue<Prop extends PropertyKey, Desc> = Desc extends {
|
||||
? Record<Prop, T>
|
||||
: 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<InferValue<Prop, Desc>>
|
||||
: Desc extends { writable: true }
|
||||
? InferValue<Prop, Desc>
|
||||
: Readonly<InferValue<Prop, Desc>>
|
||||
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<InferValue<Prop, Desc>>
|
||||
: Desc extends { writable: true }
|
||||
? InferValue<Prop, Desc>
|
||||
: Readonly<InferValue<Prop, Desc>>
|
||||
|
||||
export default function defineProperty<
|
||||
Obj extends object,
|
||||
|
4
framework/saleor/.env.template
Normal file
4
framework/saleor/.env.template
Normal file
@ -0,0 +1,4 @@
|
||||
COMMERCE_PROVIDER=saleor
|
||||
|
||||
NEXT_SALEOR_API_URL=
|
||||
NEXT_SALEOR_CHANNEL=
|
19
framework/saleor/README.md
Normal file
19
framework/saleor/README.md
Normal file
@ -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).
|
1
framework/saleor/api/cart.ts
Normal file
1
framework/saleor/api/cart.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/saleor/api/catalog/products.ts
Normal file
1
framework/saleor/api/catalog/products.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/saleor/api/checkout.ts
Normal file
1
framework/saleor/api/checkout.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/saleor/api/customers/index.ts
Normal file
1
framework/saleor/api/customers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/saleor/api/customers/login.ts
Normal file
1
framework/saleor/api/customers/login.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/saleor/api/customers/logout.ts
Normal file
1
framework/saleor/api/customers/logout.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/saleor/api/customers/signup.ts
Normal file
1
framework/saleor/api/customers/signup.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/saleor/api/endpoints/cart.ts
Normal file
1
framework/saleor/api/endpoints/cart.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function (_commerce: any) {}
|
1
framework/saleor/api/endpoints/catalog/products.ts
Normal file
1
framework/saleor/api/endpoints/catalog/products.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function (_commerce: any) {}
|
57
framework/saleor/api/endpoints/checkout/index.ts
Normal file
57
framework/saleor/api/endpoints/checkout/index.ts
Normal file
@ -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<CommerceAPI, CheckoutSchema>
|
||||
|
||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||
|
||||
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
|
||||
req,
|
||||
res,
|
||||
config,
|
||||
}) => {
|
||||
try {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Checkout</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style='margin: 10rem auto; text-align: center; font-family: SansSerif, "Segoe UI", Helvetica; color: #888;'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style='height: 60px; width: 60px;' fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<h1>Checkout not yet implemented :(</h1>
|
||||
<p>
|
||||
See <a href='https://github.com/vercel/commerce/issues/64' target='_blank'>#64</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
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<CheckoutAPI>({
|
||||
handler: checkoutEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default checkoutApi
|
1
framework/saleor/api/endpoints/customer.ts
Normal file
1
framework/saleor/api/endpoints/customer.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function (_commerce: any) {}
|
1
framework/saleor/api/endpoints/login.ts
Normal file
1
framework/saleor/api/endpoints/login.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function (_commerce: any) {}
|
1
framework/saleor/api/endpoints/logout.ts
Normal file
1
framework/saleor/api/endpoints/logout.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function (_commerce: any) {}
|
1
framework/saleor/api/endpoints/signup.ts
Normal file
1
framework/saleor/api/endpoints/signup.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function (_commerce: any) {}
|
1
framework/saleor/api/endpoints/wishlist.ts
Normal file
1
framework/saleor/api/endpoints/wishlist.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function (_commerce: any) {}
|
49
framework/saleor/api/index.ts
Normal file
49
framework/saleor/api/index.ts
Normal file
@ -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<P extends Provider = Provider> = CommerceAPI<P>
|
||||
|
||||
export function getCommerceApi<P extends Provider>(
|
||||
customProvider: P = provider as any
|
||||
): SaleorAPI<P> {
|
||||
return commerceApi(customProvider)
|
||||
}
|
50
framework/saleor/api/operations/get-all-pages.ts
Normal file
50
framework/saleor/api/operations/get-all-pages.ts
Normal file
@ -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<Provider>) {
|
||||
|
||||
async function getAllPages({
|
||||
query = Query.PageMany,
|
||||
config,
|
||||
variables,
|
||||
}: {
|
||||
url?: string
|
||||
config?: Partial<SaleorConfig>
|
||||
variables?: QueryPagesArgs
|
||||
preview?: boolean
|
||||
query?: string
|
||||
} = {}): Promise<GetAllPagesResult> {
|
||||
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
|
||||
}
|
46
framework/saleor/api/operations/get-all-product-paths.ts
Normal file
46
framework/saleor/api/operations/get-all-product-paths.ts
Normal file
@ -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<Provider>) {
|
||||
|
||||
async function getAllProductPaths({
|
||||
query,
|
||||
config,
|
||||
variables,
|
||||
}: {
|
||||
query?: string
|
||||
config?: SaleorConfig
|
||||
variables?: any
|
||||
} = {}): Promise<GetAllProductPathsResult> {
|
||||
config = commerce.getConfig(config)
|
||||
|
||||
const products = await fetchAllProducts({
|
||||
config,
|
||||
query: getAllProductsPathsQuery,
|
||||
variables,
|
||||
})
|
||||
|
||||
return {
|
||||
products: products?.map(({ node: { slug } }: ProductCountableEdge) => ({
|
||||
path: `/${slug}`,
|
||||
})),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return getAllProductPaths
|
||||
}
|
67
framework/saleor/api/operations/get-all-products.ts
Normal file
67
framework/saleor/api/operations/get-all-products.ts
Normal file
@ -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<Provider>) {
|
||||
async function getAllProducts({
|
||||
query = Query.ProductMany,
|
||||
variables,
|
||||
config,
|
||||
featured,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: any
|
||||
config?: Partial<SaleorConfig>
|
||||
preview?: boolean
|
||||
featured?: boolean
|
||||
} = {}): Promise<ReturnType> {
|
||||
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
|
||||
}
|
51
framework/saleor/api/operations/get-page.ts
Normal file
51
framework/saleor/api/operations/get-page.ts
Normal file
@ -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 extends { page?: any } = { page?: Page }> = T
|
||||
|
||||
export default function getPageOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
|
||||
async function getPage({
|
||||
query = Query.PageOne,
|
||||
variables,
|
||||
config,
|
||||
}: {
|
||||
query?: string
|
||||
variables: QueryPageArgs,
|
||||
config?: Partial<SaleorConfig>
|
||||
preview?: boolean
|
||||
}): Promise<GetPageResult> {
|
||||
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
|
||||
}
|
46
framework/saleor/api/operations/get-product.ts
Normal file
46
framework/saleor/api/operations/get-product.ts
Normal file
@ -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<Provider>) {
|
||||
async function getProduct({
|
||||
query = Query.ProductOneBySlug,
|
||||
variables,
|
||||
config: cfg,
|
||||
}: {
|
||||
query?: string
|
||||
variables: Variables
|
||||
config?: Partial<SaleorConfig>
|
||||
preview?: boolean
|
||||
}): Promise<ReturnType> {
|
||||
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
|
||||
}
|
35
framework/saleor/api/operations/get-site-info.ts
Normal file
35
framework/saleor/api/operations/get-site-info.ts
Normal file
@ -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<Provider>) {
|
||||
async function getSiteInfo({
|
||||
query,
|
||||
config,
|
||||
variables,
|
||||
}: {
|
||||
query?: string
|
||||
config?: Partial<SaleorConfig>
|
||||
preview?: boolean
|
||||
variables?: any
|
||||
} = {}): Promise<GetSiteInfoResult> {
|
||||
const cfg = commerce.getConfig(config)
|
||||
|
||||
const categories = await getCategories(cfg)
|
||||
const brands = await getVendors(cfg)
|
||||
|
||||
return {
|
||||
categories,
|
||||
brands,
|
||||
}
|
||||
}
|
||||
|
||||
return getSiteInfo
|
||||
}
|
7
framework/saleor/api/operations/index.ts
Normal file
7
framework/saleor/api/operations/index.ts
Normal file
@ -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'
|
42
framework/saleor/api/operations/login.ts
Normal file
42
framework/saleor/api/operations/login.ts
Normal file
@ -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<Provider>) {
|
||||
async function login({
|
||||
query = Mutation.SessionCreate,
|
||||
variables,
|
||||
config,
|
||||
}: {
|
||||
query?: string
|
||||
variables: any
|
||||
res: ServerResponse
|
||||
config?: SaleorConfig
|
||||
}): Promise<any> {
|
||||
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
|
||||
}
|
41
framework/saleor/api/utils/fetch-all-products.ts
Normal file
41
framework/saleor/api/utils/fetch-all-products.ts
Normal file
@ -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<ProductCountableEdge[]> => {
|
||||
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
|
37
framework/saleor/api/utils/fetch-graphql-api.ts
Normal file
37
framework/saleor/api/utils/fetch-graphql-api.ts
Normal file
@ -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 '@framework/utils'
|
||||
|
||||
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
|
2
framework/saleor/api/utils/fetch.ts
Normal file
2
framework/saleor/api/utils/fetch.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import zeitFetch from '@vercel/fetch'
|
||||
export default zeitFetch()
|
22
framework/saleor/api/utils/is-allowed-method.ts
Normal file
22
framework/saleor/api/utils/is-allowed-method.ts
Normal file
@ -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
|
||||
}
|
1
framework/saleor/api/wishlist.ts
Normal file
1
framework/saleor/api/wishlist.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
63
framework/saleor/auth/use-login.tsx
Normal file
63
framework/saleor/auth/use-login.tsx
Normal file
@ -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<typeof handler>
|
||||
|
||||
export const handler: MutationHook<LoginHook> = {
|
||||
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<Mutation, MutationTokenCreateArgs>({
|
||||
...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]
|
||||
)
|
||||
},
|
||||
}
|
41
framework/saleor/auth/use-logout.tsx
Normal file
41
framework/saleor/auth/use-logout.tsx
Normal file
@ -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<typeof handler>
|
||||
|
||||
export const handler: MutationHook<LogoutHook> = {
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
56
framework/saleor/auth/use-signup.tsx
Normal file
56
framework/saleor/auth/use-signup.tsx
Normal file
@ -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<typeof handler>
|
||||
|
||||
export const handler: MutationHook<SignupHook> = {
|
||||
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<Mutation, MutationAccountRegisterArgs>({
|
||||
...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]
|
||||
)
|
||||
},
|
||||
}
|
4
framework/saleor/cart/index.ts
Normal file
4
framework/saleor/cart/index.ts
Normal file
@ -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'
|
54
framework/saleor/cart/use-add-item.tsx
Normal file
54
framework/saleor/cart/use-add-item.tsx
Normal file
@ -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<typeof handler>
|
||||
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
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<Mutation, MutationCheckoutLinesAddArgs>({
|
||||
...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]
|
||||
)
|
||||
},
|
||||
}
|
53
framework/saleor/cart/use-cart.tsx
Normal file
53
framework/saleor/cart/use-cart.tsx
Normal file
@ -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<typeof handler>
|
||||
|
||||
export const handler: SWRHook<GetCartHook> = {
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
39
framework/saleor/cart/use-remove-item.tsx
Normal file
39
framework/saleor/cart/use-remove-item.tsx
Normal file
@ -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<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: { query: mutation.CheckoutLineDelete },
|
||||
async fetcher({ input: { itemId }, options, fetch }: HookFetcherContext<RemoveItemHook>) {
|
||||
const data = await fetch<Mutation, MutationCheckoutLineDeleteArgs>({
|
||||
...options,
|
||||
variables: {
|
||||
checkoutId: getCheckoutId().checkoutId,
|
||||
lineId: itemId,
|
||||
},
|
||||
})
|
||||
return checkoutToCart(data.checkoutLineDelete)
|
||||
},
|
||||
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) => <
|
||||
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]
|
||||
);
|
||||
},
|
||||
}
|
99
framework/saleor/cart/use-update-item.tsx
Normal file
99
framework/saleor/cart/use-update-item.tsx
Normal file
@ -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 = any> = T extends LineItem
|
||||
? Partial<UpdateItemHook['actionInput']>
|
||||
: UpdateItemHook['actionInput']
|
||||
|
||||
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: { query: mutation.CheckoutLineUpdate },
|
||||
async fetcher({
|
||||
input: { itemId, item },
|
||||
options,
|
||||
fetch
|
||||
}: HookFetcherContext<UpdateItemHook>) {
|
||||
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<Mutation, MutationCheckoutLinesUpdateArgs>({
|
||||
...options,
|
||||
variables: {
|
||||
checkoutId,
|
||||
lineItems: [
|
||||
{
|
||||
variantId: item.variantId,
|
||||
quantity: item.quantity,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
return checkoutToCart(checkoutLinesUpdate)
|
||||
},
|
||||
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) =>
|
||||
<T extends LineItem | undefined = undefined>(
|
||||
ctx: {
|
||||
item?: T
|
||||
wait?: number
|
||||
} = {}
|
||||
) => {
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart() as any
|
||||
|
||||
return useCallback(
|
||||
debounce(async (input: UpdateItemActionInput<T>) => {
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
7
framework/saleor/commerce.config.json
Normal file
7
framework/saleor/commerce.config.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"provider": "saleor",
|
||||
"features": {
|
||||
"wishlist": false,
|
||||
"customCheckout": true
|
||||
}
|
||||
}
|
5
framework/saleor/const.ts
Normal file
5
framework/saleor/const.ts
Normal file
@ -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'
|
1
framework/saleor/customer/index.ts
Normal file
1
framework/saleor/customer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as useCustomer } from './use-customer'
|
30
framework/saleor/customer/use-customer.tsx
Normal file
30
framework/saleor/customer/use-customer.tsx
Normal file
@ -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<typeof handler>
|
||||
|
||||
export const handler: SWRHook<CustomerHook> = {
|
||||
fetchOptions: {
|
||||
query: query.CustomerCurrent,
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
const data = await fetch<any | null>({
|
||||
...options,
|
||||
variables: {},
|
||||
})
|
||||
return data.me ?? null
|
||||
},
|
||||
useHook:
|
||||
({ useData }) =>
|
||||
(input) => {
|
||||
return useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input?.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
20
framework/saleor/fetcher.ts
Normal file
20
framework/saleor/fetcher.ts
Normal file
@ -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
|
32
framework/saleor/index.tsx
Normal file
32
framework/saleor/index.tsx
Normal file
@ -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<CommerceConfig>
|
||||
|
||||
export type SaleorProps = {
|
||||
children?: ReactNode
|
||||
locale: string
|
||||
} & SaleorConfig
|
||||
|
||||
export function CommerceProvider({ children, ...config }: SaleorProps) {
|
||||
return (
|
||||
<CoreCommerceProvider provider={saleorProvider} config={{ ...saleorConfig, ...config }}>
|
||||
{children}
|
||||
</CoreCommerceProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCommerce = () => useCoreCommerce()
|
8
framework/saleor/next.config.js
Normal file
8
framework/saleor/next.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
const commerce = require('./commerce.config.json')
|
||||
|
||||
module.exports = {
|
||||
commerce,
|
||||
images: {
|
||||
domains: [process.env.COMMERCE_IMAGE_HOST],
|
||||
},
|
||||
}
|
2
framework/saleor/product/use-price.tsx
Normal file
2
framework/saleor/product/use-price.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from '@commerce/product/use-price'
|
||||
export { default } from '@commerce/product/use-price'
|
74
framework/saleor/product/use-search.tsx
Normal file
74
framework/saleor/product/use-search.tsx
Normal file
@ -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<typeof handler>
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: string | number
|
||||
brandId?: string | number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export type SearchProductsData = {
|
||||
products: Product[]
|
||||
found: boolean
|
||||
}
|
||||
|
||||
export const handler: SWRHook<SearchProductsHook> = {
|
||||
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,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
26
framework/saleor/provider.ts
Normal file
26
framework/saleor/provider.ts
Normal file
@ -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
|
11488
framework/saleor/schema.d.ts
vendored
Normal file
11488
framework/saleor/schema.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
18973
framework/saleor/schema.graphql
Normal file
18973
framework/saleor/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
43
framework/saleor/types.ts
Normal file
43
framework/saleor/types.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { Cart as CoreCart } from '@commerce/types'
|
||||
import { CheckoutLine } from './schema'
|
||||
|
||||
export type SaleorCheckout = {
|
||||
id: string
|
||||
webUrl: string
|
||||
lineItems: CheckoutLine[]
|
||||
}
|
||||
|
||||
export type Cart = CoreCart.Cart & {
|
||||
lineItems: LineItem[]
|
||||
}
|
||||
export interface LineItem extends CoreCart.LineItem {
|
||||
options?: any[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Cart mutations
|
||||
*/
|
||||
|
||||
export type OptionSelections = {
|
||||
option_id: number
|
||||
option_value: number | string
|
||||
}
|
||||
|
||||
export type CartItemBody = CoreCart.CartItemBody & {
|
||||
productId: string // The product id is always required for BC
|
||||
optionSelections?: OptionSelections
|
||||
}
|
||||
|
||||
// export type GetCartHandlerBody = CoreCart.GetCartHandlerBody
|
||||
|
||||
// export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>
|
||||
|
||||
// export type AddCartItemHandlerBody = Core.AddCartItemHandlerBody<CartItemBody>
|
||||
|
||||
// export type UpdateCartItemBody = Core.UpdateCartItemBody<CartItemBody>
|
||||
|
||||
// export type UpdateCartItemHandlerBody = Core.UpdateCartItemHandlerBody<CartItemBody>
|
||||
|
||||
// export type RemoveCartItemBody = Core.RemoveCartItemBody
|
||||
|
||||
// export type RemoveCartItemHandlerBody = Core.RemoveCartItemHandlerBody
|
32
framework/saleor/types/cart.ts
Normal file
32
framework/saleor/types/cart.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import * as Core from '@commerce/types/cart'
|
||||
|
||||
export * from '@commerce/types/cart'
|
||||
|
||||
export type SaleorCart = {}
|
||||
|
||||
/**
|
||||
* Extend core cart types
|
||||
*/
|
||||
|
||||
export type Cart = Core.Cart & {
|
||||
lineItems: Core.LineItem[]
|
||||
url?: string
|
||||
}
|
||||
|
||||
export type CartTypes = Core.CartTypes
|
||||
|
||||
export type CartHooks = Core.CartHooks<CartTypes>
|
||||
|
||||
export type GetCartHook = CartHooks['getCart']
|
||||
export type AddItemHook = CartHooks['addItem']
|
||||
export type UpdateItemHook = CartHooks['updateItem']
|
||||
export type RemoveItemHook = CartHooks['removeItem']
|
||||
|
||||
export type CartSchema = Core.CartSchema<CartTypes>
|
||||
|
||||
export type CartHandlers = Core.CartHandlers<CartTypes>
|
||||
|
||||
export type GetCartHandler = CartHandlers['getCart']
|
||||
export type AddItemHandler = CartHandlers['addItem']
|
||||
export type UpdateItemHandler = CartHandlers['updateItem']
|
||||
export type RemoveItemHandler = CartHandlers['removeItem']
|
12
framework/saleor/utils/checkout-attach.ts
Normal file
12
framework/saleor/utils/checkout-attach.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as mutation from './mutations'
|
||||
import { CheckoutCustomerAttach } from '../schema'
|
||||
|
||||
export const checkoutAttach = async (fetch: any, { variables, headers }: any): Promise<CheckoutCustomerAttach> => {
|
||||
const data = await fetch({
|
||||
query: mutation.CheckoutAttach,
|
||||
variables,
|
||||
headers,
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
25
framework/saleor/utils/checkout-create.ts
Normal file
25
framework/saleor/utils/checkout-create.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
import * as mutation from './mutations'
|
||||
import { CheckoutCreate } from '../schema'
|
||||
import { CHECKOUT_ID_COOKIE } from '@framework/const'
|
||||
|
||||
export const checkoutCreate = async (fetch: any): Promise<CheckoutCreate> => {
|
||||
const data = await fetch({ query: mutation.CheckoutCreate })
|
||||
const checkout = data.checkoutCreate?.checkout
|
||||
const checkoutId = checkout?.id
|
||||
const checkoutToken = checkout?.token
|
||||
|
||||
const value = `${checkoutId}:${checkoutToken}`
|
||||
|
||||
if (checkoutId) {
|
||||
const options = {
|
||||
expires: 60 * 60 * 24 * 30,
|
||||
}
|
||||
Cookies.set(CHECKOUT_ID_COOKIE, value, options)
|
||||
}
|
||||
|
||||
return checkout
|
||||
}
|
||||
|
||||
export default checkoutCreate
|
35
framework/saleor/utils/checkout-to-cart.ts
Normal file
35
framework/saleor/utils/checkout-to-cart.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Cart } from '../types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
|
||||
import { CheckoutLinesAdd, CheckoutLinesUpdate, CheckoutCreate, CheckoutError, Checkout, Maybe, CheckoutLineDelete } from '../schema'
|
||||
|
||||
import { normalizeCart } from './normalize'
|
||||
import throwUserErrors from './throw-user-errors'
|
||||
|
||||
export type CheckoutQuery = {
|
||||
checkout: Checkout
|
||||
errors?: Array<CheckoutError>
|
||||
}
|
||||
|
||||
export type CheckoutPayload = CheckoutLinesAdd | CheckoutLinesUpdate | CheckoutCreate | CheckoutQuery | CheckoutLineDelete
|
||||
|
||||
const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => {
|
||||
if (!checkoutPayload) {
|
||||
throw new CommerceError({
|
||||
message: 'Missing checkout payload from response',
|
||||
})
|
||||
}
|
||||
|
||||
const checkout = checkoutPayload?.checkout
|
||||
throwUserErrors(checkoutPayload?.errors)
|
||||
|
||||
if (!checkout) {
|
||||
throw new CommerceError({
|
||||
message: 'Missing checkout object from response',
|
||||
})
|
||||
}
|
||||
|
||||
return normalizeCart(checkout)
|
||||
}
|
||||
|
||||
export default checkoutToCart
|
25
framework/saleor/utils/customer-token.ts
Normal file
25
framework/saleor/utils/customer-token.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import Cookies, { CookieAttributes } from 'js-cookie'
|
||||
import * as Const from '../const'
|
||||
|
||||
export const getToken = () => Cookies.get(Const.SALEOR_TOKEN)
|
||||
export const setToken = (token?: string, options?: CookieAttributes) => {
|
||||
setCookie(Const.SALEOR_TOKEN, token, options)
|
||||
}
|
||||
|
||||
export const getCSRFToken = () => Cookies.get(Const.SALEOR_CRSF_TOKEN)
|
||||
export const setCSRFToken = (token?: string, options?: CookieAttributes) => {
|
||||
setCookie(Const.SALEOR_CRSF_TOKEN, token, options)
|
||||
}
|
||||
|
||||
export const getCheckoutToken = () => Cookies.get(Const.CHECKOUT_ID_COOKIE)
|
||||
export const setCheckoutToken = (token?: string, options?: CookieAttributes) => {
|
||||
setCookie(Const.CHECKOUT_ID_COOKIE, token, options)
|
||||
}
|
||||
|
||||
const setCookie = (name: string, token?: string, options?: CookieAttributes) => {
|
||||
if (!token) {
|
||||
Cookies.remove(name)
|
||||
} else {
|
||||
Cookies.set(name, token, options ?? { expires: 60 * 60 * 24 * 30 })
|
||||
}
|
||||
}
|
49
framework/saleor/utils/fragments/checkout-details.ts
Normal file
49
framework/saleor/utils/fragments/checkout-details.ts
Normal file
@ -0,0 +1,49 @@
|
||||
export const CheckoutDetails = /* GraphQL */ `
|
||||
fragment CheckoutDetails on Checkout {
|
||||
id
|
||||
token
|
||||
created
|
||||
totalPrice {
|
||||
currency
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
}
|
||||
subtotalPrice {
|
||||
currency
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
}
|
||||
|
||||
lines {
|
||||
id
|
||||
variant {
|
||||
id
|
||||
name
|
||||
sku
|
||||
product {
|
||||
name
|
||||
slug
|
||||
}
|
||||
media {
|
||||
url
|
||||
}
|
||||
pricing {
|
||||
price {
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
quantity
|
||||
totalPrice {
|
||||
currency
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
2
framework/saleor/utils/fragments/index.ts
Normal file
2
framework/saleor/utils/fragments/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { ProductConnection } from './product'
|
||||
export { CheckoutDetails } from './checkout-details'
|
29
framework/saleor/utils/fragments/product.ts
Normal file
29
framework/saleor/utils/fragments/product.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export const ProductConnection = /* GraphQL */ `
|
||||
fragment ProductConnection on ProductCountableConnection {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
description
|
||||
slug
|
||||
pricing {
|
||||
priceRange {
|
||||
start {
|
||||
net {
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
media {
|
||||
url
|
||||
alt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
23
framework/saleor/utils/get-categories.ts
Normal file
23
framework/saleor/utils/get-categories.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Category } from '@commerce/types/site'
|
||||
import { SaleorConfig } from '../api'
|
||||
import { CollectionCountableEdge } from '../schema'
|
||||
import * as query from './queries'
|
||||
|
||||
const getCategories = async (config: SaleorConfig): Promise<Category[]> => {
|
||||
const { data } = await config.fetch(query.CollectionMany, {
|
||||
variables: {
|
||||
first: 100,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
data.collections?.edges?.map(({ node: { id, name, slug } }: CollectionCountableEdge) => ({
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
path: `/${slug}`,
|
||||
})) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
export default getCategories
|
9
framework/saleor/utils/get-checkout-id.ts
Normal file
9
framework/saleor/utils/get-checkout-id.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import Cookies from 'js-cookie'
|
||||
import { CHECKOUT_ID_COOKIE } from '../const'
|
||||
|
||||
const getCheckoutId = (id?: string) => {
|
||||
const r = Cookies.get(CHECKOUT_ID_COOKIE)?.split(':') || []
|
||||
return { checkoutId: r[0], checkoutToken: r[1] }
|
||||
}
|
||||
|
||||
export default getCheckoutId
|
18
framework/saleor/utils/get-search-variables.ts
Normal file
18
framework/saleor/utils/get-search-variables.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { getSortVariables } from './get-sort-variables'
|
||||
import type { SearchProductsInput } from '../product/use-search'
|
||||
|
||||
export const getSearchVariables = ({ brandId, search, categoryId, sort }: SearchProductsInput) => {
|
||||
const sortBy = {
|
||||
field: 'NAME',
|
||||
direction: 'ASC',
|
||||
...getSortVariables(sort, !!categoryId),
|
||||
channel: 'default-channel',
|
||||
}
|
||||
return {
|
||||
categoryId,
|
||||
filter: { search },
|
||||
sortBy,
|
||||
}
|
||||
}
|
||||
|
||||
export default getSearchVariables
|
30
framework/saleor/utils/get-sort-variables.ts
Normal file
30
framework/saleor/utils/get-sort-variables.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export const getSortVariables = (sort?: string, isCategory: boolean = false) => {
|
||||
let output = {}
|
||||
switch (sort) {
|
||||
case 'price-asc':
|
||||
output = {
|
||||
field: 'PRICE',
|
||||
direction: 'ASC',
|
||||
}
|
||||
break
|
||||
case 'price-desc':
|
||||
output = {
|
||||
field: 'PRICE',
|
||||
direction: 'DESC',
|
||||
}
|
||||
break
|
||||
case 'trending-desc':
|
||||
output = {
|
||||
field: 'RANK',
|
||||
direction: 'DESC',
|
||||
}
|
||||
break
|
||||
case 'latest-desc':
|
||||
output = {
|
||||
field: 'DATE',
|
||||
direction: 'DESC',
|
||||
}
|
||||
break
|
||||
}
|
||||
return output
|
||||
}
|
41
framework/saleor/utils/get-vendors.ts
Normal file
41
framework/saleor/utils/get-vendors.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { SaleorConfig } from '../api'
|
||||
|
||||
export type Brand = {
|
||||
entityId: string
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export type BrandEdge = {
|
||||
node: Brand
|
||||
}
|
||||
|
||||
export type Brands = BrandEdge[]
|
||||
|
||||
// TODO: Find a way to get vendors from meta
|
||||
const getVendors = async (config: SaleorConfig): Promise<BrandEdge[]> => {
|
||||
// const vendors = await fetchAllProducts({
|
||||
// config,
|
||||
// query: getAllProductVendors,
|
||||
// variables: {
|
||||
// first: 100,
|
||||
// },
|
||||
// })
|
||||
|
||||
// let vendorsStrings = vendors.map(({ node: { vendor } }) => vendor)
|
||||
|
||||
// return [...new Set(vendorsStrings)].map((v) => {
|
||||
// const id = v.replace(/\s+/g, '-').toLowerCase()
|
||||
// return {
|
||||
// node: {
|
||||
// entityId: id,
|
||||
// name: v,
|
||||
// path: `brands/${id}`,
|
||||
// },
|
||||
// }
|
||||
// })
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export default getVendors
|
27
framework/saleor/utils/handle-fetch-response.ts
Normal file
27
framework/saleor/utils/handle-fetch-response.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
|
||||
export function getError(errors: any[], status: number) {
|
||||
errors = errors ?? [{ message: 'Failed to fetch Saleor API' }]
|
||||
return new FetcherError({ errors, status })
|
||||
}
|
||||
|
||||
export async function getAsyncError(res: Response) {
|
||||
const data = await res.json()
|
||||
return getError(data.errors, res.status)
|
||||
}
|
||||
|
||||
const handleFetchResponse = async (res: Response) => {
|
||||
if (res.ok) {
|
||||
const { data, errors } = await res.json()
|
||||
|
||||
if (errors && errors.length) {
|
||||
throw getError(errors, res.status)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
throw await getAsyncError(res)
|
||||
}
|
||||
|
||||
export default handleFetchResponse
|
35
framework/saleor/utils/handle-login.ts
Normal file
35
framework/saleor/utils/handle-login.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { FetcherOptions } from '@commerce/utils/types'
|
||||
import { CreateToken, Mutation, MutationTokenCreateArgs } from '../schema'
|
||||
import { setToken, setCSRFToken } from './customer-token'
|
||||
import * as mutation from './mutations'
|
||||
import throwUserErrors from './throw-user-errors'
|
||||
|
||||
const handleLogin = (data: CreateToken) => {
|
||||
throwUserErrors(data?.errors)
|
||||
|
||||
const token = data?.token
|
||||
|
||||
if (token) {
|
||||
setToken(token)
|
||||
setCSRFToken(token)
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
export const handleAutomaticLogin = async (
|
||||
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
|
||||
input: MutationTokenCreateArgs
|
||||
) => {
|
||||
try {
|
||||
const { tokenCreate } = await fetch<Mutation, MutationTokenCreateArgs>({
|
||||
query: mutation.SessionCreate,
|
||||
variables: { ...input },
|
||||
})
|
||||
handleLogin(tokenCreate!)
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
export default handleLogin
|
19
framework/saleor/utils/index.ts
Normal file
19
framework/saleor/utils/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export { getSortVariables } from './get-sort-variables'
|
||||
|
||||
export { default as handleFetchResponse } from './handle-fetch-response'
|
||||
export { default as getSearchVariables } from './get-search-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 { checkoutAttach } from './checkout-attach'
|
||||
|
||||
export { default as checkoutToCart } from './checkout-to-cart'
|
||||
export { default as handleLogin, handleAutomaticLogin } from './handle-login'
|
||||
export { default as throwUserErrors } from './throw-user-errors'
|
||||
|
||||
export * from './queries'
|
||||
export * from './mutations'
|
||||
export * from './normalize'
|
||||
export * from './customer-token'
|
15
framework/saleor/utils/mutations/account-create.ts
Normal file
15
framework/saleor/utils/mutations/account-create.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const AccountCreate = /* GraphQL */ `
|
||||
mutation AccountCreate($input: AccountRegisterInput!) {
|
||||
accountRegister(input: $input) {
|
||||
errors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
user {
|
||||
email
|
||||
isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
12
framework/saleor/utils/mutations/checkout-attach.ts
Normal file
12
framework/saleor/utils/mutations/checkout-attach.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const CheckoutAttach = /* GraphQl */ `
|
||||
mutation CheckoutAttach($checkoutId: ID!) {
|
||||
checkoutCustomerAttach(checkoutId: $checkoutId) {
|
||||
errors {
|
||||
message
|
||||
}
|
||||
checkout {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
17
framework/saleor/utils/mutations/checkout-create.ts
Normal file
17
framework/saleor/utils/mutations/checkout-create.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as fragment from '../fragments'
|
||||
|
||||
export const CheckoutCreate = /* GraphQL */ `
|
||||
mutation CheckoutCreate {
|
||||
checkoutCreate(input: { email: "customer@example.com", lines: [], channel: "default-channel" }) {
|
||||
errors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
checkout {
|
||||
...CheckoutDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragment.CheckoutDetails}
|
||||
`
|
17
framework/saleor/utils/mutations/checkout-line-add.ts
Normal file
17
framework/saleor/utils/mutations/checkout-line-add.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as fragment from '../fragments'
|
||||
|
||||
export const CheckoutLineAdd = /* GraphQL */ `
|
||||
mutation CheckoutLineAdd($checkoutId: ID!, $lineItems: [CheckoutLineInput!]!) {
|
||||
checkoutLinesAdd(checkoutId: $checkoutId, lines: $lineItems) {
|
||||
errors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
checkout {
|
||||
...CheckoutDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragment.CheckoutDetails}
|
||||
`
|
17
framework/saleor/utils/mutations/checkout-line-remove.ts
Normal file
17
framework/saleor/utils/mutations/checkout-line-remove.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as fragment from '../fragments'
|
||||
|
||||
export const CheckoutLineDelete = /* GraphQL */ `
|
||||
mutation CheckoutLineDelete($checkoutId: ID!, $lineId: ID!) {
|
||||
checkoutLineDelete(checkoutId: $checkoutId, lineId: $lineId) {
|
||||
errors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
checkout {
|
||||
...CheckoutDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragment.CheckoutDetails}
|
||||
`
|
17
framework/saleor/utils/mutations/checkout-line-update.ts
Normal file
17
framework/saleor/utils/mutations/checkout-line-update.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as fragment from '../fragments'
|
||||
|
||||
export const CheckoutLineUpdate = /* GraphQL */ `
|
||||
mutation CheckoutLineUpdate($checkoutId: ID!, $lineItems: [CheckoutLineInput!]!) {
|
||||
checkoutLinesUpdate(checkoutId: $checkoutId, lines: $lineItems) {
|
||||
errors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
checkout {
|
||||
...CheckoutDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragment.CheckoutDetails}
|
||||
`
|
8
framework/saleor/utils/mutations/index.ts
Normal file
8
framework/saleor/utils/mutations/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export { AccountCreate } from './account-create'
|
||||
export { CheckoutCreate } from './checkout-create'
|
||||
export { CheckoutLineAdd } from './checkout-line-add'
|
||||
export { CheckoutLineUpdate } from './checkout-line-update'
|
||||
export { CheckoutLineDelete } from './checkout-line-remove'
|
||||
export { SessionCreate } from './session-create'
|
||||
export { SessionDestroy } from './session-destroy'
|
||||
export { CheckoutAttach } from './checkout-attach'
|
14
framework/saleor/utils/mutations/session-create.ts
Normal file
14
framework/saleor/utils/mutations/session-create.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const SessionCreate = /* GraphQL */ `
|
||||
mutation SessionCreate($email: String!, $password: String!) {
|
||||
tokenCreate(email: $email, password: $password) {
|
||||
token
|
||||
refreshToken
|
||||
csrfToken
|
||||
errors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
10
framework/saleor/utils/mutations/session-destroy.ts
Normal file
10
framework/saleor/utils/mutations/session-destroy.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const SessionDestroy = /* GraphQL */ `
|
||||
mutation SessionDestroy {
|
||||
tokensDeactivateAll {
|
||||
errors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
133
framework/saleor/utils/normalize.ts
Normal file
133
framework/saleor/utils/normalize.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { Product } from '@commerce/types/product'
|
||||
|
||||
import { Product as SaleorProduct, Checkout, CheckoutLine, Money, ProductVariant } from '../schema'
|
||||
|
||||
import type { Cart, LineItem } from '../types'
|
||||
|
||||
// TODO: Check nextjs-commerce bug if no images are added for a product
|
||||
const placeholderImg = '/product-img-placeholder.svg'
|
||||
|
||||
const money = ({ amount, currency }: Money) => {
|
||||
return {
|
||||
value: +amount,
|
||||
currencyCode: currency || 'USD',
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeProductOptions = (options: ProductVariant[]) => {
|
||||
return options
|
||||
?.map((option) => option?.attributes)
|
||||
.flat(1)
|
||||
.reduce<any>((acc, x) => {
|
||||
if (acc.find(({ displayName }: any) => displayName === x.attribute.name)) {
|
||||
return acc.map((opt: any) => {
|
||||
return opt.displayName === x.attribute.name
|
||||
? {
|
||||
...opt,
|
||||
values: [
|
||||
...opt.values,
|
||||
...x.values.map((value: any) => ({
|
||||
label: value?.name,
|
||||
})),
|
||||
],
|
||||
}
|
||||
: opt
|
||||
})
|
||||
}
|
||||
|
||||
return acc.concat({
|
||||
__typename: 'MultipleChoiceOption',
|
||||
displayName: x.attribute.name,
|
||||
variant: 'size',
|
||||
values: x.values.map((value: any) => ({
|
||||
label: value?.name,
|
||||
})),
|
||||
})
|
||||
}, [])
|
||||
}
|
||||
|
||||
const normalizeProductVariants = (variants: ProductVariant[]) => {
|
||||
return variants?.map((variant) => {
|
||||
const { id, sku, name, pricing } = variant
|
||||
const price = pricing?.price?.net && money(pricing.price.net)?.value
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
sku: sku ?? id,
|
||||
price,
|
||||
listPrice: price,
|
||||
requiresShipping: true,
|
||||
options: normalizeProductOptions([variant]),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeProduct(productNode: SaleorProduct): Product {
|
||||
const { id, name, media = [], variants, description, slug, pricing, ...rest } = productNode
|
||||
|
||||
const product = {
|
||||
id,
|
||||
name,
|
||||
vendor: '',
|
||||
description: description ? JSON.parse(description)?.blocks[0]?.data.text : '',
|
||||
path: `/${slug}`,
|
||||
slug: slug?.replace(/^\/+|\/+$/g, ''),
|
||||
price: (pricing?.priceRange?.start?.net && money(pricing.priceRange.start.net)) || {
|
||||
value: 0,
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
// TODO: Check nextjs-commerce bug if no images are added for a product
|
||||
images: media?.length ? media : [{ url: placeholderImg }],
|
||||
variants: variants && variants.length > 0 ? normalizeProductVariants(variants as ProductVariant[]) : [],
|
||||
options: variants && variants.length > 0 ? normalizeProductOptions(variants as ProductVariant[]) : [],
|
||||
...rest,
|
||||
}
|
||||
|
||||
return product as Product
|
||||
}
|
||||
|
||||
export function normalizeCart(checkout: Checkout): Cart {
|
||||
const lines = checkout.lines as CheckoutLine[]
|
||||
const lineItems: LineItem[] = lines.length > 0 ? lines?.map<LineItem>(normalizeLineItem) : []
|
||||
|
||||
return {
|
||||
id: checkout.id,
|
||||
customerId: '',
|
||||
email: '',
|
||||
createdAt: checkout.created,
|
||||
currency: {
|
||||
code: checkout.totalPrice?.currency!,
|
||||
},
|
||||
taxesIncluded: false,
|
||||
lineItems,
|
||||
lineItemsSubtotalPrice: checkout.subtotalPrice?.gross?.amount!,
|
||||
subtotalPrice: checkout.subtotalPrice?.gross?.amount!,
|
||||
totalPrice: checkout.totalPrice?.gross.amount!,
|
||||
discounts: [],
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLineItem({ id, variant, quantity }: CheckoutLine): LineItem {
|
||||
return {
|
||||
id,
|
||||
variantId: String(variant?.id),
|
||||
productId: String(variant?.id),
|
||||
name: `${variant.product.name}`,
|
||||
quantity,
|
||||
variant: {
|
||||
id: String(variant?.id),
|
||||
sku: variant?.sku ?? '',
|
||||
name: variant?.name!,
|
||||
image: {
|
||||
url: variant?.media![0] ? variant?.media![0].url : placeholderImg,
|
||||
},
|
||||
requiresShipping: false,
|
||||
price: variant?.pricing?.price?.gross.amount!,
|
||||
listPrice: 0,
|
||||
},
|
||||
path: String(variant?.product?.slug),
|
||||
discounts: [],
|
||||
options: [],
|
||||
}
|
||||
}
|
12
framework/saleor/utils/queries/checkout-one.ts
Normal file
12
framework/saleor/utils/queries/checkout-one.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as fragment from '../fragments'
|
||||
|
||||
export const CheckoutOne = /* GraphQL */ `
|
||||
query CheckoutOne($checkoutId: UUID!) {
|
||||
checkout(token: $checkoutId) {
|
||||
... on Checkout {
|
||||
...CheckoutDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragment.CheckoutDetails}
|
||||
`
|
13
framework/saleor/utils/queries/collection-many.ts
Normal file
13
framework/saleor/utils/queries/collection-many.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const CollectionMany = /* GraphQL */ `
|
||||
query CollectionMany($first: Int!, $channel: String = "default-channel") {
|
||||
collections(first: $first, channel: $channel) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
13
framework/saleor/utils/queries/collection-one.ts
Normal file
13
framework/saleor/utils/queries/collection-one.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as fragment from '../fragments'
|
||||
|
||||
export const CollectionOne = /* GraphQL */ `
|
||||
query getProductsFromCollection($categoryId: ID!, $first: Int = 100, $channel: String = "default-channel") {
|
||||
collection(id: $categoryId, channel: $channel) {
|
||||
id
|
||||
products(first: $first) {
|
||||
...ProductConnection
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragment.ProductConnection}
|
||||
`
|
11
framework/saleor/utils/queries/customer-current.ts
Normal file
11
framework/saleor/utils/queries/customer-current.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const CustomerCurrent = /* GraphQL */ `
|
||||
query CustomerCurrent {
|
||||
me {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
dateJoined
|
||||
}
|
||||
}
|
||||
`
|
7
framework/saleor/utils/queries/customer-one.ts
Normal file
7
framework/saleor/utils/queries/customer-one.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const CustomerOne = /* GraphQL */ `
|
||||
query CustomerOne($customerAccessToken: String!) {
|
||||
customer(customerAccessToken: $customerAccessToken) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
@ -0,0 +1,16 @@
|
||||
export const getAllProductVendors = /* GraphQL */ `
|
||||
query getAllProductVendors($first: Int = 250, $cursor: String) {
|
||||
products(first: $first, after: $cursor) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
vendor
|
||||
}
|
||||
cursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user