diff --git a/framework/vendure/README.md b/framework/vendure/README.md
index c5452d94a..0d6323db3 100644
--- a/framework/vendure/README.md
+++ b/framework/vendure/README.md
@@ -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
+```
diff --git a/framework/vendure/api/checkout.ts b/framework/vendure/api/checkout.ts
index 6dd565e51..be10ce609 100644
--- a/framework/vendure/api/checkout.ts
+++ b/framework/vendure/api/checkout.ts
@@ -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 = `
@@ -15,6 +14,9 @@ const checkoutApi= async (req: any, res: any, config: any) => {
Checkout not implemented :(
+
+ See #64
+
@@ -33,11 +35,7 @@ const checkoutApi= async (req: any, res: any, config: any) => {
}
}
-export function createApiHandler<
- T = any,
- H = {},
- Options extends {} = {}
->(
+export function createApiHandler(
handler: any,
handlers: H,
defaultOptions: Options
diff --git a/framework/vendure/api/fragments/cart.ts b/framework/vendure/api/fragments/cart.ts
index 2e246c6ba..36371e07c 100644
--- a/framework/vendure/api/fragments/cart.ts
+++ b/framework/vendure/api/fragments/cart.ts
@@ -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
}
diff --git a/framework/vendure/api/utils/concat-cookie.ts b/framework/vendure/api/utils/concat-cookie.ts
deleted file mode 100644
index 362e12e99..000000000
--- a/framework/vendure/api/utils/concat-cookie.ts
+++ /dev/null
@@ -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)]
-}
diff --git a/framework/vendure/auth/index.ts b/framework/vendure/auth/index.ts
deleted file mode 100644
index 36e757a89..000000000
--- a/framework/vendure/auth/index.ts
+++ /dev/null
@@ -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'
diff --git a/framework/vendure/auth/login.ts b/framework/vendure/auth/login.ts
deleted file mode 100644
index 15ef00eb5..000000000
--- a/framework/vendure/auth/login.ts
+++ /dev/null
@@ -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
-
-export type LoginVariables = LoginMutationVariables
-
-async function login(opts: {
- variables: LoginVariables
- config?: VendureConfig
- res: ServerResponse
-}): Promise
-
-async function login(opts: {
- query: string
- variables: V
- res: ServerResponse
- config?: VendureConfig
-}): Promise>
-
-async function login({
- query = loginMutation,
- variables,
- res: response,
- config,
-}: {
- query?: string
- variables: LoginVariables
- res: ServerResponse
- config?: VendureConfig
-}): Promise {
- config = getConfig(config)
-
- const { data, res } = await config.fetch(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
diff --git a/framework/vendure/auth/use-login.tsx b/framework/vendure/auth/use-login.tsx
index 94c756f8f..d3e365519 100644
--- a/framework/vendure/auth/use-login.tsx
+++ b/framework/vendure/auth/use-login.tsx
@@ -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 = (
- options,
- { username, password },
- fetch
-) => {
- if (!(username && password)) {
- throw new CommerceError({
- message: 'An email address and password are required to login',
- })
- }
+export default useLogin as UseLogin
- return fetch({
- ...options,
+export const handler: MutationHook = {
+ fetchOptions: {
query: loginMutation,
- variables: { username, password },
- })
-}
+ },
+ async fetcher({ input: { email, password }, options, fetch }) {
+ if (!(email && password)) {
+ throw new CommerceError({
+ message: 'A email and password are required to login',
+ })
+ }
-export function extendHook(customFetcher: typeof fetcher) {
- const useLogin = () => {
+ const variables: LoginMutationVariables = {
+ username: email,
+ password,
+ }
+
+ const { login } = await fetch({
+ ...options,
+ variables,
+ })
+
+ if (login.__typename !== 'CurrentUser') {
+ throw new ValidationError(login)
+ }
+
+ return null
+ },
+ useHook: ({ fetch }) => () => {
const { revalidate } = useCustomer()
- const fn = useCommerceLogin(
- {},
- 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)
diff --git a/framework/vendure/auth/use-logout.tsx b/framework/vendure/auth/use-logout.tsx
index d3d197c8e..3b053155b 100644
--- a/framework/vendure/auth/use-logout.tsx
+++ b/framework/vendure/auth/use-logout.tsx
@@ -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 = (options, _, fetch) => {
- return fetch({
- ...options,
- query: logoutMutation,
- })
-}
+export default useLogout as UseLogout
-export function extendHook(customFetcher: typeof fetcher) {
- const useLogout = () => {
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: logoutMutation,
+ },
+ async fetcher({ options, fetch }) {
+ await fetch({
+ ...options,
+ })
+ return null
+ },
+ useHook: ({ fetch }) => () => {
const { mutate } = useCustomer()
- const fn = useCommerceLogout({}, 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)
diff --git a/framework/vendure/auth/use-signup.tsx b/framework/vendure/auth/use-signup.tsx
index c31ba3e9b..dcd18ee5d 100644
--- a/framework/vendure/auth/use-signup.tsx
+++ b/framework/vendure/auth/use-signup.tsx
@@ -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
-export const fetcher: HookFetcher = (
- options,
- { input },
- fetch
-) => {
- const { firstName, lastName, emailAddress, password } = input
- if (!(firstName && lastName && emailAddress && password)) {
- throw new CommerceError({
- message:
- 'A first name, last name, email and password are required to signup',
- })
- }
-
- return fetch({
- ...options,
+export const handler: MutationHook<
+ null,
+ {},
+ RegisterCustomerInput,
+ RegisterCustomerInput
+> = {
+ fetchOptions: {
query: signupMutation,
- variables: { input },
- })
-}
+ },
+ async fetcher({
+ input: { firstName, lastName, emailAddress, password },
+ options,
+ fetch,
+ }) {
+ if (!(firstName && lastName && emailAddress && password)) {
+ throw new CommerceError({
+ message:
+ 'A first name, last name, email and password are required to signup',
+ })
+ }
+ const variables: SignupMutationVariables = {
+ input: {
+ firstName,
+ lastName,
+ emailAddress,
+ password,
+ },
+ }
+ const { registerCustomerAccount } = await fetch({
+ ...options,
+ 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(
- {},
- 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)
diff --git a/framework/vendure/cart/use-add-item.tsx b/framework/vendure/cart/use-add-item.tsx
index a94ab518c..780ef03ea 100644
--- a/framework/vendure/cart/use-add-item.tsx
+++ b/framework/vendure/cart/use-add-item.tsx
@@ -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
-export const fetcher: HookFetcher<
- AddItemToOrderMutation,
- AddItemToOrderMutationVariables
-> = (options, { variantId, quantity }, fetch) => {
- if (quantity && (!Number.isInteger(quantity) || quantity! < 1)) {
- throw new CommerceError({
- message: 'The item quantity has to be a valid integer greater than 0',
- })
- }
-
- return fetch({
- ...options,
+export const handler: MutationHook = {
+ fetchOptions: {
query: addItemToOrderMutation,
- variables: { variantId, quantity: quantity || 1 },
- })
-}
+ },
+ 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',
+ })
+ }
-export function extendHook(customFetcher: typeof fetcher) {
- const useAddItem = () => {
+ const { addItemToOrder } = await fetch({
+ ...options,
+ variables: {
+ quantity: input.quantity || 1,
+ variantId: input.variantId,
+ },
+ })
+
+ if (addItemToOrder.__typename === 'Order') {
+ return normalizeCart(addItemToOrder)
+ }
+ throw new CommerceError(addItemToOrder)
+ },
+ useHook: ({ fetch }) => () => {
const { mutate } = useCart()
- const fn = useCartAddItem({}, customFetcher)
return useCallback(
- async function addItem(input: AddItemInput) {
- const { addItemToOrder } = await fn({
- 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 }
+ async function addItem(input) {
+ const data = await fetch({ input })
+ await mutate(data, false)
+ return data
},
- [fn, mutate]
+ [fetch, mutate]
)
- }
-
- useAddItem.extend = extendHook
-
- return useAddItem
+ },
}
-
-export default extendHook(fetcher)
diff --git a/framework/vendure/cart/use-cart.tsx b/framework/vendure/cart/use-cart.tsx
index 98c49f924..5464be042 100644
--- a/framework/vendure/cart/use-cart.tsx
+++ b/framework/vendure/cart/use-cart.tsx
@@ -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 = (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
-) {
- const useCart = () => {
- const response = useData(
- { 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
- },
- descriptors: {
- isEmpty: {
- get() {
- return response.data?.activeOrder?.totalQuantity === 0
- },
- enumerable: true,
- },
- },
+export default useCart as UseCart
+
+export const handler: SWRHook<
+ Cart | null,
+ {},
+ FetchCartInput,
+ { isEmpty?: boolean }
+> = {
+ fetchOptions: {
+ query: getCartQuery,
+ },
+ async fetcher({ input: { cartId }, options, fetch }) {
+ const { activeOrder } = await fetch(options)
+ return activeOrder ? normalizeCart(activeOrder) : null
+ },
+ useHook: ({ useData }) => (input) => {
+ const response = useData({
+ swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
- return res
- }
-
- useCart.extend = extendHook
-
- return useCart
+ return useMemo(
+ () =>
+ Object.create(response, {
+ isEmpty: {
+ get() {
+ return (response.data?.lineItems.length ?? 0) <= 0
+ },
+ enumerable: true,
+ },
+ }),
+ [response]
+ )
+ },
}
-
-export default extendHook(fetcher)
diff --git a/framework/vendure/cart/use-remove-item.tsx b/framework/vendure/cart/use-remove-item.tsx
index d243099ef..151d05174 100644
--- a/framework/vendure/cart/use-remove-item.tsx
+++ b/framework/vendure/cart/use-remove-item.tsx
@@ -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
-export function extendHook(customFetcher: typeof fetcher) {
- const useRemoveItem = (item?: any) => {
+export const handler = {
+ fetchOptions: {
+ query: removeOrderLineMutation,
+ },
+ async fetcher({ input, options, fetch }: HookFetcherContext) {
+ const variables: RemoveOrderLineMutationVariables = {
+ orderLineId: input.id,
+ }
+ const { removeOrderLine } = await fetch({
+ ...options,
+ variables,
+ })
+
+ if (removeOrderLine.__typename === 'Order') {
+ return normalizeCart(removeOrderLine)
+ }
+ throw new CommerceError(removeOrderLine)
+ },
+ useHook: ({
+ fetch,
+ }: MutationHookContext) => (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)
diff --git a/framework/vendure/cart/use-update-item.tsx b/framework/vendure/cart/use-update-item.tsx
index 4c617b6ce..215e18980 100644
--- a/framework/vendure/cart/use-update-item.tsx
+++ b/framework/vendure/cart/use-update-item.tsx
@@ -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
+
+export const handler = {
+ fetchOptions: {
+ query: adjustOrderLineMutation,
+ },
+ async fetcher(context: HookFetcherContext>) {
+ const { input, options, fetch } = context
+ const variables: AdjustOrderLineMutationVariables = {
+ quantity: input.item.quantity || 1,
+ orderLineId: input.itemId,
+ }
+ const { adjustOrderLine } = await fetch({
+ ...options,
+ variables,
+ })
+
+ if (adjustOrderLine.__typename === 'Order') {
+ return normalizeCart(adjustOrderLine)
+ }
+ throw new CommerceError(adjustOrderLine)
+ },
+ useHook: ({
+ fetch,
+ }: MutationHookContext>) => (
+ 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,
- quantity: input.quantity,
- })
- if (adjustOrderLine.__typename === 'Order') {
- await mutate({ adjustOrderLine }, false)
- } else {
- throw new CommerceError({
- message: (adjustOrderLine as ErrorResult).message,
+ 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',
})
}
- return { adjustOrderLine }
- }, cfg?.wait ?? 500),
- [fn, mutate]
+ const data = await fetch({
+ input: {
+ item: {
+ productId,
+ variantId,
+ quantity: input.quantity,
+ },
+ itemId,
+ },
+ })
+ await mutate(data, false)
+ return data
+ },
+ [fetch, mutate]
)
- }
-
- useUpdateItem.extend = extendHook
-
- return useUpdateItem
+ },
}
-
-export default extendHook(fetcher)
diff --git a/framework/vendure/codegen.json b/framework/vendure/codegen.json
index c46dbaf0c..79a2b6ffa 100644
--- a/framework/vendure/codegen.json
+++ b/framework/vendure/codegen.json
@@ -14,7 +14,7 @@
"plugins": ["typescript", "typescript-operations"],
"config": {
"scalars": {
- "ID": "number"
+ "ID": "string"
}
}
},
diff --git a/framework/vendure/commerce.config.json b/framework/vendure/commerce.config.json
new file mode 100644
index 000000000..70806f062
--- /dev/null
+++ b/framework/vendure/commerce.config.json
@@ -0,0 +1,6 @@
+{
+ "provider": "vendure",
+ "features": {
+ "wishlist": false
+ }
+}
diff --git a/framework/vendure/common/get-site-info.ts b/framework/vendure/common/get-site-info.ts
index 37da5e7eb..ba7df175b 100644
--- a/framework/vendure/common/get-site-info.ts
+++ b/framework/vendure/common/get-site-info.ts
@@ -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 {
diff --git a/framework/vendure/customer/use-customer.tsx b/framework/vendure/customer/use-customer.tsx
index eea83a1a6..74275abd1 100644
--- a/framework/vendure/customer/use-customer.tsx
+++ b/framework/vendure/customer/use-customer.tsx
@@ -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 = async (
- options,
- _,
- fetch
-) => {
- return await fetch({ query: activeCustomerQuery })
-}
+export default useCustomer as UseCustomer
-export function extendHook(
- customFetcher: typeof fetcher,
- swrOptions?: SwrOptions
-) {
- const useCustomer = () => {
- const response = useCommerceCustomer({}, [], customFetcher, {
- revalidateOnFocus: false,
- ...swrOptions,
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: activeCustomerQuery,
+ },
+ async fetcher({ options, fetch }) {
+ const { activeCustomer } = await fetch({
+ ...options,
})
-
- return useResponse(response, {
- normalizer: (data) => {
- return data?.activeCustomer
- ? {
- firstName: data?.activeCustomer?.firstName ?? '',
- lastName: data?.activeCustomer?.lastName ?? '',
- email: data?.activeCustomer?.emailAddress ?? '',
- }
- : null
+ 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)
diff --git a/framework/vendure/fetcher.ts b/framework/vendure/fetcher.ts
new file mode 100644
index 000000000..d980b9c90
--- /dev/null
+++ b/framework/vendure/fetcher.ts
@@ -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)
+}
diff --git a/framework/vendure/index.tsx b/framework/vendure/index.tsx
index d80c69f6a..47e60c7df 100644
--- a/framework/vendure/index.tsx
+++ b/framework/vendure/index.tsx
@@ -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
@@ -57,7 +21,10 @@ export type VendureProps = {
export function CommerceProvider({ children, ...config }: VendureProps) {
return (
-
+
{children}
)
diff --git a/framework/vendure/lib/array-to-tree.ts b/framework/vendure/lib/array-to-tree.ts
index 8711d6c1e..403cf226e 100644
--- a/framework/vendure/lib/array-to-tree.ts
+++ b/framework/vendure/lib/array-to-tree.ts
@@ -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 & {
children: Array>
expanded: boolean
}
export type RootNode = {
- id?: number
+ id?: string
children: Array>
}
@@ -54,8 +54,8 @@ export function arrayToTree(
*/
function treeToMap(
tree?: RootNode
-): Map> {
- const nodeMap = new Map>()
+): Map> {
+ const nodeMap = new Map>()
function visit(node: TreeNode) {
nodeMap.set(node.id, node)
node.children.forEach(visit)
diff --git a/framework/vendure/lib/normalize.ts b/framework/vendure/lib/normalize.ts
index 2b8de0f8c..0be24461c 100644
--- a/framework/vendure/lib/normalize.ts
+++ b/framework/vendure/lib/normalize.ts
@@ -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,
+ },
})),
}
}
diff --git a/framework/vendure/product/get-all-products.ts b/framework/vendure/product/get-all-products.ts
index f9815aa37..447dd4d2f 100644
--- a/framework/vendure/product/get-all-products.ts
+++ b/framework/vendure/product/get-all-products.ts
@@ -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!) {
diff --git a/framework/vendure/product/get-product.ts b/framework/vendure/product/get-product.ts
index 0e46028fd..61be2c3f6 100644
--- a/framework/vendure/product/get-product.ts
+++ b/framework/vendure/product/get-product.ts
@@ -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(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,
}
}
diff --git a/framework/vendure/product/use-price.tsx b/framework/vendure/product/use-price.tsx
index a79940a76..0174faf5e 100644
--- a/framework/vendure/product/use-price.tsx
+++ b/framework/vendure/product/use-price.tsx
@@ -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'
diff --git a/framework/vendure/product/use-search.tsx b/framework/vendure/product/use-search.tsx
index d34013e30..3c285e26c 100644
--- a/framework/vendure/product/use-search.tsx
+++ b/framework/vendure/product/use-search.tsx
@@ -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
+
export type SearchProductsInput = {
search?: string
- categoryId?: number
- brandId?: number
+ categoryId?: string
+ brandId?: string
sort?: string
}
-export const fetcher: HookFetcher = (
- 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
-) {
- const useSearch = (input: SearchProductsInput = {}) => {
- const response = useCommerceSearch(
- {},
- [
+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({
+ 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)
diff --git a/framework/vendure/provider.ts b/framework/vendure/provider.ts
new file mode 100644
index 000000000..38b0b8c4b
--- /dev/null
+++ b/framework/vendure/provider.ts
@@ -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 },
+}
diff --git a/framework/vendure/schema.d.ts b/framework/vendure/schema.d.ts
index a9361440a..78e821257 100644
--- a/framework/vendure/schema.d.ts
+++ b/framework/vendure/schema.d.ts
@@ -8,7 +8,7 @@ export type MakeMaybe = Omit &
{ [SubKey in K]: Maybe }
/** 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
/** Returns a list of eligible shipping methods based on the current active Order */
eligibleShippingMethods: Array
+ /** Returns a list of payment methods and their eligibility based on the current active Order */
+ eligiblePaymentMethods: Array
/** Returns information about the current authenticated User */
me?: Maybe
/** 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
+ customFields?: Maybe
}
export type Coordinate = {
@@ -376,6 +379,7 @@ export type Channel = Node & {
defaultLanguageCode: LanguageCode
currencyCode: CurrencyCode
pricesIncludeTax: Scalars['Boolean']
+ customFields?: Maybe
}
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
label?: Maybe
description?: Maybe
ui?: Maybe
@@ -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
+}
+
+export type PaymentMethodQuote = {
+ __typename?: 'PaymentMethodQuote'
+ id: Scalars['ID']
+ code: Scalars['String']
+ isEligible: Scalars['Boolean']
+ eligibilityMessage?: Maybe
+}
+
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
-}
-
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
@@ -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
+}
+
/** 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
priceIncludesTax?: Maybe
priceWithTax?: Maybe
+ stockLevel?: Maybe
}
export type ProductVariantSortParameter = {
@@ -2715,6 +2748,7 @@ export type ProductVariantSortParameter = {
name?: Maybe
price?: Maybe
priceWithTax?: Maybe
+ stockLevel?: Maybe
}
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>
lines: Array<
- { __typename?: 'OrderLine' } & Pick & {
+ { __typename?: 'OrderLine' } & Pick<
+ OrderLine,
+ 'id' | 'quantity' | 'linePriceWithTax' | 'discountedLinePriceWithTax'
+ > & {
featuredAsset?: Maybe<
{ __typename?: 'Asset' } & Pick
>
+ 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 }
}
>
@@ -2836,28 +2886,6 @@ export type SearchResultFragment = { __typename?: 'SearchResult' } & Pick<
| ({ __typename?: 'SinglePrice' } & Pick)
}
-export type LoginServerMutationVariables = Exact<{
- email: Scalars['String']
- password: Scalars['String']
-}>
-
-export type LoginServerMutation = { __typename?: 'Mutation' } & {
- login:
- | ({ __typename: 'CurrentUser' } & Pick)
- | ({ __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
+ { __typename?: 'ProductOption' } & Pick<
+ ProductOption,
+ 'id' | 'name'
+ >
>
}
>
diff --git a/framework/vendure/schema.graphql b/framework/vendure/schema.graphql
index 6d02bb154..88812044e 100644
--- a/framework/vendure/schema.graphql
+++ b/framework/vendure/schema.graphql
@@ -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 {
diff --git a/framework/vendure/wishlist/index.ts b/framework/vendure/wishlist/index.ts
deleted file mode 100644
index 9ea28291c..000000000
--- a/framework/vendure/wishlist/index.ts
+++ /dev/null
@@ -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'
diff --git a/framework/vendure/wishlist/use-add-item.tsx b/framework/vendure/wishlist/use-add-item.tsx
index 6e7d9de41..75f067c3a 100644
--- a/framework/vendure/wishlist/use-add-item.tsx
+++ b/framework/vendure/wishlist/use-add-item.tsx
@@ -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 = (
- 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',
- })
- }
-
- const data = await fn({ item: input })
- await revalidate()
- return data
- },
- [fn, revalidate, customer]
- )
+export function emptyHook() {
+ const useEmptyHook = async (options = {}) => {
+ return useCallback(async function () {
+ return Promise.resolve()
+ }, [])
}
- useAddItem.extend = extendHook
-
- return useAddItem
+ return useEmptyHook
}
-export default extendHook(fetcher)
+export default emptyHook
diff --git a/framework/vendure/wishlist/use-remove-item.tsx b/framework/vendure/wishlist/use-remove-item.tsx
index 86614a21a..a2d3a8a05 100644
--- a/framework/vendure/wishlist/use-remove-item.tsx
+++ b/framework/vendure/wishlist/use-remove-item.tsx
@@ -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 = (
- 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(
- 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',
- })
- }
-
- const data = await fn({ itemId: String(input.id) })
- await revalidate()
- return data
- },
- [fn, revalidate, customer]
- )
+export function emptyHook(options?: Options) {
+ const useEmptyHook = async ({ id }: { id: string | number }) => {
+ return useCallback(async function () {
+ return Promise.resolve()
+ }, [])
}
- useRemoveItem.extend = extendHook
-
- return useRemoveItem
+ return useEmptyHook
}
-export default extendHook(fetcher)
+export default emptyHook
diff --git a/framework/vendure/wishlist/use-wishlist-actions.tsx b/framework/vendure/wishlist/use-wishlist-actions.tsx
deleted file mode 100644
index 711d00516..000000000
--- a/framework/vendure/wishlist/use-wishlist-actions.tsx
+++ /dev/null
@@ -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 }
-}
diff --git a/framework/vendure/wishlist/use-wishlist.tsx b/framework/vendure/wishlist/use-wishlist.tsx
index 870b90019..d2ce9db5b 100644
--- a/framework/vendure/wishlist/use-wishlist.tsx
+++ b/framework/vendure/wishlist/use-wishlist.tsx
@@ -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 = (
- 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 = () => {
+ return null
}
export function extendHook(
customFetcher: typeof fetcher,
- swrOptions?: SwrOptions
+ // swrOptions?: SwrOptions
+ 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