mirror of
https://github.com/vercel/commerce.git
synced 2025-06-19 05:31:22 +00:00
Update Vendure provider to latest changes
This commit is contained in:
parent
41e59e9a85
commit
23c3412c17
@ -4,8 +4,6 @@ UI hooks and data fetching methods built from the ground up for e-commerce appli
|
||||
|
||||
## Usage
|
||||
|
||||
As this is still under development, no npm package is yet provided. Here's how you can try it out:
|
||||
|
||||
1. First you'll need a Vendure server. You can your own local server up-and-running with just a single command:
|
||||
```shell
|
||||
npx @vendure/create my-app
|
||||
@ -25,30 +23,41 @@ As this is still under development, no npm package is yet provided. Here's how y
|
||||
3. Clone this repo and install its dependencies with `yarn install` or `npm install`
|
||||
4. Change the paths in [tsconfig.json](../../tsconfig.json) to point to the Vendure hooks:
|
||||
```diff
|
||||
"paths": {
|
||||
"@lib/*": ["lib/*"],
|
||||
"@assets/*": ["assets/*"],
|
||||
"@config/*": ["config/*"],
|
||||
"@components/*": ["components/*"],
|
||||
"@utils/*": ["utils/*"],
|
||||
"@commerce/*": ["framework/commerce/*"],
|
||||
"@commerce": ["framework/commerce"],
|
||||
- "@framework/*": ["framework/bigcommerce/*"],
|
||||
- "@framework": ["framework/bigcommerce"]
|
||||
+ "@framework/*": ["framework/vendure/*"],
|
||||
+ "@framework": ["framework/vendure"]
|
||||
}
|
||||
```
|
||||
5. Set the Vendure Shop API URL in your `.env.local` file:
|
||||
```sh
|
||||
NEXT_PUBLIC_VENDURE_SHOP_API_URL=http://localhost:3001/shop-api
|
||||
```
|
||||
6. With the Vendure server running, start this project using `yarn dev` or `npm run dev`.
|
||||
6. Add the `localhost` domain to the `images` property next.config.js file as per the [image optimization docs](https://nextjs.org/docs/basic-features/image-optimization#domains)
|
||||
```js
|
||||
module.exports = {
|
||||
// ...
|
||||
images: {
|
||||
domains: ['example.com'],
|
||||
},
|
||||
}
|
||||
```
|
||||
7. With the Vendure server running, start this project using `yarn dev` or `npm run dev`.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. Vendure does not ship with built-in wishlist functionality.
|
||||
2. Nor does it come with any kind of blog/page-building feature. Both of these can be created as Vendure plugins, however.
|
||||
3. The entire Vendure customer flow is carried out via its GraphQL API. This means that there is no external, pre-existing checkout flow. The checkout flow must be created as part of the Next.js app.
|
||||
3. The entire Vendure customer flow is carried out via its GraphQL API. This means that there is no external, pre-existing checkout flow. The checkout flow must be created as part of the Next.js app. See https://github.com/vercel/commerce/issues/64 for further discusion.
|
||||
4. By default, the sign-up flow in Vendure uses email verification. This means that using the existing "sign up" flow from this project will not grant a new user the ability to authenticate, since the new account must first be verified. Again, the necessary parts to support this flow can be created as part of the Next.js app.
|
||||
5. The mapping of products & variants may not totally match up with what the storefront expects. As the storefront matures and improves this mapping can be refined.
|
||||
|
||||
## Code generation
|
||||
|
||||
This provider makes use of GraphQL code generation. The [schema.graphql](./schema.graphql) and [schema.d.ts](./schema.d.ts) files contain the generated types & schema introspection results.
|
||||
|
||||
When developing the provider, changes to any GraphQL operations should be followed by re-generation of the types and schema files:
|
||||
|
||||
From the project root dir, run
|
||||
|
||||
```sh
|
||||
graphql-codegen --config ./framework/vendure/codegen.json
|
||||
```
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { BigcommerceConfig, getConfig } from '../../bigcommerce/api'
|
||||
import { NextApiHandler } from 'next'
|
||||
|
||||
const checkoutApi= async (req: any, res: any, config: any) => {
|
||||
const checkoutApi = async (req: any, res: any, config: any) => {
|
||||
try {
|
||||
// TODO: make the embedded checkout work too!
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@ -15,6 +14,9 @@ const checkoutApi= async (req: any, res: any, config: any) => {
|
||||
<body>
|
||||
<div style='margin: 10rem auto; text-align: center; font-family: SansSerif, "Segoe UI", Helvetica'>
|
||||
<h1>Checkout not implemented :(</h1>
|
||||
<p>
|
||||
See <a href='https://github.com/vercel/commerce/issues/64' target='_blank'>#64</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -33,11 +35,7 @@ const checkoutApi= async (req: any, res: any, config: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export function createApiHandler<
|
||||
T = any,
|
||||
H = {},
|
||||
Options extends {} = {}
|
||||
>(
|
||||
export function createApiHandler<T = any, H = {}, Options extends {} = {}>(
|
||||
handler: any,
|
||||
handlers: H,
|
||||
defaultOptions: Options
|
||||
|
@ -2,6 +2,7 @@ export const cartFragment = /* GraphQL */ `
|
||||
fragment Cart on Order {
|
||||
id
|
||||
code
|
||||
createdAt
|
||||
totalQuantity
|
||||
subTotal
|
||||
subTotalWithTax
|
||||
@ -14,13 +15,23 @@ export const cartFragment = /* GraphQL */ `
|
||||
lines {
|
||||
id
|
||||
quantity
|
||||
linePriceWithTax
|
||||
discountedLinePriceWithTax
|
||||
featuredAsset {
|
||||
id
|
||||
preview
|
||||
}
|
||||
discounts {
|
||||
description
|
||||
amount
|
||||
}
|
||||
productVariant {
|
||||
id
|
||||
name
|
||||
sku
|
||||
price
|
||||
priceWithTax
|
||||
stockLevel
|
||||
product {
|
||||
slug
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
type Header = string | number | string[] | undefined
|
||||
|
||||
export default function concatHeader(prev: Header, val: Header) {
|
||||
if (!val) return prev
|
||||
if (!prev) return val
|
||||
|
||||
if (Array.isArray(prev)) return prev.concat(String(val))
|
||||
|
||||
prev = String(prev)
|
||||
|
||||
if (Array.isArray(val)) return [prev].concat(val)
|
||||
|
||||
return [prev, String(val)]
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export { default as useLogin } from './use-login'
|
||||
export { default as useLogout } from './use-logout'
|
||||
export { default as useSignup } from './use-signup'
|
@ -1,82 +0,0 @@
|
||||
import type { ServerResponse } from 'http'
|
||||
import type { LoginMutation, LoginMutationVariables } from '../schema'
|
||||
import concatHeader from '../api/utils/concat-cookie'
|
||||
import { getConfig, VendureConfig } from '../api'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import { ErrorResult } from '../schema'
|
||||
|
||||
export const loginMutation = /* GraphQL */ `
|
||||
mutation loginServer($email: String!, $password: String!) {
|
||||
login(username: $email, password: $password) {
|
||||
__typename
|
||||
... on CurrentUser {
|
||||
id
|
||||
}
|
||||
... on ErrorResult {
|
||||
errorCode
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export type LoginResult<T extends { result?: any } = { result?: string }> = T
|
||||
|
||||
export type LoginVariables = LoginMutationVariables
|
||||
|
||||
async function login(opts: {
|
||||
variables: LoginVariables
|
||||
config?: VendureConfig
|
||||
res: ServerResponse
|
||||
}): Promise<LoginResult>
|
||||
|
||||
async function login<T extends { result?: any }, V = any>(opts: {
|
||||
query: string
|
||||
variables: V
|
||||
res: ServerResponse
|
||||
config?: VendureConfig
|
||||
}): Promise<LoginResult<T>>
|
||||
|
||||
async function login({
|
||||
query = loginMutation,
|
||||
variables,
|
||||
res: response,
|
||||
config,
|
||||
}: {
|
||||
query?: string
|
||||
variables: LoginVariables
|
||||
res: ServerResponse
|
||||
config?: VendureConfig
|
||||
}): Promise<LoginResult> {
|
||||
config = getConfig(config)
|
||||
|
||||
const { data, res } = await config.fetch<LoginMutation>(query, { variables })
|
||||
|
||||
if (data.login.__typename !== 'CurrentUser') {
|
||||
throw new CommerceError({ message: (data.login as ErrorResult).message })
|
||||
}
|
||||
// Bigcommerce returns a Set-Cookie header with the auth cookie
|
||||
let cookie = res.headers.get('Set-Cookie')
|
||||
|
||||
if (cookie && typeof cookie === 'string') {
|
||||
// In development, don't set a secure cookie or the browser will ignore it
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
cookie = cookie.replace('; Secure', '')
|
||||
// SameSite=none can't be set unless the cookie is Secure
|
||||
// bc seems to sometimes send back SameSite=None rather than none so make
|
||||
// this case insensitive
|
||||
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
|
||||
}
|
||||
|
||||
response.setHeader(
|
||||
'Set-Cookie',
|
||||
concatHeader(response.getHeader('Set-Cookie'), cookie)!
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
result: data.login.id.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
export default login
|
@ -1,13 +1,9 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCommerceLogin from '@commerce/use-login'
|
||||
import { MutationHook } from '@commerce/utils/types'
|
||||
import useLogin, { UseLogin } from '@commerce/auth/use-login'
|
||||
import { CommerceError, ValidationError } from '@commerce/utils/errors'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import {
|
||||
ErrorResult,
|
||||
LoginMutation,
|
||||
LoginMutationVariables,
|
||||
} from '@framework/schema'
|
||||
import { LoginMutation, LoginMutationVariables } from '../schema'
|
||||
|
||||
export const loginMutation = /* GraphQL */ `
|
||||
mutation login($username: String!, $password: String!) {
|
||||
@ -24,53 +20,45 @@ export const loginMutation = /* GraphQL */ `
|
||||
}
|
||||
`
|
||||
|
||||
export const fetcher: HookFetcher<LoginMutation, LoginMutationVariables> = (
|
||||
options,
|
||||
{ username, password },
|
||||
fetch
|
||||
) => {
|
||||
if (!(username && password)) {
|
||||
export default useLogin as UseLogin<typeof handler>
|
||||
|
||||
export const handler: MutationHook<null, {}, any> = {
|
||||
fetchOptions: {
|
||||
query: loginMutation,
|
||||
},
|
||||
async fetcher({ input: { email, password }, options, fetch }) {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message: 'An email address and password are required to login',
|
||||
message: 'A email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
query: loginMutation,
|
||||
variables: { username, password },
|
||||
})
|
||||
}
|
||||
const variables: LoginMutationVariables = {
|
||||
username: email,
|
||||
password,
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useLogin = () => {
|
||||
const { login } = await fetch<LoginMutation>({
|
||||
...options,
|
||||
variables,
|
||||
})
|
||||
|
||||
if (login.__typename !== 'CurrentUser') {
|
||||
throw new ValidationError(login)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
const fn = useCommerceLogin<LoginMutation, LoginMutationVariables>(
|
||||
{},
|
||||
customFetcher
|
||||
)
|
||||
|
||||
return useCallback(
|
||||
async function login(input: { email: string; password: string }) {
|
||||
const data = await fn({
|
||||
username: input.email,
|
||||
password: input.password,
|
||||
})
|
||||
if (data.login.__typename !== 'CurrentUser') {
|
||||
throw new CommerceError({
|
||||
message: (data.login as ErrorResult).message,
|
||||
})
|
||||
}
|
||||
async function login(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
[fetch, revalidate]
|
||||
)
|
||||
}
|
||||
|
||||
useLogin.extend = extendHook
|
||||
|
||||
return useLogin
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import useCommerceLogout from '@commerce/use-logout'
|
||||
import { MutationHook } from '@commerce/utils/types'
|
||||
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import { LogoutMutation } from '@framework/schema'
|
||||
import { LogoutMutation } from '../schema'
|
||||
|
||||
export const logoutMutation = /* GraphQL */ `
|
||||
mutation logout {
|
||||
@ -12,31 +12,28 @@ export const logoutMutation = /* GraphQL */ `
|
||||
}
|
||||
`
|
||||
|
||||
export const fetcher: HookFetcher<LogoutMutation> = (options, _, fetch) => {
|
||||
return fetch({
|
||||
...options,
|
||||
query: logoutMutation,
|
||||
})
|
||||
}
|
||||
export default useLogout as UseLogout<typeof handler>
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useLogout = () => {
|
||||
export const handler: MutationHook<null> = {
|
||||
fetchOptions: {
|
||||
query: logoutMutation,
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
await fetch<LogoutMutation>({
|
||||
...options,
|
||||
})
|
||||
return null
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCustomer()
|
||||
const fn = useCommerceLogout<LogoutMutation>({}, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function logout() {
|
||||
const data = await fn(null)
|
||||
await mutate(null as any, false)
|
||||
const data = await fetch()
|
||||
await mutate(null, false)
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
[fetch, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useLogout.extend = extendHook
|
||||
|
||||
return useLogout
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCommerceSignup from '@commerce/use-signup'
|
||||
import { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError, ValidationError } from '@commerce/utils/errors'
|
||||
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import {
|
||||
ErrorResult,
|
||||
RegisterCustomerInput,
|
||||
SignupMutation,
|
||||
SignupMutationVariables,
|
||||
} from '@framework/schema'
|
||||
} from '../schema'
|
||||
|
||||
export const signupMutation = /* GraphQL */ `
|
||||
mutation signup($input: RegisterCustomerInput!) {
|
||||
@ -24,66 +24,57 @@ export const signupMutation = /* GraphQL */ `
|
||||
}
|
||||
`
|
||||
|
||||
export type SignupInput = {
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
password: string
|
||||
}
|
||||
export default useSignup as UseSignup<typeof handler>
|
||||
|
||||
export const fetcher: HookFetcher<SignupMutation, SignupMutationVariables> = (
|
||||
export const handler: MutationHook<
|
||||
null,
|
||||
{},
|
||||
RegisterCustomerInput,
|
||||
RegisterCustomerInput
|
||||
> = {
|
||||
fetchOptions: {
|
||||
query: signupMutation,
|
||||
},
|
||||
async fetcher({
|
||||
input: { firstName, lastName, emailAddress, password },
|
||||
options,
|
||||
{ input },
|
||||
fetch
|
||||
) => {
|
||||
const { firstName, lastName, emailAddress, password } = input
|
||||
fetch,
|
||||
}) {
|
||||
if (!(firstName && lastName && emailAddress && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to signup',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
const variables: SignupMutationVariables = {
|
||||
input: {
|
||||
firstName,
|
||||
lastName,
|
||||
emailAddress,
|
||||
password,
|
||||
},
|
||||
}
|
||||
const { registerCustomerAccount } = await fetch<SignupMutation>({
|
||||
...options,
|
||||
query: signupMutation,
|
||||
variables: { input },
|
||||
variables,
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useSignup = () => {
|
||||
if (registerCustomerAccount.__typename !== 'Success') {
|
||||
throw new ValidationError(registerCustomerAccount)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
const fn = useCommerceSignup<SignupMutation, SignupMutationVariables>(
|
||||
{},
|
||||
customFetcher
|
||||
)
|
||||
|
||||
return useCallback(
|
||||
async function signup(input: SignupInput) {
|
||||
const { registerCustomerAccount } = await fn({
|
||||
input: {
|
||||
firstName: input.firstName,
|
||||
lastName: input.lastName,
|
||||
emailAddress: input.email,
|
||||
password: input.password,
|
||||
},
|
||||
})
|
||||
if (registerCustomerAccount.__typename !== 'Success') {
|
||||
throw new CommerceError({
|
||||
message: (registerCustomerAccount as ErrorResult).message,
|
||||
})
|
||||
}
|
||||
async function signup(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return { registerCustomerAccount }
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
[fetch, revalidate]
|
||||
)
|
||||
}
|
||||
|
||||
useSignup.extend = extendHook
|
||||
|
||||
return useSignup
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,15 +1,12 @@
|
||||
import { Cart, CartItemBody } from '@commerce/types'
|
||||
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import fetchGraphqlApi from '@framework/api/utils/fetch-graphql-api'
|
||||
import useCartAddItem from '@commerce/cart/use-add-item'
|
||||
import useCart from './use-cart'
|
||||
import { MutationHook } from '@commerce/utils/types'
|
||||
import { useCallback } from 'react'
|
||||
import useCart from './use-cart'
|
||||
import { cartFragment } from '../api/fragments/cart'
|
||||
import {
|
||||
AddItemToOrderMutation,
|
||||
AddItemToOrderMutationVariables,
|
||||
ErrorResult,
|
||||
} from '@framework/schema'
|
||||
import { AddItemToOrderMutation } from '../schema'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
|
||||
export const addItemToOrderMutation = /* GraphQL */ `
|
||||
mutation addItemToOrder($variantId: ID!, $quantity: Int!) {
|
||||
@ -25,56 +22,45 @@ export const addItemToOrderMutation = /* GraphQL */ `
|
||||
${cartFragment}
|
||||
`
|
||||
|
||||
export type AddItemInput = {
|
||||
productId?: number
|
||||
variantId: number
|
||||
quantity?: number
|
||||
}
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const fetcher: HookFetcher<
|
||||
AddItemToOrderMutation,
|
||||
AddItemToOrderMutationVariables
|
||||
> = (options, { variantId, quantity }, fetch) => {
|
||||
if (quantity && (!Number.isInteger(quantity) || quantity! < 1)) {
|
||||
export const handler: MutationHook<Cart, {}, CartItemBody> = {
|
||||
fetchOptions: {
|
||||
query: addItemToOrderMutation,
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
if (
|
||||
input.quantity &&
|
||||
(!Number.isInteger(input.quantity) || input.quantity! < 1)
|
||||
) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer greater than 0',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
const { addItemToOrder } = await fetch<AddItemToOrderMutation>({
|
||||
...options,
|
||||
query: addItemToOrderMutation,
|
||||
variables: { variantId, quantity: quantity || 1 },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useAddItem = () => {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartAddItem({}, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
const { addItemToOrder } = await fn({
|
||||
variables: {
|
||||
quantity: input.quantity || 1,
|
||||
variantId: input.variantId,
|
||||
})
|
||||
if (addItemToOrder.__typename === 'Order') {
|
||||
await mutate({ addItemToOrder }, false)
|
||||
} else {
|
||||
throw new CommerceError({
|
||||
message: (addItemToOrder as ErrorResult).message,
|
||||
})
|
||||
}
|
||||
return { addItemToOrder }
|
||||
},
|
||||
[fn, mutate]
|
||||
)
|
||||
})
|
||||
|
||||
if (addItemToOrder.__typename === 'Order') {
|
||||
return normalizeCart(addItemToOrder)
|
||||
}
|
||||
throw new CommerceError(addItemToOrder)
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCart()
|
||||
|
||||
useAddItem.extend = extendHook
|
||||
|
||||
return useAddItem
|
||||
return useCallback(
|
||||
async function addItem(input) {
|
||||
const data = await fetch({ input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import useData, { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useResponse from '@commerce/utils/use-response'
|
||||
import { Cart } from '@commerce/types'
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useCart, { FetchCartInput, UseCart } from '@commerce/cart/use-cart'
|
||||
import { cartFragment } from '../api/fragments/cart'
|
||||
import { CartFragment } from '../schema'
|
||||
import { normalizeCart } from '@framework/lib/normalize'
|
||||
import { ActiveOrderQuery, CartFragment } from '../schema'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export const getCartQuery = /* GraphQL */ `
|
||||
query activeOrder {
|
||||
@ -14,10 +15,6 @@ export const getCartQuery = /* GraphQL */ `
|
||||
${cartFragment}
|
||||
`
|
||||
|
||||
export const fetcher: HookFetcher<any, null> = (options, input, fetch) => {
|
||||
return fetch({ ...options, query: getCartQuery })
|
||||
}
|
||||
|
||||
export type CartResult = {
|
||||
activeOrder?: CartFragment
|
||||
addItemToOrder?: CartFragment
|
||||
@ -25,42 +22,37 @@ export type CartResult = {
|
||||
removeOrderLine?: CartFragment
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<any | null>
|
||||
) {
|
||||
const useCart = () => {
|
||||
const response = useData<CartResult>(
|
||||
{ query: getCartQuery },
|
||||
[],
|
||||
customFetcher,
|
||||
swrOptions
|
||||
)
|
||||
const res = useResponse(response, {
|
||||
normalizer: (data) => {
|
||||
const order =
|
||||
data?.activeOrder ||
|
||||
data?.addItemToOrder ||
|
||||
data?.adjustOrderLine ||
|
||||
data?.removeOrderLine
|
||||
return order ? normalizeCart(order) : null
|
||||
export default useCart as UseCart<typeof handler>
|
||||
|
||||
export const handler: SWRHook<
|
||||
Cart | null,
|
||||
{},
|
||||
FetchCartInput,
|
||||
{ isEmpty?: boolean }
|
||||
> = {
|
||||
fetchOptions: {
|
||||
query: getCartQuery,
|
||||
},
|
||||
descriptors: {
|
||||
async fetcher({ input: { cartId }, options, fetch }) {
|
||||
const { activeOrder } = await fetch<ActiveOrderQuery>(options)
|
||||
return activeOrder ? normalizeCart(activeOrder) : null
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
const response = useData({
|
||||
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||
})
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return response.data?.activeOrder?.totalQuantity === 0
|
||||
return (response.data?.lineItems.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
useCart.extend = extendHook
|
||||
|
||||
return useCart
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import useCartRemoveItem from '@commerce/cart/use-remove-item'
|
||||
import { HookFetcherContext, MutationHookContext } from '@commerce/utils/types'
|
||||
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCart from './use-cart'
|
||||
import { cartFragment } from '@framework/api/fragments/cart'
|
||||
import { cartFragment } from '../api/fragments/cart'
|
||||
import {
|
||||
ErrorResult,
|
||||
RemoveOrderLineMutation,
|
||||
RemoveOrderLineMutationVariables,
|
||||
} from '@framework/schema'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
} from '../schema'
|
||||
import { Cart, LineItem, RemoveCartItemBody } from '@commerce/types'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
|
||||
export const removeOrderLineMutation = /* GraphQL */ `
|
||||
mutation removeOrderLine($orderLineId: ID!) {
|
||||
@ -24,44 +25,38 @@ export const removeOrderLineMutation = /* GraphQL */ `
|
||||
${cartFragment}
|
||||
`
|
||||
|
||||
export const fetcher: HookFetcher<
|
||||
RemoveOrderLineMutation,
|
||||
RemoveOrderLineMutationVariables
|
||||
> = (options, { orderLineId }, fetch) => {
|
||||
return fetch({
|
||||
...options,
|
||||
query: removeOrderLineMutation,
|
||||
variables: { orderLineId },
|
||||
})
|
||||
}
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useRemoveItem = (item?: any) => {
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
query: removeOrderLineMutation,
|
||||
},
|
||||
async fetcher({ input, options, fetch }: HookFetcherContext<LineItem>) {
|
||||
const variables: RemoveOrderLineMutationVariables = {
|
||||
orderLineId: input.id,
|
||||
}
|
||||
const { removeOrderLine } = await fetch<RemoveOrderLineMutation>({
|
||||
...options,
|
||||
variables,
|
||||
})
|
||||
|
||||
if (removeOrderLine.__typename === 'Order') {
|
||||
return normalizeCart(removeOrderLine)
|
||||
}
|
||||
throw new CommerceError(removeOrderLine)
|
||||
},
|
||||
useHook: ({
|
||||
fetch,
|
||||
}: MutationHookContext<Cart | null, RemoveCartItemBody>) => (ctx = {}) => {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartRemoveItem<
|
||||
RemoveOrderLineMutation,
|
||||
RemoveOrderLineMutationVariables
|
||||
>({}, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function removeItem(input: any) {
|
||||
const { removeOrderLine } = await fn({ orderLineId: input.id })
|
||||
if (removeOrderLine.__typename === 'Order') {
|
||||
await mutate({ removeOrderLine }, false)
|
||||
} else {
|
||||
throw new CommerceError({
|
||||
message: (removeOrderLine as ErrorResult).message,
|
||||
})
|
||||
}
|
||||
return { removeOrderLine }
|
||||
async function removeItem(input) {
|
||||
const data = await fetch({ input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fn, mutate]
|
||||
[fetch, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useRemoveItem.extend = extendHook
|
||||
|
||||
return useRemoveItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,15 +1,20 @@
|
||||
import { useCallback } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import useCartUpdateItem from '@commerce/cart/use-update-item'
|
||||
import { HookFetcherContext, MutationHookContext } from '@commerce/utils/types'
|
||||
import { CommerceError, ValidationError } from '@commerce/utils/errors'
|
||||
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
|
||||
import {
|
||||
Cart,
|
||||
CartItemBody,
|
||||
LineItem,
|
||||
UpdateCartItemBody,
|
||||
} from '@commerce/types'
|
||||
import useCart from './use-cart'
|
||||
import { cartFragment } from '@framework/api/fragments/cart'
|
||||
import {
|
||||
AdjustOrderLineMutation,
|
||||
AdjustOrderLineMutationVariables,
|
||||
ErrorResult,
|
||||
} from '@framework/schema'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
} from '../schema'
|
||||
import { cartFragment } from '../api/fragments/cart'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
|
||||
export const adjustOrderLineMutation = /* GraphQL */ `
|
||||
mutation adjustOrderLine($orderLineId: ID!, $quantity: Int!) {
|
||||
@ -24,47 +29,64 @@ export const adjustOrderLineMutation = /* GraphQL */ `
|
||||
}
|
||||
${cartFragment}
|
||||
`
|
||||
export const fetcher: HookFetcher<
|
||||
AdjustOrderLineMutation,
|
||||
AdjustOrderLineMutationVariables
|
||||
> = (options, { orderLineId, quantity }, fetch) => {
|
||||
return fetch({
|
||||
...options,
|
||||
query: adjustOrderLineMutation,
|
||||
variables: { orderLineId, quantity },
|
||||
})
|
||||
}
|
||||
|
||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
||||
const useUpdateItem = (item?: any) => {
|
||||
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
query: adjustOrderLineMutation,
|
||||
},
|
||||
async fetcher(context: HookFetcherContext<UpdateCartItemBody<CartItemBody>>) {
|
||||
const { input, options, fetch } = context
|
||||
const variables: AdjustOrderLineMutationVariables = {
|
||||
quantity: input.item.quantity || 1,
|
||||
orderLineId: input.itemId,
|
||||
}
|
||||
const { adjustOrderLine } = await fetch<AdjustOrderLineMutation>({
|
||||
...options,
|
||||
variables,
|
||||
})
|
||||
|
||||
if (adjustOrderLine.__typename === 'Order') {
|
||||
return normalizeCart(adjustOrderLine)
|
||||
}
|
||||
throw new CommerceError(adjustOrderLine)
|
||||
},
|
||||
useHook: ({
|
||||
fetch,
|
||||
}: MutationHookContext<Cart | null, UpdateCartItemBody<CartItemBody>>) => (
|
||||
ctx: {
|
||||
item?: LineItem
|
||||
wait?: number
|
||||
} = {}
|
||||
) => {
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartUpdateItem<
|
||||
AdjustOrderLineMutation,
|
||||
AdjustOrderLineMutationVariables
|
||||
>({}, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
debounce(async (input: any) => {
|
||||
const { adjustOrderLine } = await fn({
|
||||
orderLineId: item.id,
|
||||
async function addItem(input: CartItemBody) {
|
||||
const itemId = 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,
|
||||
},
|
||||
})
|
||||
if (adjustOrderLine.__typename === 'Order') {
|
||||
await mutate({ adjustOrderLine }, false)
|
||||
} else {
|
||||
throw new CommerceError({
|
||||
message: (adjustOrderLine as ErrorResult).message,
|
||||
})
|
||||
}
|
||||
return { adjustOrderLine }
|
||||
}, cfg?.wait ?? 500),
|
||||
[fn, mutate]
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fetch, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useUpdateItem.extend = extendHook
|
||||
|
||||
return useUpdateItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -14,7 +14,7 @@
|
||||
"plugins": ["typescript", "typescript-operations"],
|
||||
"config": {
|
||||
"scalars": {
|
||||
"ID": "number"
|
||||
"ID": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
6
framework/vendure/commerce.config.json
Normal file
6
framework/vendure/commerce.config.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"provider": "vendure",
|
||||
"features": {
|
||||
"wishlist": false
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { VendureConfig, getConfig } from '../api'
|
||||
import { GetCollectionsQuery } from '@framework/schema'
|
||||
import { arrayToTree } from '@framework/lib/array-to-tree'
|
||||
import { GetCollectionsQuery } from '../schema'
|
||||
import { arrayToTree } from '../lib/array-to-tree'
|
||||
|
||||
export const getCollectionsQuery = /* GraphQL */ `
|
||||
query getCollections {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceCustomer from '@commerce/use-customer'
|
||||
import { ActiveCustomerQuery } from '@framework/schema'
|
||||
import useResponse from '@commerce/utils/use-response'
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
||||
import { Customer } from '@commerce/types'
|
||||
import { ActiveCustomerQuery } from '../schema'
|
||||
|
||||
export const activeCustomerQuery = /* GraphQL */ `
|
||||
query activeCustomer {
|
||||
@ -15,40 +14,30 @@ export const activeCustomerQuery = /* GraphQL */ `
|
||||
}
|
||||
`
|
||||
|
||||
export const fetcher: HookFetcher<ActiveCustomerQuery> = async (
|
||||
options,
|
||||
_,
|
||||
fetch
|
||||
) => {
|
||||
return await fetch<ActiveCustomerQuery>({ query: activeCustomerQuery })
|
||||
}
|
||||
export default useCustomer as UseCustomer<typeof handler>
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<ActiveCustomerQuery>
|
||||
) {
|
||||
const useCustomer = () => {
|
||||
const response = useCommerceCustomer({}, [], customFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
export const handler: SWRHook<Customer | null> = {
|
||||
fetchOptions: {
|
||||
query: activeCustomerQuery,
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
const { activeCustomer } = await fetch<ActiveCustomerQuery>({
|
||||
...options,
|
||||
})
|
||||
|
||||
return useResponse(response, {
|
||||
normalizer: (data) => {
|
||||
return data?.activeCustomer
|
||||
? {
|
||||
firstName: data?.activeCustomer?.firstName ?? '',
|
||||
lastName: data?.activeCustomer?.lastName ?? '',
|
||||
email: data?.activeCustomer?.emailAddress ?? '',
|
||||
}
|
||||
return activeCustomer
|
||||
? ({
|
||||
firstName: activeCustomer.firstName ?? '',
|
||||
lastName: activeCustomer.lastName ?? '',
|
||||
email: activeCustomer.emailAddress ?? '',
|
||||
} as any)
|
||||
: null
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
return useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input?.swrOptions,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
useCustomer.extend = extendHook
|
||||
|
||||
return useCustomer
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
49
framework/vendure/fetcher.ts
Normal file
49
framework/vendure/fetcher.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Fetcher } from '@commerce/utils/types'
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
|
||||
async function getText(res: Response) {
|
||||
try {
|
||||
return (await res.text()) || res.statusText
|
||||
} catch (error) {
|
||||
return res.statusText
|
||||
}
|
||||
}
|
||||
|
||||
async function getError(res: Response) {
|
||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
||||
const data = await res.json()
|
||||
return new FetcherError({ errors: data.errors, status: res.status })
|
||||
}
|
||||
return new FetcherError({ message: await getText(res), status: res.status })
|
||||
}
|
||||
|
||||
export const fetcher: Fetcher = async ({
|
||||
url,
|
||||
method = 'POST',
|
||||
variables,
|
||||
query,
|
||||
body: bodyObj,
|
||||
}) => {
|
||||
const shopApiUrl = process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL
|
||||
if (!shopApiUrl) {
|
||||
throw new Error(
|
||||
'The Vendure Shop API url has not been provided. Please define NEXT_PUBLIC_VENDURE_SHOP_API_URL in .env.local'
|
||||
)
|
||||
}
|
||||
const hasBody = Boolean(variables || query)
|
||||
const body = hasBody ? JSON.stringify({ query, variables }) : undefined
|
||||
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
||||
const res = await fetch(shopApiUrl, {
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const { data } = await res.json()
|
||||
return data
|
||||
}
|
||||
|
||||
throw await getError(res)
|
||||
}
|
@ -1,51 +1,15 @@
|
||||
import * as React from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
import { CommerceConfig, CommerceProvider as CoreCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
|
||||
async function getText(res: Response) {
|
||||
try {
|
||||
return (await res.text()) || res.statusText
|
||||
} catch (error) {
|
||||
return res.statusText
|
||||
}
|
||||
}
|
||||
|
||||
async function getError(res: Response) {
|
||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
||||
const data = await res.json()
|
||||
return new FetcherError({ errors: data.errors, status: res.status })
|
||||
}
|
||||
return new FetcherError({ message: await getText(res), status: res.status })
|
||||
}
|
||||
import {
|
||||
CommerceConfig,
|
||||
CommerceProvider as CoreCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@commerce'
|
||||
import { vendureProvider } from './provider'
|
||||
|
||||
export const vendureConfig: CommerceConfig = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'bc_cartId',
|
||||
async fetcher({ url, method = 'POST', variables, query, body: bodyObj }) {
|
||||
const shopApiUrl = process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL
|
||||
if (!shopApiUrl) {
|
||||
throw new Error('The Vendure Shop API url has not been provided. Please define NEXT_PUBLIC_VENDURE_SHOP_API_URL in .env.local')
|
||||
}
|
||||
const hasBody = Boolean(variables || query)
|
||||
const body = hasBody
|
||||
? JSON.stringify({ query, variables })
|
||||
: undefined
|
||||
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
||||
const res = await fetch(shopApiUrl, {
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
credentials: 'include'
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const { data } = await res.json()
|
||||
return data
|
||||
}
|
||||
|
||||
throw await getError(res)
|
||||
},
|
||||
cartCookie: 'session',
|
||||
}
|
||||
|
||||
export type VendureConfig = Partial<CommerceConfig>
|
||||
@ -57,7 +21,10 @@ export type VendureProps = {
|
||||
|
||||
export function CommerceProvider({ children, ...config }: VendureProps) {
|
||||
return (
|
||||
<CoreCommerceProvider config={{ ...vendureConfig, ...config }}>
|
||||
<CoreCommerceProvider
|
||||
provider={vendureProvider}
|
||||
config={{ ...vendureConfig, ...config }}
|
||||
>
|
||||
{children}
|
||||
</CoreCommerceProvider>
|
||||
)
|
||||
|
@ -1,10 +1,10 @@
|
||||
export type HasParent = { id: number; parent?: { id: number } | null }
|
||||
export type HasParent = { id: string; parent?: { id: string } | null }
|
||||
export type TreeNode<T extends HasParent> = T & {
|
||||
children: Array<TreeNode<T>>
|
||||
expanded: boolean
|
||||
}
|
||||
export type RootNode<T extends HasParent> = {
|
||||
id?: number
|
||||
id?: string
|
||||
children: Array<TreeNode<T>>
|
||||
}
|
||||
|
||||
@ -54,8 +54,8 @@ export function arrayToTree<T extends HasParent>(
|
||||
*/
|
||||
function treeToMap<T extends HasParent>(
|
||||
tree?: RootNode<T>
|
||||
): Map<number, TreeNode<T>> {
|
||||
const nodeMap = new Map<number, TreeNode<T>>()
|
||||
): Map<string, TreeNode<T>> {
|
||||
const nodeMap = new Map<string, TreeNode<T>>()
|
||||
function visit(node: TreeNode<T>) {
|
||||
nodeMap.set(node.id, node)
|
||||
node.children.forEach(visit)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import update from '@framework/lib/immutability'
|
||||
import { CartFragment, SearchResultFragment } from '@framework/schema'
|
||||
import { Cart, Product } from '@commerce/types'
|
||||
import update from '../lib/immutability'
|
||||
import { CartFragment, SearchResultFragment } from '../schema'
|
||||
|
||||
function normalizeProductOption(productOption: any) {
|
||||
const {
|
||||
@ -89,11 +90,14 @@ export function normalizeSearchResult(item: SearchResultFragment): Product {
|
||||
export function normalizeCart(order: CartFragment): Cart {
|
||||
return {
|
||||
id: order.id.toString(),
|
||||
createdAt: order.createdAt,
|
||||
taxesIncluded: true,
|
||||
lineItemsSubtotalPrice: order.subTotalWithTax / 100,
|
||||
currency: { code: order.currencyCode },
|
||||
subTotal: order.subTotalWithTax / 100,
|
||||
total: order.totalWithTax / 100,
|
||||
customerId: order.customer?.id as number,
|
||||
items: order.lines?.map((l) => ({
|
||||
subtotalPrice: order.subTotalWithTax / 100,
|
||||
totalPrice: order.totalWithTax / 100,
|
||||
customerId: order.customer?.id,
|
||||
lineItems: order.lines?.map((l) => ({
|
||||
id: l.id,
|
||||
name: l.productVariant.name,
|
||||
quantity: l.quantity,
|
||||
@ -101,7 +105,19 @@ export function normalizeCart(order: CartFragment): Cart {
|
||||
variantId: l.productVariant.id,
|
||||
productId: l.productVariant.productId,
|
||||
images: [{ url: l.featuredAsset?.preview + '?preset=thumb' || '' }],
|
||||
prices: [],
|
||||
discounts: l.discounts.map((d) => ({ value: d.amount / 100 })),
|
||||
path: '',
|
||||
variant: {
|
||||
id: l.productVariant.id,
|
||||
name: l.productVariant.name,
|
||||
sku: l.productVariant.sku,
|
||||
price: l.discountedLinePriceWithTax / 100,
|
||||
listPrice: l.linePriceWithTax / 100,
|
||||
image: {
|
||||
url: l.featuredAsset?.preview + '?preset=thumb' || '',
|
||||
},
|
||||
requiresShipping: true,
|
||||
},
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Product } from '@commerce/types'
|
||||
import { getConfig, VendureConfig } from '../api'
|
||||
import { searchResultFragment } from '@framework/api/fragments/search-result'
|
||||
import { GetAllProductsQuery } from '@framework/schema'
|
||||
import { normalizeSearchResult } from '@framework/lib/normalize'
|
||||
import { searchResultFragment } from '../api/fragments/search-result'
|
||||
import { GetAllProductsQuery } from '../schema'
|
||||
import { normalizeSearchResult } from '../lib/normalize'
|
||||
|
||||
export const getAllProductsQuery = /* GraphQL */ `
|
||||
query getAllProducts($input: SearchInput!) {
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { Product } from '@commerce/types'
|
||||
import { getConfig, VendureConfig } from '../api'
|
||||
import { GetProductQuery } from '@framework/schema'
|
||||
|
||||
export const getProductQuery = /* GraphQL */ `
|
||||
query getProduct($slug: String!) {
|
||||
@ -21,12 +23,20 @@ export const getProductQuery = /* GraphQL */ `
|
||||
name
|
||||
code
|
||||
groupId
|
||||
group {
|
||||
id
|
||||
options {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
optionGroups {
|
||||
id
|
||||
code
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
@ -47,7 +57,7 @@ async function getProduct({
|
||||
config = getConfig(config)
|
||||
|
||||
const locale = config.locale
|
||||
const { data } = await config.fetch(query, { variables })
|
||||
const { data } = await config.fetch<GetProductQuery>(query, { variables })
|
||||
const product = data.product
|
||||
|
||||
if (product) {
|
||||
@ -57,26 +67,28 @@ async function getProduct({
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
slug: product.slug,
|
||||
images: product.assets.map((a: any) => ({
|
||||
images: product.assets.map((a) => ({
|
||||
url: a.preview,
|
||||
alt: a.name,
|
||||
})),
|
||||
variants: product.variants.map((v: any) => ({
|
||||
variants: product.variants.map((v) => ({
|
||||
id: v.id,
|
||||
options: v.options.map((o: any) => ({
|
||||
options: v.options.map((o) => ({
|
||||
id: o.id,
|
||||
displayName: o.name,
|
||||
values: [],
|
||||
values: o.group.options.map((_o) => ({ label: _o.name })),
|
||||
})),
|
||||
})),
|
||||
price: {
|
||||
value: product.variants[0].priceWithTax / 100,
|
||||
currencyCode: product.variants[0].currencyCode,
|
||||
},
|
||||
options: product.optionGroups.map((og: any) => ({
|
||||
options: product.optionGroups.map((og) => ({
|
||||
id: og.id,
|
||||
displayName: og.name,
|
||||
values: og.options.map((o: any) => ({ label: o.name })),
|
||||
values: og.options.map((o) => ({ label: o.name })),
|
||||
})),
|
||||
},
|
||||
} as Product,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
export * from '@commerce/use-price'
|
||||
export { default } from '@commerce/use-price'
|
||||
export * from '@commerce/product/use-price'
|
||||
export { default } from '@commerce/product/use-price'
|
||||
|
@ -1,10 +1,9 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceSearch from '@commerce/products/use-search'
|
||||
import useResponse from '@commerce/utils/use-response'
|
||||
import { searchResultFragment } from '@framework/api/fragments/search-result'
|
||||
import { SearchQuery } from '@framework/schema'
|
||||
import { normalizeSearchResult } from '@framework/lib/normalize'
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useSearch, { UseSearch } from '@commerce/product/use-search'
|
||||
import { Product } from '@commerce/types'
|
||||
import { SearchQuery, SearchQueryVariables } from '../schema'
|
||||
import { searchResultFragment } from '../api/fragments/search-result'
|
||||
import { normalizeSearchResult } from '../lib/normalize'
|
||||
|
||||
export const searchQuery = /* GraphQL */ `
|
||||
query search($input: SearchInput!) {
|
||||
@ -18,61 +17,61 @@ export const searchQuery = /* GraphQL */ `
|
||||
${searchResultFragment}
|
||||
`
|
||||
|
||||
export default useSearch as UseSearch<typeof handler>
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: number
|
||||
brandId?: number
|
||||
categoryId?: string
|
||||
brandId?: string
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<SearchQuery, SearchProductsInput> = (
|
||||
options,
|
||||
{ search, categoryId, brandId, sort },
|
||||
fetch
|
||||
) => {
|
||||
return fetch({
|
||||
query: searchQuery,
|
||||
variables: {
|
||||
input: {
|
||||
term: search,
|
||||
collectionId: categoryId,
|
||||
groupByProduct: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
export type SearchProductsData = {
|
||||
products: Product[]
|
||||
found: boolean
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<any, SearchProductsInput>
|
||||
) {
|
||||
const useSearch = (input: SearchProductsInput = {}) => {
|
||||
const response = useCommerceSearch<SearchQuery, SearchProductsInput>(
|
||||
{},
|
||||
[
|
||||
export const handler: SWRHook<
|
||||
SearchProductsData,
|
||||
SearchProductsInput,
|
||||
SearchProductsInput
|
||||
> = {
|
||||
fetchOptions: {
|
||||
query: searchQuery,
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
const { categoryId, brandId } = input
|
||||
|
||||
const variables: SearchQueryVariables = {
|
||||
input: {
|
||||
term: input.search,
|
||||
collectionId: input.categoryId,
|
||||
groupByProduct: true,
|
||||
// TODO: what is the "sort" value?
|
||||
},
|
||||
}
|
||||
const { search } = await fetch<SearchQuery>({
|
||||
query: searchQuery,
|
||||
variables,
|
||||
})
|
||||
|
||||
return {
|
||||
found: search.totalItems > 0,
|
||||
products: search.items.map((item) => normalizeSearchResult(item)) ?? [],
|
||||
}
|
||||
},
|
||||
useHook: ({ useData }) => (input = {}) => {
|
||||
return useData({
|
||||
input: [
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
customFetcher,
|
||||
{ revalidateOnFocus: false, ...swrOptions }
|
||||
)
|
||||
|
||||
return useResponse(response, {
|
||||
normalizer: (data) => {
|
||||
return {
|
||||
found: data?.search.totalItems && data?.search.totalItems > 0,
|
||||
products:
|
||||
data?.search.items.map((item) => normalizeSearchResult(item)) ?? [],
|
||||
}
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
useSearch.extend = extendHook
|
||||
|
||||
return useSearch
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
21
framework/vendure/provider.ts
Normal file
21
framework/vendure/provider.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Provider } from '@commerce'
|
||||
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 vendureProvider: Provider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: '',
|
||||
fetcher,
|
||||
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||
customer: { useCustomer },
|
||||
products: { useSearch },
|
||||
auth: { useLogin, useLogout, useSignup },
|
||||
}
|
119
framework/vendure/schema.d.ts
vendored
119
framework/vendure/schema.d.ts
vendored
@ -8,7 +8,7 @@ export type MakeMaybe<T, K extends keyof T> = Omit<T, K> &
|
||||
{ [SubKey in K]: Maybe<T[SubKey]> }
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: number
|
||||
ID: string
|
||||
String: string
|
||||
Boolean: boolean
|
||||
Int: number
|
||||
@ -41,6 +41,8 @@ export type Query = {
|
||||
collection?: Maybe<Collection>
|
||||
/** Returns a list of eligible shipping methods based on the current active Order */
|
||||
eligibleShippingMethods: Array<ShippingMethodQuote>
|
||||
/** Returns a list of payment methods and their eligibility based on the current active Order */
|
||||
eligiblePaymentMethods: Array<PaymentMethodQuote>
|
||||
/** Returns information about the current authenticated User */
|
||||
me?: Maybe<CurrentUser>
|
||||
/** Returns the possible next states that the activeOrder can transition to */
|
||||
@ -329,6 +331,7 @@ export type Asset = Node & {
|
||||
source: Scalars['String']
|
||||
preview: Scalars['String']
|
||||
focalPoint?: Maybe<Coordinate>
|
||||
customFields?: Maybe<Scalars['JSON']>
|
||||
}
|
||||
|
||||
export type Coordinate = {
|
||||
@ -376,6 +379,7 @@ export type Channel = Node & {
|
||||
defaultLanguageCode: LanguageCode
|
||||
currencyCode: CurrencyCode
|
||||
pricesIncludeTax: Scalars['Boolean']
|
||||
customFields?: Maybe<Scalars['JSON']>
|
||||
}
|
||||
|
||||
export type Collection = Node & {
|
||||
@ -534,6 +538,7 @@ export enum ErrorCode {
|
||||
OrderModificationError = 'ORDER_MODIFICATION_ERROR',
|
||||
IneligibleShippingMethodError = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
|
||||
OrderPaymentStateError = 'ORDER_PAYMENT_STATE_ERROR',
|
||||
IneligiblePaymentMethodError = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
|
||||
PaymentFailedError = 'PAYMENT_FAILED_ERROR',
|
||||
PaymentDeclinedError = 'PAYMENT_DECLINED_ERROR',
|
||||
CouponCodeInvalidError = 'COUPON_CODE_INVALID_ERROR',
|
||||
@ -652,6 +657,8 @@ export type ConfigArgDefinition = {
|
||||
name: Scalars['String']
|
||||
type: Scalars['String']
|
||||
list: Scalars['Boolean']
|
||||
required: Scalars['Boolean']
|
||||
defaultValue?: Maybe<Scalars['JSON']>
|
||||
label?: Maybe<Scalars['String']>
|
||||
description?: Maybe<Scalars['String']>
|
||||
ui?: Maybe<Scalars['JSON']>
|
||||
@ -678,6 +685,7 @@ export type DeletionResponse = {
|
||||
|
||||
export type ConfigArgInput = {
|
||||
name: Scalars['String']
|
||||
/** A JSON stringified representation of the actual value */
|
||||
value: Scalars['String']
|
||||
}
|
||||
|
||||
@ -789,6 +797,25 @@ export type Success = {
|
||||
success: Scalars['Boolean']
|
||||
}
|
||||
|
||||
export type ShippingMethodQuote = {
|
||||
__typename?: 'ShippingMethodQuote'
|
||||
id: Scalars['ID']
|
||||
price: Scalars['Int']
|
||||
priceWithTax: Scalars['Int']
|
||||
name: Scalars['String']
|
||||
description: Scalars['String']
|
||||
/** Any optional metadata returned by the ShippingCalculator in the ShippingCalculationResult */
|
||||
metadata?: Maybe<Scalars['JSON']>
|
||||
}
|
||||
|
||||
export type PaymentMethodQuote = {
|
||||
__typename?: 'PaymentMethodQuote'
|
||||
id: Scalars['ID']
|
||||
code: Scalars['String']
|
||||
isEligible: Scalars['Boolean']
|
||||
eligibilityMessage?: Maybe<Scalars['String']>
|
||||
}
|
||||
|
||||
export type Country = Node & {
|
||||
__typename?: 'Country'
|
||||
id: Scalars['ID']
|
||||
@ -1827,16 +1854,6 @@ export type OrderList = PaginatedList & {
|
||||
totalItems: Scalars['Int']
|
||||
}
|
||||
|
||||
export type ShippingMethodQuote = {
|
||||
__typename?: 'ShippingMethodQuote'
|
||||
id: Scalars['ID']
|
||||
price: Scalars['Int']
|
||||
priceWithTax: Scalars['Int']
|
||||
name: Scalars['String']
|
||||
description: Scalars['String']
|
||||
metadata?: Maybe<Scalars['JSON']>
|
||||
}
|
||||
|
||||
export type ShippingLine = {
|
||||
__typename?: 'ShippingLine'
|
||||
shippingMethod: ShippingMethod
|
||||
@ -1897,6 +1914,10 @@ export type OrderLine = Node & {
|
||||
unitPrice: Scalars['Int']
|
||||
/** The price of a single unit, including tax but excluding discounts */
|
||||
unitPriceWithTax: Scalars['Int']
|
||||
/** Non-zero if the unitPrice has changed since it was initially added to Order */
|
||||
unitPriceChangeSinceAdded: Scalars['Int']
|
||||
/** Non-zero if the unitPriceWithTax has changed since it was initially added to Order */
|
||||
unitPriceWithTaxChangeSinceAdded: Scalars['Int']
|
||||
/**
|
||||
* The price of a single unit including discounts, excluding tax.
|
||||
*
|
||||
@ -2173,6 +2194,7 @@ export type ProductVariant = Node & {
|
||||
/** @deprecated price now always excludes tax */
|
||||
priceIncludesTax: Scalars['Boolean']
|
||||
priceWithTax: Scalars['Int']
|
||||
stockLevel: Scalars['String']
|
||||
taxRateApplied: TaxRate
|
||||
taxCategory: TaxCategory
|
||||
options: Array<ProductOption>
|
||||
@ -2279,6 +2301,7 @@ export type TaxCategory = Node & {
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
name: Scalars['String']
|
||||
isDefault: Scalars['Boolean']
|
||||
}
|
||||
|
||||
export type TaxRate = Node & {
|
||||
@ -2337,7 +2360,7 @@ export type OrderModificationError = ErrorResult & {
|
||||
message: Scalars['String']
|
||||
}
|
||||
|
||||
/** Returned when attempting to set a ShippingMethod for which the order is not eligible */
|
||||
/** Returned when attempting to set a ShippingMethod for which the Order is not eligible */
|
||||
export type IneligibleShippingMethodError = ErrorResult & {
|
||||
__typename?: 'IneligibleShippingMethodError'
|
||||
errorCode: ErrorCode
|
||||
@ -2351,6 +2374,14 @@ export type OrderPaymentStateError = ErrorResult & {
|
||||
message: Scalars['String']
|
||||
}
|
||||
|
||||
/** Returned when attempting to add a Payment using a PaymentMethod for which the Order is not eligible. */
|
||||
export type IneligiblePaymentMethodError = ErrorResult & {
|
||||
__typename?: 'IneligiblePaymentMethodError'
|
||||
errorCode: ErrorCode
|
||||
message: Scalars['String']
|
||||
eligibilityCheckerMessage?: Maybe<Scalars['String']>
|
||||
}
|
||||
|
||||
/** Returned when a Payment fails due to an error. */
|
||||
export type PaymentFailedError = ErrorResult & {
|
||||
__typename?: 'PaymentFailedError'
|
||||
@ -2546,6 +2577,7 @@ export type ApplyCouponCodeResult =
|
||||
export type AddPaymentToOrderResult =
|
||||
| Order
|
||||
| OrderPaymentStateError
|
||||
| IneligiblePaymentMethodError
|
||||
| PaymentFailedError
|
||||
| PaymentDeclinedError
|
||||
| OrderStateTransitionError
|
||||
@ -2704,6 +2736,7 @@ export type ProductVariantFilterParameter = {
|
||||
currencyCode?: Maybe<StringOperators>
|
||||
priceIncludesTax?: Maybe<BooleanOperators>
|
||||
priceWithTax?: Maybe<NumberOperators>
|
||||
stockLevel?: Maybe<StringOperators>
|
||||
}
|
||||
|
||||
export type ProductVariantSortParameter = {
|
||||
@ -2715,6 +2748,7 @@ export type ProductVariantSortParameter = {
|
||||
name?: Maybe<SortOrder>
|
||||
price?: Maybe<SortOrder>
|
||||
priceWithTax?: Maybe<SortOrder>
|
||||
stockLevel?: Maybe<SortOrder>
|
||||
}
|
||||
|
||||
export type CustomerFilterParameter = {
|
||||
@ -2800,6 +2834,7 @@ export type CartFragment = { __typename?: 'Order' } & Pick<
|
||||
Order,
|
||||
| 'id'
|
||||
| 'code'
|
||||
| 'createdAt'
|
||||
| 'totalQuantity'
|
||||
| 'subTotal'
|
||||
| 'subTotalWithTax'
|
||||
@ -2809,13 +2844,28 @@ export type CartFragment = { __typename?: 'Order' } & Pick<
|
||||
> & {
|
||||
customer?: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'id'>>
|
||||
lines: Array<
|
||||
{ __typename?: 'OrderLine' } & Pick<OrderLine, 'id' | 'quantity'> & {
|
||||
{ __typename?: 'OrderLine' } & Pick<
|
||||
OrderLine,
|
||||
'id' | 'quantity' | 'linePriceWithTax' | 'discountedLinePriceWithTax'
|
||||
> & {
|
||||
featuredAsset?: Maybe<
|
||||
{ __typename?: 'Asset' } & Pick<Asset, 'id' | 'preview'>
|
||||
>
|
||||
discounts: Array<
|
||||
{ __typename?: 'Adjustment' } & Pick<
|
||||
Adjustment,
|
||||
'description' | 'amount'
|
||||
>
|
||||
>
|
||||
productVariant: { __typename?: 'ProductVariant' } & Pick<
|
||||
ProductVariant,
|
||||
'id' | 'name' | 'productId'
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'sku'
|
||||
| 'price'
|
||||
| 'priceWithTax'
|
||||
| 'stockLevel'
|
||||
| 'productId'
|
||||
> & { product: { __typename?: 'Product' } & Pick<Product, 'slug'> }
|
||||
}
|
||||
>
|
||||
@ -2836,28 +2886,6 @@ export type SearchResultFragment = { __typename?: 'SearchResult' } & Pick<
|
||||
| ({ __typename?: 'SinglePrice' } & Pick<SinglePrice, 'value'>)
|
||||
}
|
||||
|
||||
export type LoginServerMutationVariables = Exact<{
|
||||
email: Scalars['String']
|
||||
password: Scalars['String']
|
||||
}>
|
||||
|
||||
export type LoginServerMutation = { __typename?: 'Mutation' } & {
|
||||
login:
|
||||
| ({ __typename: 'CurrentUser' } & Pick<CurrentUser, 'id'>)
|
||||
| ({ __typename: 'InvalidCredentialsError' } & Pick<
|
||||
InvalidCredentialsError,
|
||||
'errorCode' | 'message'
|
||||
>)
|
||||
| ({ __typename: 'NotVerifiedError' } & Pick<
|
||||
NotVerifiedError,
|
||||
'errorCode' | 'message'
|
||||
>)
|
||||
| ({ __typename: 'NativeAuthStrategyError' } & Pick<
|
||||
NativeAuthStrategyError,
|
||||
'errorCode' | 'message'
|
||||
>)
|
||||
}
|
||||
|
||||
export type LoginMutationVariables = Exact<{
|
||||
username: Scalars['String']
|
||||
password: Scalars['String']
|
||||
@ -3049,17 +3077,32 @@ export type GetProductQuery = { __typename?: 'Query' } & {
|
||||
{ __typename?: 'ProductOption' } & Pick<
|
||||
ProductOption,
|
||||
'id' | 'name' | 'code' | 'groupId'
|
||||
> & {
|
||||
group: { __typename?: 'ProductOptionGroup' } & Pick<
|
||||
ProductOptionGroup,
|
||||
'id'
|
||||
> & {
|
||||
options: Array<
|
||||
{ __typename?: 'ProductOption' } & Pick<
|
||||
ProductOption,
|
||||
'name'
|
||||
>
|
||||
>
|
||||
}
|
||||
}
|
||||
>
|
||||
}
|
||||
>
|
||||
optionGroups: Array<
|
||||
{ __typename?: 'ProductOptionGroup' } & Pick<
|
||||
ProductOptionGroup,
|
||||
'code' | 'name'
|
||||
'id' | 'code' | 'name'
|
||||
> & {
|
||||
options: Array<
|
||||
{ __typename?: 'ProductOption' } & Pick<ProductOption, 'name'>
|
||||
{ __typename?: 'ProductOption' } & Pick<
|
||||
ProductOption,
|
||||
'id' | 'name'
|
||||
>
|
||||
>
|
||||
}
|
||||
>
|
||||
|
@ -36,6 +36,11 @@ type Query {
|
||||
"""
|
||||
eligibleShippingMethods: [ShippingMethodQuote!]!
|
||||
|
||||
"""
|
||||
Returns a list of payment methods and their eligibility based on the current active Order
|
||||
"""
|
||||
eligiblePaymentMethods: [PaymentMethodQuote!]!
|
||||
|
||||
"""
|
||||
Returns information about the current authenticated User
|
||||
"""
|
||||
@ -289,6 +294,7 @@ type Asset implements Node {
|
||||
source: String!
|
||||
preview: String!
|
||||
focalPoint: Coordinate
|
||||
customFields: JSON
|
||||
}
|
||||
|
||||
type Coordinate {
|
||||
@ -331,6 +337,7 @@ type Channel implements Node {
|
||||
defaultLanguageCode: LanguageCode!
|
||||
currencyCode: CurrencyCode!
|
||||
pricesIncludeTax: Boolean!
|
||||
customFields: JSON
|
||||
}
|
||||
|
||||
type Collection implements Node {
|
||||
@ -568,6 +575,7 @@ enum ErrorCode {
|
||||
ORDER_MODIFICATION_ERROR
|
||||
INELIGIBLE_SHIPPING_METHOD_ERROR
|
||||
ORDER_PAYMENT_STATE_ERROR
|
||||
INELIGIBLE_PAYMENT_METHOD_ERROR
|
||||
PAYMENT_FAILED_ERROR
|
||||
PAYMENT_DECLINED_ERROR
|
||||
COUPON_CODE_INVALID_ERROR
|
||||
@ -704,6 +712,8 @@ type ConfigArgDefinition {
|
||||
name: String!
|
||||
type: String!
|
||||
list: Boolean!
|
||||
required: Boolean!
|
||||
defaultValue: JSON
|
||||
label: String
|
||||
description: String
|
||||
ui: JSON
|
||||
@ -727,6 +737,10 @@ type DeletionResponse {
|
||||
|
||||
input ConfigArgInput {
|
||||
name: String!
|
||||
|
||||
"""
|
||||
A JSON stringified representation of the actual value
|
||||
"""
|
||||
value: String!
|
||||
}
|
||||
|
||||
@ -839,6 +853,26 @@ type Success {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type ShippingMethodQuote {
|
||||
id: ID!
|
||||
price: Int!
|
||||
priceWithTax: Int!
|
||||
name: String!
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Any optional metadata returned by the ShippingCalculator in the ShippingCalculationResult
|
||||
"""
|
||||
metadata: JSON
|
||||
}
|
||||
|
||||
type PaymentMethodQuote {
|
||||
id: ID!
|
||||
code: String!
|
||||
isEligible: Boolean!
|
||||
eligibilityMessage: String
|
||||
}
|
||||
|
||||
type Country implements Node {
|
||||
id: ID!
|
||||
createdAt: DateTime!
|
||||
@ -2817,15 +2851,6 @@ type OrderList implements PaginatedList {
|
||||
totalItems: Int!
|
||||
}
|
||||
|
||||
type ShippingMethodQuote {
|
||||
id: ID!
|
||||
price: Int!
|
||||
priceWithTax: Int!
|
||||
name: String!
|
||||
description: String!
|
||||
metadata: JSON
|
||||
}
|
||||
|
||||
type ShippingLine {
|
||||
shippingMethod: ShippingMethod!
|
||||
price: Int!
|
||||
@ -2904,6 +2929,16 @@ type OrderLine implements Node {
|
||||
"""
|
||||
unitPriceWithTax: Int!
|
||||
|
||||
"""
|
||||
Non-zero if the unitPrice has changed since it was initially added to Order
|
||||
"""
|
||||
unitPriceChangeSinceAdded: Int!
|
||||
|
||||
"""
|
||||
Non-zero if the unitPriceWithTax has changed since it was initially added to Order
|
||||
"""
|
||||
unitPriceWithTaxChangeSinceAdded: Int!
|
||||
|
||||
"""
|
||||
The price of a single unit including discounts, excluding tax.
|
||||
|
||||
@ -3197,6 +3232,7 @@ type ProductVariant implements Node {
|
||||
priceIncludesTax: Boolean!
|
||||
@deprecated(reason: "price now always excludes tax")
|
||||
priceWithTax: Int!
|
||||
stockLevel: String!
|
||||
taxRateApplied: TaxRate!
|
||||
taxCategory: TaxCategory!
|
||||
options: [ProductOption!]!
|
||||
@ -3292,6 +3328,7 @@ type TaxCategory implements Node {
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
name: String!
|
||||
isDefault: Boolean!
|
||||
}
|
||||
|
||||
type TaxRate implements Node {
|
||||
@ -3347,7 +3384,7 @@ type OrderModificationError implements ErrorResult {
|
||||
}
|
||||
|
||||
"""
|
||||
Returned when attempting to set a ShippingMethod for which the order is not eligible
|
||||
Returned when attempting to set a ShippingMethod for which the Order is not eligible
|
||||
"""
|
||||
type IneligibleShippingMethodError implements ErrorResult {
|
||||
errorCode: ErrorCode!
|
||||
@ -3362,6 +3399,15 @@ type OrderPaymentStateError implements ErrorResult {
|
||||
message: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Returned when attempting to add a Payment using a PaymentMethod for which the Order is not eligible.
|
||||
"""
|
||||
type IneligiblePaymentMethodError implements ErrorResult {
|
||||
errorCode: ErrorCode!
|
||||
message: String!
|
||||
eligibilityCheckerMessage: String
|
||||
}
|
||||
|
||||
"""
|
||||
Returned when a Payment fails due to an error.
|
||||
"""
|
||||
@ -3562,6 +3608,7 @@ union ApplyCouponCodeResult =
|
||||
union AddPaymentToOrderResult =
|
||||
Order
|
||||
| OrderPaymentStateError
|
||||
| IneligiblePaymentMethodError
|
||||
| PaymentFailedError
|
||||
| PaymentDeclinedError
|
||||
| OrderStateTransitionError
|
||||
@ -3718,6 +3765,7 @@ input ProductVariantFilterParameter {
|
||||
currencyCode: StringOperators
|
||||
priceIncludesTax: BooleanOperators
|
||||
priceWithTax: NumberOperators
|
||||
stockLevel: StringOperators
|
||||
}
|
||||
|
||||
input ProductVariantSortParameter {
|
||||
@ -3729,6 +3777,7 @@ input ProductVariantSortParameter {
|
||||
name: SortOrder
|
||||
price: SortOrder
|
||||
priceWithTax: SortOrder
|
||||
stockLevel: SortOrder
|
||||
}
|
||||
|
||||
input CustomerFilterParameter {
|
||||
|
@ -1,4 +0,0 @@
|
||||
export { default as useAddItem } from './use-add-item'
|
||||
export { default as useWishlist } from './use-wishlist'
|
||||
export { default as useRemoveItem } from './use-remove-item'
|
||||
export { default as useWishlistActions } from './use-wishlist-actions'
|
@ -1,57 +1,13 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useWishlistAddItem from '@commerce/wishlist/use-add-item'
|
||||
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
export type AddItemInput = ItemBody
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
|
||||
options,
|
||||
{ item },
|
||||
fetch
|
||||
) => {
|
||||
// TODO: add validations before doing the fetch
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useAddItem = (opts?: UseWishlistOptions) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const { revalidate } = useWishlist(opts)
|
||||
const fn = useWishlistAddItem(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
if (!customer) {
|
||||
// A signed customer is required in order to have a wishlist
|
||||
throw new CommerceError({
|
||||
message: 'Signed customer not found',
|
||||
})
|
||||
export function emptyHook() {
|
||||
const useEmptyHook = async (options = {}) => {
|
||||
return useCallback(async function () {
|
||||
return Promise.resolve()
|
||||
}, [])
|
||||
}
|
||||
|
||||
const data = await fn({ item: input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn, revalidate, customer]
|
||||
)
|
||||
}
|
||||
|
||||
useAddItem.extend = extendHook
|
||||
|
||||
return useAddItem
|
||||
return useEmptyHook
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
export default emptyHook
|
||||
|
@ -1,61 +1,17 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item'
|
||||
import type { RemoveItemBody } from '../api/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'DELETE',
|
||||
type Options = {
|
||||
includeProducts?: boolean
|
||||
}
|
||||
|
||||
export type RemoveItemInput = {
|
||||
id: string | number
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
|
||||
options,
|
||||
{ itemId },
|
||||
fetch
|
||||
) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useRemoveItem = (opts?: UseWishlistOptions) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const { revalidate } = useWishlist(opts)
|
||||
const fn = useWishlistRemoveItem<Wishlist | null, RemoveItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
|
||||
return useCallback(
|
||||
async function removeItem(input: RemoveItemInput) {
|
||||
if (!customer) {
|
||||
// A signed customer is required in order to have a wishlist
|
||||
throw new CommerceError({
|
||||
message: 'Signed customer not found',
|
||||
})
|
||||
export function emptyHook(options?: Options) {
|
||||
const useEmptyHook = async ({ id }: { id: string | number }) => {
|
||||
return useCallback(async function () {
|
||||
return Promise.resolve()
|
||||
}, [])
|
||||
}
|
||||
|
||||
const data = await fn({ itemId: String(input.id) })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn, revalidate, customer]
|
||||
)
|
||||
}
|
||||
|
||||
useRemoveItem.extend = extendHook
|
||||
|
||||
return useRemoveItem
|
||||
return useEmptyHook
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
export default emptyHook
|
||||
|
@ -1,11 +0,0 @@
|
||||
import useAddItem from './use-add-item'
|
||||
import useRemoveItem from './use-remove-item'
|
||||
|
||||
// This hook is probably not going to be used, but it's here
|
||||
// to show how a commerce should be structuring it
|
||||
export default function useWishlistActions() {
|
||||
const addItem = useAddItem()
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
return { addItem, removeItem }
|
||||
}
|
@ -1,17 +1,22 @@
|
||||
// TODO: replace this hook and other wishlist hooks with a handler, or remove them if
|
||||
// Shopify doesn't have a wishlist
|
||||
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import { SwrOptions } from '@commerce/utils/use-data'
|
||||
import defineProperty from '@commerce/utils/define-property'
|
||||
import useCommerceWishlist from '@commerce/wishlist/use-wishlist'
|
||||
import type { Wishlist } from '../api/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import { Product } from '../schema'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'GET',
|
||||
const defaultOpts = {}
|
||||
|
||||
export type Wishlist = {
|
||||
items: [
|
||||
{
|
||||
product_id: number
|
||||
variant_id: number
|
||||
id: number
|
||||
product: Product
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export type { Wishlist }
|
||||
|
||||
export interface UseWishlistOptions {
|
||||
includeProducts?: boolean
|
||||
}
|
||||
@ -20,55 +25,17 @@ export interface UseWishlistInput extends UseWishlistOptions {
|
||||
customerId?: number
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = (
|
||||
options,
|
||||
{ customerId, includeProducts },
|
||||
fetch
|
||||
) => {
|
||||
if (!customerId) return null
|
||||
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
|
||||
|
||||
if (includeProducts) url.searchParams.set('products', '1')
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options?.method ?? defaultOpts.method,
|
||||
})
|
||||
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
|
||||
// swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
|
||||
swrOptions?: any
|
||||
) {
|
||||
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const response = useCommerceWishlist(
|
||||
defaultOpts,
|
||||
[
|
||||
['customerId', customer?.id],
|
||||
['includeProducts', includeProducts],
|
||||
],
|
||||
customFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
}
|
||||
)
|
||||
|
||||
// Uses a getter to only calculate the prop when required
|
||||
// response.data is also a getter and it's better to not trigger it early
|
||||
if (!('isEmpty' in response)) {
|
||||
defineProperty(response, 'isEmpty', {
|
||||
get() {
|
||||
return (response.data?.items?.length || 0) <= 0
|
||||
},
|
||||
set: (x) => x,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
return { data: null }
|
||||
}
|
||||
|
||||
useWishlist.extend = extendHook
|
||||
|
Loading…
x
Reference in New Issue
Block a user