mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 15:06:59 +00:00
Add Open Commerce Provider
Add Open Commerce Provider
This commit is contained in:
commit
faae6a1e02
@ -5,7 +5,9 @@ import type {
|
|||||||
HookFetcherContext,
|
HookFetcherContext,
|
||||||
} from '@vercel/commerce/utils/types'
|
} from '@vercel/commerce/utils/types'
|
||||||
import { ValidationError } from '@vercel/commerce/utils/errors'
|
import { ValidationError } from '@vercel/commerce/utils/errors'
|
||||||
import useUpdateItem, { UseUpdateItem } from '@vercel/commerce/cart/use-update-item'
|
import useUpdateItem, {
|
||||||
|
UseUpdateItem,
|
||||||
|
} from '@vercel/commerce/cart/use-update-item'
|
||||||
import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart'
|
import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart'
|
||||||
import { handler as removeItemHandler } from './use-remove-item'
|
import { handler as removeItemHandler } from './use-remove-item'
|
||||||
import useCart from './use-cart'
|
import useCart from './use-cart'
|
||||||
|
@ -15,7 +15,8 @@ A commerce provider is a headless e-commerce platform that integrates with the [
|
|||||||
- Kibo Commerce ([packages/kibocommerce](../kibocommerce))
|
- Kibo Commerce ([packages/kibocommerce](../kibocommerce))
|
||||||
- Commerce.js ([packages/commercejs](../commercejs))
|
- Commerce.js ([packages/commercejs](../commercejs))
|
||||||
- SFCC - SalesForce Cloud Commerce ([packages/sfcc](../sfcc))
|
- SFCC - SalesForce Cloud Commerce ([packages/sfcc](../sfcc))
|
||||||
|
- Mailchimp Open Commerce ([packages/opencommerce](../opencommerce))
|
||||||
|
-
|
||||||
Adding a commerce provider means adding a new folder in `packages` with a folder structure like the next one:
|
Adding a commerce provider means adding a new folder in `packages` with a folder structure like the next one:
|
||||||
|
|
||||||
- `src`
|
- `src`
|
||||||
|
5
packages/opencommerce/.env.template
Normal file
5
packages/opencommerce/.env.template
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
COMMERCE_PROVIDER=@vercel/commerce-opencommerce
|
||||||
|
|
||||||
|
OPENCOMMERCE_STOREFRONT_API_URL=
|
||||||
|
OPENCOMMERCE_PRIMARY_SHOP_ID=
|
||||||
|
OPENCOMMERCE_IMAGE_DOMAIN=
|
2
packages/opencommerce/.prettierignore
Normal file
2
packages/opencommerce/.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
6
packages/opencommerce/.prettierrc
Normal file
6
packages/opencommerce/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
56
packages/opencommerce/README.md
Normal file
56
packages/opencommerce/README.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
## Open Commerce Provider
|
||||||
|
|
||||||
|
**Preview:** https://commerce-pinkcloudvnn.vercel.app/
|
||||||
|
|
||||||
|
We are using this admin interface for demo: https://admin.open-commerce.io/
|
||||||
|
Storefront API URL: https://api.open-commerce.io/graphql
|
||||||
|
|
||||||
|
## Available Features
|
||||||
|
|
||||||
|
- Cart
|
||||||
|
- Search
|
||||||
|
- Custom Checkout
|
||||||
|
- Custom Navigation
|
||||||
|
|
||||||
|
## Steps to get started:
|
||||||
|
|
||||||
|
1. Duplicate `site/.env.template` and rename it to `site/.env.local`
|
||||||
|
2. Setup env variables related to open commerce provider, something looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
COMMERCE_PROVIDER=@vercel/commerce-opencommerce
|
||||||
|
OPENCOMMERCE_STOREFRONT_API_URL=https://api.open-commerce.io/graphql
|
||||||
|
OPENCOMMERCE_PRIMARY_SHOP_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
OPENCOMMERCE_IMAGE_DOMAIN=api.open-commerce.io
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: We can query the primary shop ID from the graphql api playground
|
||||||
|
|
||||||
|
3. Run `yarn` and `yarn dev` in the root folder
|
||||||
|
|
||||||
|
### Troubleshoot
|
||||||
|
|
||||||
|
https://github.com/reactioncommerce/commerce#troubleshoot
|
||||||
|
|
||||||
|
For more information about the repository, check out the README file here: https://github.com/reactioncommerce/commerce
|
||||||
|
|
||||||
|
## Custom Checkouts
|
||||||
|
|
||||||
|
The demo site use [Example Payment Plugin](https://github.com/reactioncommerce/api-plugin-payments-example) for processing payments. To make the checkout flow works as expected, some prerequisite steps need to be handled:
|
||||||
|
|
||||||
|
- Add and enable at least one flat-rate shipping option in the admin setting page. By default, we select the first shipping option to create the order
|
||||||
|
- Enable the `IOU Example` payment method in the admin setting page
|
||||||
|
- Fill in all the required fields in the `Add Shipping Address` step in the storefront
|
||||||
|
- After processing checkout, check the orders page in the admin interface to confirm the order has been successfully created
|
||||||
|
|
||||||
|
We can also use `Stripe` as the default payment method by following steps:
|
||||||
|
|
||||||
|
- Add and enable at least one flat-rate shipping option in the admin setting page. By default, we select the first shipping option to create the order
|
||||||
|
- Enable the `Stripe (SCA)` payment method in the admin setting page
|
||||||
|
- Add `OPENCOMMERCE_STRIPE_PUBLIC_API_KEY` env variable to the `site/.env.local` file
|
||||||
|
- Add `STRIPE_API_KEY` env variable to reaction API `.env` file with the private API key from Stripe
|
||||||
|
- Modify the `submit-checkout` api endpoint to include additional steps for Stripe payment (eg: create/confirm payment intent, pass payment intent id to placeOrder request body,...).
|
||||||
|
|
||||||
|
## Custom Navigation
|
||||||
|
|
||||||
|
By default NextJS only display two categories/tags of products in the navbar. This feature enables us to show the organized categories/tags as navigation in the admin to our storefront. Also we can leverage this feature to show URLs that are not relative to the shop (eg: admin page, documentation page, ...). This feature is enabled by default, we can turn it off anytime by modify the value in the `commerce.config.json` file.
|
22
packages/opencommerce/codegen.json
Normal file
22
packages/opencommerce/codegen.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"overwrite": true,
|
||||||
|
"schema": "http://localhost:3000/graphql",
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"./src/api/**/*.ts": {
|
||||||
|
"noRequire": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generates": {
|
||||||
|
"./schema.d.ts": {
|
||||||
|
"plugins": ["typescript", "typescript-operations"]
|
||||||
|
},
|
||||||
|
"./schema.graphql": {
|
||||||
|
"plugins": ["schema-ast"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"afterAllFileWrite": ["prettier --write"]
|
||||||
|
}
|
||||||
|
}
|
1
packages/opencommerce/global.d.ts
vendored
Normal file
1
packages/opencommerce/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module '@components/checkout/context'
|
84
packages/opencommerce/package.json
Normal file
84
packages/opencommerce/package.json
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"name": "@vercel/commerce-opencommerce",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"release": "taskr release",
|
||||||
|
"build": "taskr build",
|
||||||
|
"dev": "taskr",
|
||||||
|
"types": "tsc --emitDeclarationOnly",
|
||||||
|
"prettier-fix": "prettier --write .",
|
||||||
|
"generate": "graphql-codegen --config codegen.json"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/index.js",
|
||||||
|
"./*": [
|
||||||
|
"./dist/*.js",
|
||||||
|
"./dist/*/index.js"
|
||||||
|
],
|
||||||
|
"./next.config": "./dist/next.config.cjs"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": [
|
||||||
|
"src/*",
|
||||||
|
"src/*/index"
|
||||||
|
],
|
||||||
|
"next.config": [
|
||||||
|
"dist/next.config.d.cts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": [
|
||||||
|
"dist/*.d.ts",
|
||||||
|
"dist/*/index.d.ts"
|
||||||
|
],
|
||||||
|
"next.config": [
|
||||||
|
"dist/next.config.d.cts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vercel/commerce": "^0.0.1",
|
||||||
|
"@vercel/fetch": "^6.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": "^12",
|
||||||
|
"react": "^17",
|
||||||
|
"react-dom": "^17"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@graphql-codegen/cli": "2.6.2",
|
||||||
|
"@graphql-codegen/schema-ast": "^2.4.1",
|
||||||
|
"@graphql-codegen/typescript": "2.4.8",
|
||||||
|
"@graphql-codegen/typescript-operations": "2.3.5",
|
||||||
|
"@taskr/clear": "^1.1.0",
|
||||||
|
"@taskr/esnext": "^1.1.0",
|
||||||
|
"@taskr/watch": "^1.1.0",
|
||||||
|
"@types/node": "^17.0.8",
|
||||||
|
"@types/react": "^17.0.38",
|
||||||
|
"lint-staged": "^12.1.7",
|
||||||
|
"next": "^12.0.8",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"taskr": "^1.1.0",
|
||||||
|
"taskr-swc": "^0.0.1",
|
||||||
|
"typescript": "^4.5.4"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"**/*.{js,jsx,ts,tsx,json}": [
|
||||||
|
"prettier --write",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
8370
packages/opencommerce/schema.d.ts
vendored
Normal file
8370
packages/opencommerce/schema.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
14495
packages/opencommerce/schema.graphql
Normal file
14495
packages/opencommerce/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
76
packages/opencommerce/src/api/endpoints/cart/add-item.ts
Normal file
76
packages/opencommerce/src/api/endpoints/cart/add-item.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { normalizeCart } from '../../../utils/normalize'
|
||||||
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
|
import addCartItemsMutation from '../../mutations/add-cart-item'
|
||||||
|
import createCartMutation from '../../mutations/create-cart'
|
||||||
|
|
||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||||
|
res,
|
||||||
|
body: { cartId, item },
|
||||||
|
config,
|
||||||
|
req: { cookies },
|
||||||
|
}) => {
|
||||||
|
if (!item) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Missing item' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!item.quantity) item.quantity = 1
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
input: {
|
||||||
|
shopId: config.shopId,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
productConfiguration: {
|
||||||
|
productId: item.productId,
|
||||||
|
productVariantId: item.variantId,
|
||||||
|
},
|
||||||
|
quantity: item.quantity,
|
||||||
|
price: {
|
||||||
|
amount: item.variant?.price,
|
||||||
|
currencyCode: item.currencyCode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cartId) {
|
||||||
|
const {
|
||||||
|
data: { createCart },
|
||||||
|
} = await config.fetch(createCartMutation, { variables })
|
||||||
|
res.setHeader('Set-Cookie', [
|
||||||
|
getCartCookie(
|
||||||
|
config.cartCookie,
|
||||||
|
createCart.cart._id,
|
||||||
|
config.cartCookieMaxAge
|
||||||
|
),
|
||||||
|
getCartCookie(
|
||||||
|
config.anonymousCartTokenCookie,
|
||||||
|
createCart.token,
|
||||||
|
config.cartCookieMaxAge
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
return res.status(200).json({ data: normalizeCart(createCart.cart) })
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { addCartItems },
|
||||||
|
} = await config.fetch(addCartItemsMutation, {
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
items: variables.input.items,
|
||||||
|
cartId,
|
||||||
|
cartToken: cookies[config.anonymousCartTokenCookie],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(200).json({ data: normalizeCart(addCartItems.cart) })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default addItem
|
32
packages/opencommerce/src/api/endpoints/cart/get-cart.ts
Normal file
32
packages/opencommerce/src/api/endpoints/cart/get-cart.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { normalizeCart } from '../../../utils/normalize'
|
||||||
|
import getAnonymousCartQuery from '../../queries/get-anonymous-cart'
|
||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
// Return current cart info
|
||||||
|
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
||||||
|
res,
|
||||||
|
req: { cookies },
|
||||||
|
body: { cartId },
|
||||||
|
config,
|
||||||
|
}) => {
|
||||||
|
if (cartId && cookies[config.anonymousCartTokenCookie]) {
|
||||||
|
const {
|
||||||
|
data: { cart: rawAnonymousCart },
|
||||||
|
} = await config.fetch(getAnonymousCartQuery, {
|
||||||
|
variables: {
|
||||||
|
cartId,
|
||||||
|
cartToken: cookies[config.anonymousCartTokenCookie],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
data: rawAnonymousCart ? normalizeCart(rawAnonymousCart) : null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
data: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getCart
|
26
packages/opencommerce/src/api/endpoints/cart/index.ts
Normal file
26
packages/opencommerce/src/api/endpoints/cart/index.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||||
|
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
|
||||||
|
import type { CartSchema } from '../../../types/cart'
|
||||||
|
import type { OpenCommerceAPI } from '../../index'
|
||||||
|
import getCart from './get-cart'
|
||||||
|
import addItem from './add-item'
|
||||||
|
import updateItem from './update-item'
|
||||||
|
import removeItem from './remove-item'
|
||||||
|
|
||||||
|
export type CartAPI = GetAPISchema<OpenCommerceAPI, CartSchema>
|
||||||
|
|
||||||
|
export type CartEndpoint = CartAPI['endpoint']
|
||||||
|
|
||||||
|
export const handlers: CartEndpoint['handlers'] = {
|
||||||
|
addItem,
|
||||||
|
getCart,
|
||||||
|
updateItem,
|
||||||
|
removeItem,
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartApi = createEndpoint<CartAPI>({
|
||||||
|
handler: cartEndpoint,
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default cartApi
|
39
packages/opencommerce/src/api/endpoints/cart/remove-item.ts
Normal file
39
packages/opencommerce/src/api/endpoints/cart/remove-item.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { normalizeCart } from '../../../utils/normalize'
|
||||||
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
|
import removeCartItemsMutation from '../../mutations/remove-cart-item'
|
||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
||||||
|
res,
|
||||||
|
body: { cartId, itemId },
|
||||||
|
config,
|
||||||
|
req: { cookies },
|
||||||
|
}) => {
|
||||||
|
if (!cartId || !itemId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { removeCartItems },
|
||||||
|
} = await config.fetch(removeCartItemsMutation, {
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
cartId,
|
||||||
|
cartItemIds: [itemId],
|
||||||
|
cartToken: cookies[config.anonymousCartTokenCookie],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
res.setHeader(
|
||||||
|
'Set-Cookie',
|
||||||
|
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ data: normalizeCart(removeCartItems.cart) })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default removeItem
|
39
packages/opencommerce/src/api/endpoints/cart/update-item.ts
Normal file
39
packages/opencommerce/src/api/endpoints/cart/update-item.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { normalizeCart } from '../../../utils/normalize'
|
||||||
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
|
import updateCartItemsQuantityMutation from '../../mutations/update-cart-item-quantity'
|
||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||||
|
res,
|
||||||
|
body: { cartId, itemId, item },
|
||||||
|
config,
|
||||||
|
req: { cookies },
|
||||||
|
}) => {
|
||||||
|
if (!cartId || !itemId || !item) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { updateCartItemsQuantity },
|
||||||
|
} = await config.fetch(updateCartItemsQuantityMutation, {
|
||||||
|
variables: {
|
||||||
|
updateCartItemsQuantityInput: {
|
||||||
|
cartId,
|
||||||
|
cartToken: cookies[config.anonymousCartTokenCookie],
|
||||||
|
items: [{ cartItemId: itemId, quantity: item.quantity }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the cart cookie
|
||||||
|
res.setHeader(
|
||||||
|
'Set-Cookie',
|
||||||
|
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
||||||
|
)
|
||||||
|
res.status(200).json({ data: normalizeCart(updateCartItemsQuantity.cart) })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateItem
|
@ -0,0 +1,56 @@
|
|||||||
|
import type { ProductsEndpoint } from './products'
|
||||||
|
import getSearchVariables from '../../utils/get-search-variables'
|
||||||
|
import getSortVariables from '../../utils/get-sort-variables'
|
||||||
|
import { CatalogItemsQueryVariables } from '../../../../schema'
|
||||||
|
import getShopCurrencyQuery from '../../queries/get-shop-currency-query'
|
||||||
|
|
||||||
|
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
||||||
|
body: { brandId, search, sort, categoryId },
|
||||||
|
res,
|
||||||
|
config,
|
||||||
|
commerce,
|
||||||
|
}) => {
|
||||||
|
let sortParams = getSortVariables(sort)
|
||||||
|
if (sortParams?.sortBy === 'featured' && !categoryId) {
|
||||||
|
sortParams = null
|
||||||
|
}
|
||||||
|
|
||||||
|
let currency: string | null = null
|
||||||
|
|
||||||
|
if (sortParams?.sortBy === 'minPrice') {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
shop: {
|
||||||
|
currency: { code },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = await config.fetch(getShopCurrencyQuery, {
|
||||||
|
variables: { id: config.shopId },
|
||||||
|
})
|
||||||
|
currency = code
|
||||||
|
}
|
||||||
|
|
||||||
|
const { products } = await commerce.getAllProducts({
|
||||||
|
variables: {
|
||||||
|
...getSearchVariables({ brandId, search, categoryId }),
|
||||||
|
...(sortParams
|
||||||
|
? {
|
||||||
|
sortBy: sortParams.sortBy as CatalogItemsQueryVariables['sortBy'],
|
||||||
|
sortOrder:
|
||||||
|
sortParams.sortOrder as CatalogItemsQueryVariables['sortOrder'],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(currency ? { sortByPriceCurrencyCode: currency } : {}),
|
||||||
|
},
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
data: {
|
||||||
|
products,
|
||||||
|
found: !!products.length,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getProducts
|
20
packages/opencommerce/src/api/endpoints/catalog/products.ts
Normal file
20
packages/opencommerce/src/api/endpoints/catalog/products.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { CommerceAPI, createEndpoint, GetAPISchema } from '@vercel/commerce/api'
|
||||||
|
import productsEndpoint from '@vercel/commerce/api/endpoints/catalog/products'
|
||||||
|
import type { ProductsSchema } from '../../../types/product'
|
||||||
|
import type { OpenCommerceAPI } from '../../index'
|
||||||
|
import getProducts from './get-products'
|
||||||
|
|
||||||
|
export type ProductsAPI = GetAPISchema<OpenCommerceAPI, ProductsSchema>
|
||||||
|
|
||||||
|
export type ProductsEndpoint = ProductsAPI['endpoint']
|
||||||
|
|
||||||
|
export const handlers: ProductsEndpoint['handlers'] = {
|
||||||
|
getProducts,
|
||||||
|
}
|
||||||
|
|
||||||
|
const productsApi = createEndpoint<ProductsAPI>({
|
||||||
|
handler: productsEndpoint,
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default productsApi
|
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
23
packages/opencommerce/src/api/endpoints/checkout/index.ts
Normal file
23
packages/opencommerce/src/api/endpoints/checkout/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||||
|
import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
|
||||||
|
import type { CheckoutSchema } from '../../../types/checkout'
|
||||||
|
import type { OpenCommerceAPI } from '../..'
|
||||||
|
|
||||||
|
import submitCheckout from './submit-checkout'
|
||||||
|
import getCheckout from './get-checkout'
|
||||||
|
|
||||||
|
export type CheckoutAPI = GetAPISchema<OpenCommerceAPI, CheckoutSchema>
|
||||||
|
|
||||||
|
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||||
|
|
||||||
|
export const handlers: CheckoutEndpoint['handlers'] = {
|
||||||
|
submitCheckout,
|
||||||
|
getCheckout,
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkoutApi = createEndpoint<CheckoutAPI>({
|
||||||
|
handler: checkoutEndpoint,
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default checkoutApi
|
@ -0,0 +1,65 @@
|
|||||||
|
import { LineItem } from '../../../types/cart'
|
||||||
|
import placeOrder from '../../mutations/place-order'
|
||||||
|
import setEmailOnAnonymousCart from '../../mutations/set-email-on-anonymous-cart'
|
||||||
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
|
import type { CheckoutEndpoint } from '.'
|
||||||
|
|
||||||
|
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
|
||||||
|
res,
|
||||||
|
body: { item, cartId },
|
||||||
|
config: { fetch, shopId, anonymousCartTokenCookie, cartCookie },
|
||||||
|
req: { cookies },
|
||||||
|
}) => {
|
||||||
|
await fetch(setEmailOnAnonymousCart, {
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
cartId,
|
||||||
|
cartToken: cookies[anonymousCartTokenCookie],
|
||||||
|
email: 'opencommerce@test.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data } = await fetch(placeOrder, {
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
payments: {
|
||||||
|
data: { fullName: 'Open Commerce Demo Site' },
|
||||||
|
amount: item.checkout.cart.checkout.summary.total.amount,
|
||||||
|
method: 'iou_example',
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
cartId,
|
||||||
|
currencyCode: item.checkout.cart.currency.code,
|
||||||
|
email: 'opencommerce@test.com',
|
||||||
|
shopId,
|
||||||
|
fulfillmentGroups: {
|
||||||
|
shopId,
|
||||||
|
data: item.checkout.cart.checkout.fulfillmentGroups[0].data,
|
||||||
|
items: item.checkout.cart.lineItems.map((item: LineItem) => ({
|
||||||
|
price: item.variant.price,
|
||||||
|
quantity: item.quantity,
|
||||||
|
productConfiguration: {
|
||||||
|
productId: item.productId,
|
||||||
|
productVariantId: item.variantId,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
type: item.checkout.cart.checkout.fulfillmentGroups[0].type,
|
||||||
|
selectedFulfillmentMethodId:
|
||||||
|
item.checkout.cart.checkout.fulfillmentGroups[0]
|
||||||
|
.selectedFulfillmentOption.fulfillmentMethod._id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
res.setHeader('Set-Cookie', [
|
||||||
|
getCartCookie(cartCookie),
|
||||||
|
getCartCookie(anonymousCartTokenCookie),
|
||||||
|
])
|
||||||
|
|
||||||
|
res.status(200).json({ data: null, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default submitCheckout
|
@ -0,0 +1,73 @@
|
|||||||
|
import setShippingAddressOnCartMutation from '../../../mutations/add-shipping-address'
|
||||||
|
import type { CustomerAddressEndpoint } from '.'
|
||||||
|
import updateFulfillmentOptions from '../../../mutations/update-fulfillment-options'
|
||||||
|
import selectFulfillmentOptions from '../../../mutations/select-fulfillment-options'
|
||||||
|
|
||||||
|
const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
|
||||||
|
res,
|
||||||
|
body: { item, cartId },
|
||||||
|
config: { fetch, anonymousCartTokenCookie },
|
||||||
|
req: { cookies },
|
||||||
|
}) => {
|
||||||
|
// Return an error if no cart is present
|
||||||
|
if (!cartId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Cookie not found' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register address
|
||||||
|
const {
|
||||||
|
data: { setShippingAddressOnCart },
|
||||||
|
} = await fetch(setShippingAddressOnCartMutation, {
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
address: {
|
||||||
|
address1: item.streetNumber || 'NextJS storefront',
|
||||||
|
country: item.country,
|
||||||
|
fullName: `${item.firstName || 'Test'} ${
|
||||||
|
item.lastName || 'Account'
|
||||||
|
}}`,
|
||||||
|
city: item.city || 'LA',
|
||||||
|
phone: '0123456789',
|
||||||
|
postal: item.zipCode || '1234567',
|
||||||
|
region: item.city || 'LA',
|
||||||
|
},
|
||||||
|
cartId,
|
||||||
|
cartToken: cookies[anonymousCartTokenCookie],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { updateFulfillmentOptionsForGroup },
|
||||||
|
} = await fetch(updateFulfillmentOptions, {
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
cartId,
|
||||||
|
fulfillmentGroupId:
|
||||||
|
setShippingAddressOnCart.cart.checkout.fulfillmentGroups[0]._id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await fetch(selectFulfillmentOptions, {
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
cartId,
|
||||||
|
cartToken: cookies[anonymousCartTokenCookie],
|
||||||
|
fulfillmentGroupId:
|
||||||
|
updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups[0]
|
||||||
|
._id,
|
||||||
|
fulfillmentMethodId:
|
||||||
|
updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups[0]
|
||||||
|
.availableFulfillmentOptions[0].fulfillmentMethod._id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(200).json({ data: null, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default addItem
|
@ -0,0 +1,9 @@
|
|||||||
|
import type { CustomerAddressEndpoint } from '.'
|
||||||
|
|
||||||
|
const getCards: CustomerAddressEndpoint['handlers']['getAddresses'] = async ({
|
||||||
|
res,
|
||||||
|
}) => {
|
||||||
|
return res.status(200).json({ data: null, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getCards
|
@ -0,0 +1,33 @@
|
|||||||
|
import type {
|
||||||
|
CustomerAddressSchema,
|
||||||
|
CustomerAddressTypes,
|
||||||
|
} from '../../../../types/customer/address'
|
||||||
|
import type { OpenCommerceAPI } from '../../..'
|
||||||
|
|
||||||
|
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||||
|
import customerAddressEndpoint from '@vercel/commerce/api/endpoints/customer/address'
|
||||||
|
|
||||||
|
import getAddresses from './get-addresses'
|
||||||
|
import addItem from './add-item'
|
||||||
|
import updateItem from './update-item'
|
||||||
|
import removeItem from './remove-item'
|
||||||
|
|
||||||
|
export type CustomerAddressAPI = GetAPISchema<
|
||||||
|
OpenCommerceAPI,
|
||||||
|
CustomerAddressSchema<CustomerAddressTypes>
|
||||||
|
>
|
||||||
|
export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint']
|
||||||
|
|
||||||
|
export const handlers: CustomerAddressEndpoint['handlers'] = {
|
||||||
|
getAddresses,
|
||||||
|
addItem,
|
||||||
|
updateItem,
|
||||||
|
removeItem,
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerAddressApi = createEndpoint<CustomerAddressAPI>({
|
||||||
|
handler: customerAddressEndpoint,
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default customerAddressApi
|
@ -0,0 +1,9 @@
|
|||||||
|
import type { CustomerAddressEndpoint } from '.'
|
||||||
|
|
||||||
|
const removeItem: CustomerAddressEndpoint['handlers']['removeItem'] = async ({
|
||||||
|
res,
|
||||||
|
}) => {
|
||||||
|
return res.status(200).json({ data: null, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default removeItem
|
@ -0,0 +1,34 @@
|
|||||||
|
import selectFulfillmentOptions from '../../../mutations/select-fulfillment-options'
|
||||||
|
import type { CustomerAddressEndpoint } from '.'
|
||||||
|
|
||||||
|
const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = async ({
|
||||||
|
res,
|
||||||
|
body: { item, cartId },
|
||||||
|
config: { fetch, anonymousCartTokenCookie },
|
||||||
|
req: { cookies },
|
||||||
|
}) => {
|
||||||
|
// Return an error if no cart is present
|
||||||
|
if (!cartId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Cookie not found' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.shippingMethod) {
|
||||||
|
await fetch(selectFulfillmentOptions, {
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
cartId,
|
||||||
|
cartToken: cookies[anonymousCartTokenCookie],
|
||||||
|
fulfillmentGroupId: item.shippingMethod.fulfillmentGroupId,
|
||||||
|
fulfillmentMethodId: item.shippingMethod.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ data: null, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateItem
|
1
packages/opencommerce/src/api/endpoints/customer/card.ts
Normal file
1
packages/opencommerce/src/api/endpoints/customer/card.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
packages/opencommerce/src/api/endpoints/login/index.ts
Normal file
1
packages/opencommerce/src/api/endpoints/login/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
packages/opencommerce/src/api/endpoints/logout/index.ts
Normal file
1
packages/opencommerce/src/api/endpoints/logout/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
packages/opencommerce/src/api/endpoints/signup/index.ts
Normal file
1
packages/opencommerce/src/api/endpoints/signup/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
47
packages/opencommerce/src/api/index.ts
Normal file
47
packages/opencommerce/src/api/index.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
CommerceAPI,
|
||||||
|
CommerceAPIConfig,
|
||||||
|
getCommerceApi as commerceApi,
|
||||||
|
} from '@vercel/commerce/api'
|
||||||
|
import createFetchGraphqlApi from './utils/fetch-grapql-api'
|
||||||
|
|
||||||
|
import * as operations from './operations'
|
||||||
|
|
||||||
|
const API_URL = process.env.OPENCOMMERCE_STOREFRONT_API_URL
|
||||||
|
const SHOP_ID = process.env.OPENCOMMERCE_PRIMARY_SHOP_ID
|
||||||
|
|
||||||
|
if (!API_URL) {
|
||||||
|
throw new Error(
|
||||||
|
`The environment variable OPENCOMMERCE_STOREFRONT_API_URL is missing and it's required to access your store`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenCommerceConfig extends CommerceAPIConfig {
|
||||||
|
shopId: string
|
||||||
|
anonymousCartTokenCookie: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ONE_DAY = 60 * 60 * 24
|
||||||
|
|
||||||
|
const config: OpenCommerceConfig = {
|
||||||
|
commerceUrl: API_URL,
|
||||||
|
apiToken: '',
|
||||||
|
shopId: SHOP_ID ?? '',
|
||||||
|
customerCookie: 'opencommerce_customerToken',
|
||||||
|
cartCookie: 'opencommerce_cartId',
|
||||||
|
cartCookieMaxAge: ONE_DAY * 30,
|
||||||
|
anonymousCartTokenCookie: 'opencommerce_anonymousCartToken',
|
||||||
|
fetch: createFetchGraphqlApi(() => getCommerceApi().getConfig()),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const provider = { config, operations }
|
||||||
|
|
||||||
|
export type Provider = typeof provider
|
||||||
|
|
||||||
|
export type OpenCommerceAPI<P extends Provider = Provider> = CommerceAPI<P>
|
||||||
|
|
||||||
|
export function getCommerceApi<P extends Provider>(
|
||||||
|
customProvider: P = provider as any
|
||||||
|
): OpenCommerceAPI<P> {
|
||||||
|
return commerceApi(customProvider)
|
||||||
|
}
|
24
packages/opencommerce/src/api/mutations/add-cart-item.ts
Normal file
24
packages/opencommerce/src/api/mutations/add-cart-item.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
cartPayloadFragment,
|
||||||
|
incorrectPriceFailureDetailsFragment,
|
||||||
|
minOrderQuantityFailureDetailsFragment,
|
||||||
|
} from '../queries/get-cart-query'
|
||||||
|
|
||||||
|
const addCartItemsMutation = /* GraphQL */ `
|
||||||
|
mutation addCartItemsMutation($input: AddCartItemsInput!) {
|
||||||
|
addCartItems(input: $input) {
|
||||||
|
cart {
|
||||||
|
${cartPayloadFragment}
|
||||||
|
}
|
||||||
|
incorrectPriceFailures {
|
||||||
|
${incorrectPriceFailureDetailsFragment}
|
||||||
|
}
|
||||||
|
minOrderQuantityFailures {
|
||||||
|
${minOrderQuantityFailureDetailsFragment}
|
||||||
|
}
|
||||||
|
clientMutationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default addCartItemsMutation
|
@ -0,0 +1,13 @@
|
|||||||
|
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||||
|
|
||||||
|
const setShippingAddressOnCartMutation = `
|
||||||
|
mutation setShippingAddressOnCartMutation($input: SetShippingAddressOnCartInput!) {
|
||||||
|
setShippingAddressOnCart(input: $input) {
|
||||||
|
cart {
|
||||||
|
${cartPayloadFragment}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default setShippingAddressOnCartMutation
|
15
packages/opencommerce/src/api/mutations/authenticate.ts
Normal file
15
packages/opencommerce/src/api/mutations/authenticate.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const authenticateMutation = /* GraphQL */ `
|
||||||
|
mutation authenticate(
|
||||||
|
$serviceName: String!
|
||||||
|
$params: AuthenticateParamsInput!
|
||||||
|
) {
|
||||||
|
authenticate(serviceName: $serviceName, params: $params) {
|
||||||
|
sessionId
|
||||||
|
tokens {
|
||||||
|
refreshToken
|
||||||
|
accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export default authenticateMutation
|
24
packages/opencommerce/src/api/mutations/create-cart.ts
Normal file
24
packages/opencommerce/src/api/mutations/create-cart.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
cartPayloadFragment,
|
||||||
|
incorrectPriceFailureDetailsFragment,
|
||||||
|
minOrderQuantityFailureDetailsFragment,
|
||||||
|
} from '../queries/get-cart-query'
|
||||||
|
|
||||||
|
const createCartMutation = /* GraphQL */ `
|
||||||
|
mutation createCartMutation($input: CreateCartInput!) {
|
||||||
|
createCart(input: $input) {
|
||||||
|
cart {
|
||||||
|
${cartPayloadFragment}
|
||||||
|
}
|
||||||
|
incorrectPriceFailures {
|
||||||
|
${incorrectPriceFailureDetailsFragment}
|
||||||
|
}
|
||||||
|
minOrderQuantityFailures {
|
||||||
|
${minOrderQuantityFailureDetailsFragment}
|
||||||
|
}
|
||||||
|
clientMutationId
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export default createCartMutation
|
211
packages/opencommerce/src/api/mutations/place-order.ts
Normal file
211
packages/opencommerce/src/api/mutations/place-order.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
const orderCommon = `
|
||||||
|
_id
|
||||||
|
account {
|
||||||
|
_id
|
||||||
|
}
|
||||||
|
cartId
|
||||||
|
createdAt
|
||||||
|
displayStatus(language: $language)
|
||||||
|
email
|
||||||
|
fulfillmentGroups {
|
||||||
|
_id
|
||||||
|
data {
|
||||||
|
... on ShippingOrderFulfillmentGroupData {
|
||||||
|
shippingAddress {
|
||||||
|
_id
|
||||||
|
address1
|
||||||
|
address2
|
||||||
|
city
|
||||||
|
company
|
||||||
|
country
|
||||||
|
fullName
|
||||||
|
isCommercial
|
||||||
|
isShippingDefault
|
||||||
|
phone
|
||||||
|
postal
|
||||||
|
region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items {
|
||||||
|
nodes {
|
||||||
|
_id
|
||||||
|
addedAt
|
||||||
|
createdAt
|
||||||
|
imageURLs {
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
original
|
||||||
|
small
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
|
isTaxable
|
||||||
|
optionTitle
|
||||||
|
parcel {
|
||||||
|
containers
|
||||||
|
distanceUnit
|
||||||
|
height
|
||||||
|
length
|
||||||
|
massUnit
|
||||||
|
weight
|
||||||
|
width
|
||||||
|
}
|
||||||
|
price {
|
||||||
|
amount
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
productConfiguration {
|
||||||
|
productId
|
||||||
|
productVariantId
|
||||||
|
}
|
||||||
|
productSlug
|
||||||
|
productType
|
||||||
|
productVendor
|
||||||
|
productTags {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quantity
|
||||||
|
shop {
|
||||||
|
_id
|
||||||
|
}
|
||||||
|
subtotal {
|
||||||
|
amount
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
taxCode
|
||||||
|
title
|
||||||
|
updatedAt
|
||||||
|
variantTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedFulfillmentOption {
|
||||||
|
fulfillmentMethod {
|
||||||
|
_id
|
||||||
|
carrier
|
||||||
|
displayName
|
||||||
|
fulfillmentTypes
|
||||||
|
group
|
||||||
|
name
|
||||||
|
}
|
||||||
|
handlingPrice {
|
||||||
|
amount
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
price {
|
||||||
|
amount
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shop {
|
||||||
|
_id
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
fulfillmentTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
itemTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
surchargeTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
taxTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
total {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracking
|
||||||
|
type
|
||||||
|
}
|
||||||
|
payments {
|
||||||
|
_id
|
||||||
|
amount {
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
billingAddress {
|
||||||
|
address1
|
||||||
|
address2
|
||||||
|
city
|
||||||
|
company
|
||||||
|
country
|
||||||
|
fullName
|
||||||
|
isCommercial
|
||||||
|
phone
|
||||||
|
postal
|
||||||
|
region
|
||||||
|
}
|
||||||
|
displayName
|
||||||
|
method {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
referenceId
|
||||||
|
shop {
|
||||||
|
_id
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status
|
||||||
|
summary {
|
||||||
|
fulfillmentTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
itemTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
surchargeTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
taxTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
total {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalItemQuantity
|
||||||
|
updatedAt
|
||||||
|
`
|
||||||
|
|
||||||
|
const placeOrder = /* GraphQL */ `
|
||||||
|
mutation placeOrderMutation(
|
||||||
|
$input: PlaceOrderInput!
|
||||||
|
$language: String! = "en"
|
||||||
|
) {
|
||||||
|
placeOrder(input: $input) {
|
||||||
|
orders {
|
||||||
|
${orderCommon}
|
||||||
|
}
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default placeOrder
|
13
packages/opencommerce/src/api/mutations/remove-cart-item.ts
Normal file
13
packages/opencommerce/src/api/mutations/remove-cart-item.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||||
|
|
||||||
|
const removeCartItemsMutation = `
|
||||||
|
mutation removeCartItemsMutation($input: RemoveCartItemsInput!) {
|
||||||
|
removeCartItems(input: $input) {
|
||||||
|
cart {
|
||||||
|
${cartPayloadFragment}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default removeCartItemsMutation
|
@ -0,0 +1,14 @@
|
|||||||
|
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||||
|
|
||||||
|
const selectFulfillmentOptions = /* GraphQL */ `
|
||||||
|
mutation setFulfillmentOptionCartMutation(
|
||||||
|
$input: SelectFulfillmentOptionForGroupInput!
|
||||||
|
) {
|
||||||
|
selectFulfillmentOptionForGroup(input: $input) {
|
||||||
|
cart {
|
||||||
|
${cartPayloadFragment}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export default selectFulfillmentOptions
|
@ -0,0 +1,15 @@
|
|||||||
|
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||||
|
|
||||||
|
const setEmailOnAnonymousCart = /* GraphQL */ `
|
||||||
|
mutation setEmailOnAnonymousCartMutation(
|
||||||
|
$input: SetEmailOnAnonymousCartInput!
|
||||||
|
) {
|
||||||
|
setEmailOnAnonymousCart(input: $input) {
|
||||||
|
cart {
|
||||||
|
${cartPayloadFragment}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default setEmailOnAnonymousCart
|
@ -0,0 +1,13 @@
|
|||||||
|
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||||
|
|
||||||
|
const updateCartItemsQuantityMutation = /* GraphQL */ `
|
||||||
|
mutation updateCartItemsQuantity($updateCartItemsQuantityInput: UpdateCartItemsQuantityInput!) {
|
||||||
|
updateCartItemsQuantity(input: $updateCartItemsQuantityInput) {
|
||||||
|
cart {
|
||||||
|
${cartPayloadFragment}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default updateCartItemsQuantityMutation
|
@ -0,0 +1,15 @@
|
|||||||
|
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||||
|
|
||||||
|
const updateFulfillmentOptions = /* GraphQL */ `
|
||||||
|
mutation updateFulfillmentOptionsForGroup(
|
||||||
|
$input: UpdateFulfillmentOptionsForGroupInput!
|
||||||
|
) {
|
||||||
|
updateFulfillmentOptionsForGroup(input: $input) {
|
||||||
|
cart {
|
||||||
|
${cartPayloadFragment}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default updateFulfillmentOptions
|
47
packages/opencommerce/src/api/operations/get-all-pages.ts
Normal file
47
packages/opencommerce/src/api/operations/get-all-pages.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import type {
|
||||||
|
OperationContext,
|
||||||
|
OperationOptions,
|
||||||
|
} from '@vercel/commerce/api/operations'
|
||||||
|
import { GetAllPagesOperation } from '../../types/page'
|
||||||
|
|
||||||
|
import type { OpenCommerceConfig, Provider } from '../index'
|
||||||
|
|
||||||
|
export type Page = { url: string }
|
||||||
|
export type GetAllPagesResult = { pages: Page[] }
|
||||||
|
|
||||||
|
export default function getAllPagesOperation({
|
||||||
|
commerce,
|
||||||
|
}: OperationContext<Provider>) {
|
||||||
|
async function getAllPages<T extends GetAllPagesOperation>(opts?: {
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
}): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getAllPages<T extends GetAllPagesOperation>(
|
||||||
|
opts: {
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
} & OperationOptions
|
||||||
|
): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getAllPages<T extends GetAllPagesOperation>(
|
||||||
|
opts: {
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
} & OperationOptions
|
||||||
|
): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getAllPages<T extends GetAllPagesOperation>({
|
||||||
|
config,
|
||||||
|
preview,
|
||||||
|
}: {
|
||||||
|
url?: string
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
} = {}): Promise<GetAllPagesResult> {
|
||||||
|
return Promise.resolve({
|
||||||
|
pages: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return getAllPages
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
import type {
|
||||||
|
OperationContext,
|
||||||
|
OperationOptions,
|
||||||
|
} from '@vercel/commerce/api/operations'
|
||||||
|
import type {
|
||||||
|
CatalogItemsQuery,
|
||||||
|
CatalogItemsQueryVariables,
|
||||||
|
CatalogItemProduct,
|
||||||
|
} from '../../../schema'
|
||||||
|
import type { GetAllProductPathsOperation } from '../../types/product'
|
||||||
|
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||||
|
import filterEdges from '../utils/filter-edges'
|
||||||
|
import { OpenCommerceConfig, Provider } from '..'
|
||||||
|
import getAllProductPathsQuery from '../queries/get-all-product-paths-query'
|
||||||
|
|
||||||
|
export default function getAllProductPathsOperation({
|
||||||
|
commerce,
|
||||||
|
}: OperationContext<Provider>) {
|
||||||
|
async function getAllProductPaths<
|
||||||
|
T extends GetAllProductPathsOperation
|
||||||
|
>(opts?: {
|
||||||
|
variables?: CatalogItemsQueryVariables
|
||||||
|
config?: OpenCommerceConfig
|
||||||
|
}): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
|
||||||
|
opts: {
|
||||||
|
variables?: CatalogItemsQueryVariables
|
||||||
|
config?: OpenCommerceConfig
|
||||||
|
} & OperationOptions
|
||||||
|
): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
|
||||||
|
query = getAllProductPathsQuery,
|
||||||
|
variables,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
query?: string
|
||||||
|
variables?: CatalogItemsQueryVariables
|
||||||
|
config?: OpenCommerceConfig
|
||||||
|
} = {}): Promise<T['data']> {
|
||||||
|
const { fetch, shopId } = commerce.getConfig(config)
|
||||||
|
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||||
|
// required in case there's a custom `query`
|
||||||
|
const { data } = await fetch<
|
||||||
|
RecursivePartial<CatalogItemsQuery>,
|
||||||
|
CatalogItemsQueryVariables
|
||||||
|
>(query, {
|
||||||
|
variables: { ...variables, shopIds: [shopId] },
|
||||||
|
})
|
||||||
|
|
||||||
|
const products = data.catalogItems?.edges
|
||||||
|
|
||||||
|
return {
|
||||||
|
products: filterEdges(products as RecursiveRequired<typeof products>).map(
|
||||||
|
({ node }) => ({
|
||||||
|
path: `/${(node as CatalogItemProduct).product!.slug}`,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getAllProductPaths
|
||||||
|
}
|
62
packages/opencommerce/src/api/operations/get-all-products.ts
Normal file
62
packages/opencommerce/src/api/operations/get-all-products.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import type { OperationContext } from '@vercel/commerce/api/operations'
|
||||||
|
import type { GetAllProductsOperation } from '../../types/product'
|
||||||
|
import {
|
||||||
|
CatalogItemProduct,
|
||||||
|
CatalogItemsQuery,
|
||||||
|
CatalogItemsQueryVariables,
|
||||||
|
} from '../../../schema'
|
||||||
|
import catalogItemsQuery from '../queries/get-all-products-query'
|
||||||
|
import type { OpenCommerceConfig, Provider } from '..'
|
||||||
|
import { normalizeProduct } from '../../utils/normalize'
|
||||||
|
import { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||||
|
import filterEdges from '../utils/filter-edges'
|
||||||
|
|
||||||
|
export default function getAllProductsOperation({
|
||||||
|
commerce,
|
||||||
|
}: OperationContext<Provider>) {
|
||||||
|
async function getAllProducts<T extends GetAllProductsOperation>(opts?: {
|
||||||
|
variables?: Omit<CatalogItemsQueryVariables, 'shopIds'>
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
}): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getAllProducts<T extends GetAllProductsOperation>({
|
||||||
|
query = catalogItemsQuery,
|
||||||
|
variables,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
query?: string
|
||||||
|
variables?: Omit<CatalogItemsQueryVariables, 'shopIds'>
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
} = {}): Promise<T['data']> {
|
||||||
|
const { fetch, locale, shopId } = commerce.getConfig(config)
|
||||||
|
|
||||||
|
const { data } = await fetch<
|
||||||
|
RecursivePartial<CatalogItemsQuery>,
|
||||||
|
CatalogItemsQueryVariables
|
||||||
|
>(
|
||||||
|
query,
|
||||||
|
{ variables: { ...variables, shopIds: [shopId] } },
|
||||||
|
{
|
||||||
|
...(locale && {
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': locale,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const edges = data.catalogItems?.edges
|
||||||
|
return {
|
||||||
|
products: filterEdges(edges as RecursiveRequired<typeof edges>).map(
|
||||||
|
(item) =>
|
||||||
|
normalizeProduct(
|
||||||
|
item?.node ? (item.node as CatalogItemProduct) : null
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAllProducts
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
export default function getCustomerWishlistOperation() {
|
||||||
|
function getCustomerWishlist(): any {
|
||||||
|
return { wishlist: {} }
|
||||||
|
}
|
||||||
|
return getCustomerWishlist
|
||||||
|
}
|
32
packages/opencommerce/src/api/operations/get-page.ts
Normal file
32
packages/opencommerce/src/api/operations/get-page.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type {
|
||||||
|
OperationContext,
|
||||||
|
OperationOptions,
|
||||||
|
} from '@vercel/commerce/api/operations'
|
||||||
|
import { GetPageOperation } from '../../types/page'
|
||||||
|
import { Provider, OpenCommerceConfig } from '..'
|
||||||
|
|
||||||
|
type Page = any
|
||||||
|
type GetPageResult = { page?: Page }
|
||||||
|
|
||||||
|
export default function getPageOperation({
|
||||||
|
commerce,
|
||||||
|
}: OperationContext<Provider>) {
|
||||||
|
async function getPage<T extends GetPageOperation>(opts: {
|
||||||
|
variables: T['variables']
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
}): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getPage<T extends GetPageOperation>(
|
||||||
|
opts: {
|
||||||
|
variables: T['variables']
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
} & OperationOptions
|
||||||
|
): Promise<T['data']>
|
||||||
|
|
||||||
|
function getPage(): Promise<GetPageResult> {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
return getPage
|
||||||
|
}
|
68
packages/opencommerce/src/api/operations/get-product.ts
Normal file
68
packages/opencommerce/src/api/operations/get-product.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import type {
|
||||||
|
OperationContext,
|
||||||
|
OperationOptions,
|
||||||
|
} from '@vercel/commerce/api/operations'
|
||||||
|
import { GetProductOperation } from '../../types/product'
|
||||||
|
import { normalizeProduct } from '../../utils/normalize'
|
||||||
|
import type { OpenCommerceConfig, Provider } from '..'
|
||||||
|
import {
|
||||||
|
CatalogItemProduct,
|
||||||
|
GetProductBySlugQuery,
|
||||||
|
GetProductBySlugQueryVariables,
|
||||||
|
} from '../../../schema'
|
||||||
|
import getProductQuery from '../queries/get-product-query'
|
||||||
|
|
||||||
|
export default function getProductOperation({
|
||||||
|
commerce,
|
||||||
|
}: OperationContext<Provider>) {
|
||||||
|
async function getProduct<T extends GetProductOperation>(opts: {
|
||||||
|
variables: GetProductBySlugQueryVariables
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
}): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getProduct<T extends GetProductOperation>(
|
||||||
|
opts: {
|
||||||
|
variables: GetProductBySlugQueryVariables
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
} & OperationOptions
|
||||||
|
): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getProduct<T extends GetProductOperation>({
|
||||||
|
query = getProductQuery,
|
||||||
|
variables,
|
||||||
|
config: cfg,
|
||||||
|
}: {
|
||||||
|
query?: string
|
||||||
|
variables: GetProductBySlugQueryVariables
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
}): Promise<T['data']> {
|
||||||
|
const { fetch, locale } = commerce.getConfig(cfg)
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { catalogItemProduct },
|
||||||
|
} = await fetch<GetProductBySlugQuery, GetProductBySlugQueryVariables>(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
variables,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...(locale && {
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': locale,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(catalogItemProduct && {
|
||||||
|
product: normalizeProduct(catalogItemProduct as CatalogItemProduct),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getProduct
|
||||||
|
}
|
78
packages/opencommerce/src/api/operations/get-site-info.ts
Normal file
78
packages/opencommerce/src/api/operations/get-site-info.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
OperationContext,
|
||||||
|
OperationOptions,
|
||||||
|
} from '@vercel/commerce/api/operations'
|
||||||
|
import {
|
||||||
|
GetTagsQuery,
|
||||||
|
GetAllProductVendorsQuery,
|
||||||
|
GetTagsQueryVariables,
|
||||||
|
GetAllProductVendorsQueryVariables,
|
||||||
|
} from '../../../schema'
|
||||||
|
import getTagsQuery from '../queries/get-tags-query'
|
||||||
|
import { GetSiteInfoOperation, OCCategory, SiteTypes } from '../../types/site'
|
||||||
|
import {
|
||||||
|
normalizeCategory,
|
||||||
|
normalizeNavigation,
|
||||||
|
normalizeVendors,
|
||||||
|
} from '../../utils/normalize'
|
||||||
|
import type { OpenCommerceConfig, Provider } from '..'
|
||||||
|
import filterEdges from '../utils/filter-edges'
|
||||||
|
import getAllProductVendors from '../queries/get-vendors-query'
|
||||||
|
import getPrimaryShopQuery from '../queries/get-primary-shop-query'
|
||||||
|
|
||||||
|
export default function getSiteInfoOperation({
|
||||||
|
commerce,
|
||||||
|
}: OperationContext<Provider>) {
|
||||||
|
async function getSiteInfo<T extends GetSiteInfoOperation>(opts?: {
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
}): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getSiteInfo<T extends GetSiteInfoOperation>(
|
||||||
|
opts: {
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
} & OperationOptions
|
||||||
|
): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getSiteInfo<T extends GetSiteInfoOperation>({
|
||||||
|
config: cfg,
|
||||||
|
}: {
|
||||||
|
query?: string
|
||||||
|
config?: Partial<OpenCommerceConfig>
|
||||||
|
preview?: boolean
|
||||||
|
} = {}): Promise<T['data']> {
|
||||||
|
const { fetch, shopId } = commerce.getConfig(cfg)
|
||||||
|
|
||||||
|
const [categoriesResponse, vendorsResponse, primaryShopResponse] =
|
||||||
|
await Promise.all([
|
||||||
|
await fetch<GetTagsQuery, GetTagsQueryVariables>(getTagsQuery, {
|
||||||
|
variables: { first: 250, shopId },
|
||||||
|
}),
|
||||||
|
await fetch<
|
||||||
|
GetAllProductVendorsQuery,
|
||||||
|
GetAllProductVendorsQueryVariables
|
||||||
|
>(getAllProductVendors, { variables: { shopIds: [shopId] } }),
|
||||||
|
await fetch(getPrimaryShopQuery),
|
||||||
|
])
|
||||||
|
|
||||||
|
const categories = filterEdges(categoriesResponse.data.tags?.edges).map(
|
||||||
|
(edge) => normalizeCategory(edge.node! as OCCategory)
|
||||||
|
)
|
||||||
|
|
||||||
|
const brands = [
|
||||||
|
...new Set(filterEdges(vendorsResponse.data.vendors?.nodes)),
|
||||||
|
].map(normalizeVendors)
|
||||||
|
|
||||||
|
const navigationItems =
|
||||||
|
primaryShopResponse.data.primaryShop.defaultNavigationTree.items ?? []
|
||||||
|
|
||||||
|
return {
|
||||||
|
categories,
|
||||||
|
brands,
|
||||||
|
navigation: normalizeNavigation(navigationItems),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSiteInfo
|
||||||
|
}
|
7
packages/opencommerce/src/api/operations/index.ts
Normal file
7
packages/opencommerce/src/api/operations/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export { default as getAllPages } from './get-all-pages'
|
||||||
|
export { default as getPage } from './get-page'
|
||||||
|
export { default as getAllProducts } from './get-all-products'
|
||||||
|
export { default as getAllProductPaths } from './get-all-product-paths'
|
||||||
|
export { default as getProduct } from './get-product'
|
||||||
|
export { default as getSiteInfo } from './get-site-info'
|
||||||
|
export { default as login } from './login'
|
53
packages/opencommerce/src/api/operations/login.ts
Normal file
53
packages/opencommerce/src/api/operations/login.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { ServerResponse } from 'http'
|
||||||
|
import type {
|
||||||
|
OperationContext,
|
||||||
|
OperationOptions,
|
||||||
|
} from '@vercel/commerce/api/operations'
|
||||||
|
import type { LoginOperation } from '../../types/login'
|
||||||
|
import type { AuthenticateMutation } from '../../../schema'
|
||||||
|
import type { RecursivePartial } from '../utils/types'
|
||||||
|
import loginMutation from '../mutations/authenticate'
|
||||||
|
import type { OpenCommerceConfig, Provider } from '..'
|
||||||
|
|
||||||
|
export default function loginOperation({
|
||||||
|
commerce,
|
||||||
|
}: OperationContext<Provider>) {
|
||||||
|
async function login<T extends LoginOperation>(opts: {
|
||||||
|
variables: T['variables']
|
||||||
|
config?: OpenCommerceConfig
|
||||||
|
res: ServerResponse
|
||||||
|
}): Promise<T['data']>
|
||||||
|
|
||||||
|
async function login<T extends LoginOperation>(
|
||||||
|
opts: {
|
||||||
|
variables: T['variables']
|
||||||
|
config?: OpenCommerceConfig
|
||||||
|
res: ServerResponse
|
||||||
|
} & OperationOptions
|
||||||
|
): Promise<T['data']>
|
||||||
|
|
||||||
|
async function login<T extends LoginOperation>({
|
||||||
|
query = loginMutation,
|
||||||
|
variables,
|
||||||
|
res: response,
|
||||||
|
config: cfg,
|
||||||
|
}: {
|
||||||
|
query?: string
|
||||||
|
variables: T['variables']
|
||||||
|
res: ServerResponse
|
||||||
|
config?: OpenCommerceConfig
|
||||||
|
}): Promise<T['data']> {
|
||||||
|
const { fetch } = commerce.getConfig(cfg)
|
||||||
|
|
||||||
|
const { data } = await fetch<RecursivePartial<AuthenticateMutation>>(
|
||||||
|
query,
|
||||||
|
{ variables }
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: data.authenticate?.tokens?.accessToken ?? undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return login
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
const getAllProductsPathsQuery = `
|
||||||
|
query catalogItems(
|
||||||
|
$first: ConnectionLimitInt = 250
|
||||||
|
$sortBy: CatalogItemSortByField = updatedAt
|
||||||
|
$shopIds: [ID]!
|
||||||
|
) {
|
||||||
|
catalogItems(first: $first, sortBy: $sortBy, shopIds: $shopIds) {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
hasPreviousPage
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
... on CatalogItemProduct {
|
||||||
|
product {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export default getAllProductsPathsQuery
|
@ -0,0 +1,66 @@
|
|||||||
|
const catalogItemsQuery = /* GraphQL */ `
|
||||||
|
query catalogItems(
|
||||||
|
$first: ConnectionLimitInt = 250
|
||||||
|
$sortBy: CatalogItemSortByField = updatedAt
|
||||||
|
$sortOrder: SortOrder = desc
|
||||||
|
$sortByPriceCurrencyCode: String
|
||||||
|
$tagIds: [ID]
|
||||||
|
$shopIds: [ID]!
|
||||||
|
$searchQuery: String
|
||||||
|
) {
|
||||||
|
catalogItems(
|
||||||
|
first: $first
|
||||||
|
sortBy: $sortBy
|
||||||
|
sortOrder: $sortOrder
|
||||||
|
tagIds: $tagIds
|
||||||
|
shopIds: $shopIds
|
||||||
|
searchQuery: $searchQuery
|
||||||
|
sortByPriceCurrencyCode: $sortByPriceCurrencyCode
|
||||||
|
) {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
hasPreviousPage
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
_id
|
||||||
|
... on CatalogItemProduct {
|
||||||
|
product {
|
||||||
|
_id
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
description
|
||||||
|
vendor
|
||||||
|
isLowQuantity
|
||||||
|
isSoldOut
|
||||||
|
isBackorder
|
||||||
|
shop {
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pricing {
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayPrice
|
||||||
|
minPrice
|
||||||
|
maxPrice
|
||||||
|
}
|
||||||
|
media {
|
||||||
|
URLs {
|
||||||
|
thumbnail
|
||||||
|
small
|
||||||
|
medium
|
||||||
|
large
|
||||||
|
original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export default catalogItemsQuery
|
11
packages/opencommerce/src/api/queries/get-anonymous-cart.ts
Normal file
11
packages/opencommerce/src/api/queries/get-anonymous-cart.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { cartQueryFragment } from './get-cart-query'
|
||||||
|
|
||||||
|
export const getAnonymousCart = `
|
||||||
|
query anonymousCartByCartIdQuery($cartId: ID!, $cartToken: String!, $itemsAfterCursor: ConnectionCursor) {
|
||||||
|
cart: anonymousCartByCartId(cartId: $cartId, cartToken: $cartToken) {
|
||||||
|
${cartQueryFragment}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default getAnonymousCart
|
@ -0,0 +1,11 @@
|
|||||||
|
import { cartQueryFragment } from './get-cart-query'
|
||||||
|
|
||||||
|
const accountCartByAccountIdQuery = `
|
||||||
|
query accountCartByAccountIdQuery($accountId: ID!, $shopId: ID!, $itemsAfterCursor: ConnectionCursor) {
|
||||||
|
cart: accountCartByAccountId(accountId: $accountId, shopId: $shopId) {
|
||||||
|
${cartQueryFragment}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default accountCartByAccountIdQuery
|
241
packages/opencommerce/src/api/queries/get-cart-query.ts
Normal file
241
packages/opencommerce/src/api/queries/get-cart-query.ts
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
export const cartCommon = `
|
||||||
|
_id
|
||||||
|
createdAt
|
||||||
|
account {
|
||||||
|
_id
|
||||||
|
emailRecords {
|
||||||
|
address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shop {
|
||||||
|
_id
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
email
|
||||||
|
updatedAt
|
||||||
|
expiresAt
|
||||||
|
checkout {
|
||||||
|
fulfillmentGroups {
|
||||||
|
_id
|
||||||
|
type
|
||||||
|
data {
|
||||||
|
shippingAddress {
|
||||||
|
address1
|
||||||
|
address2
|
||||||
|
city
|
||||||
|
company
|
||||||
|
country
|
||||||
|
fullName
|
||||||
|
isBillingDefault
|
||||||
|
isCommercial
|
||||||
|
isShippingDefault
|
||||||
|
phone
|
||||||
|
postal
|
||||||
|
region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
availableFulfillmentOptions {
|
||||||
|
price {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
fulfillmentMethod {
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedFulfillmentOption {
|
||||||
|
fulfillmentMethod {
|
||||||
|
_id
|
||||||
|
name
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
price {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
handlingPrice {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shop {
|
||||||
|
_id
|
||||||
|
}
|
||||||
|
shippingAddress {
|
||||||
|
address1
|
||||||
|
address2
|
||||||
|
city
|
||||||
|
company
|
||||||
|
country
|
||||||
|
fullName
|
||||||
|
isBillingDefault
|
||||||
|
isCommercial
|
||||||
|
isShippingDefault
|
||||||
|
phone
|
||||||
|
postal
|
||||||
|
region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
fulfillmentTotal {
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
itemTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
surchargeTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
taxTotal {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
total {
|
||||||
|
amount
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalItemQuantity
|
||||||
|
`
|
||||||
|
|
||||||
|
const cartItemConnectionFragment = `
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
_id
|
||||||
|
productConfiguration {
|
||||||
|
productId
|
||||||
|
productVariantId
|
||||||
|
}
|
||||||
|
addedAt
|
||||||
|
attributes {
|
||||||
|
label
|
||||||
|
value
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
isBackorder
|
||||||
|
isLowQuantity
|
||||||
|
isSoldOut
|
||||||
|
imageURLs {
|
||||||
|
large
|
||||||
|
small
|
||||||
|
original
|
||||||
|
medium
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
|
metafields {
|
||||||
|
value
|
||||||
|
key
|
||||||
|
}
|
||||||
|
parcel {
|
||||||
|
length
|
||||||
|
width
|
||||||
|
weight
|
||||||
|
height
|
||||||
|
}
|
||||||
|
price {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
priceWhenAdded {
|
||||||
|
amount
|
||||||
|
displayAmount
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
productSlug
|
||||||
|
productType
|
||||||
|
quantity
|
||||||
|
shop {
|
||||||
|
_id
|
||||||
|
}
|
||||||
|
subtotal {
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
title
|
||||||
|
productTags {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
productVendor
|
||||||
|
variantTitle
|
||||||
|
optionTitle
|
||||||
|
updatedAt
|
||||||
|
inventoryAvailableToSell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const cartPayloadFragment = `
|
||||||
|
${cartCommon}
|
||||||
|
items {
|
||||||
|
${cartItemConnectionFragment}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const incorrectPriceFailureDetailsFragment = `
|
||||||
|
currentPrice {
|
||||||
|
amount
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
productConfiguration {
|
||||||
|
productId
|
||||||
|
productVariantId
|
||||||
|
}
|
||||||
|
providedPrice {
|
||||||
|
amount
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const minOrderQuantityFailureDetailsFragment = `
|
||||||
|
minOrderQuantity
|
||||||
|
productConfiguration {
|
||||||
|
productId
|
||||||
|
productVariantId
|
||||||
|
}
|
||||||
|
quantity
|
||||||
|
`
|
||||||
|
|
||||||
|
const getCartQuery = /* GraphQL */ `
|
||||||
|
query($checkoutId: ID!) {
|
||||||
|
node(id: $checkoutId) {
|
||||||
|
... on Checkout {
|
||||||
|
${cartCommon}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const cartQueryFragment = `
|
||||||
|
${cartCommon}
|
||||||
|
items(first: 20, after: $itemsAfterCursor) {
|
||||||
|
${cartItemConnectionFragment}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default getCartQuery
|
@ -0,0 +1,50 @@
|
|||||||
|
const getPrimaryShopQuery = /* GraphQL */ `
|
||||||
|
query primaryShop($language: String! = "en") {
|
||||||
|
primaryShop {
|
||||||
|
_id
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
defaultNavigationTree(language: $language) {
|
||||||
|
...NavigationTreeFragment
|
||||||
|
}
|
||||||
|
description
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment NavigationTreeFragment on NavigationTree {
|
||||||
|
_id
|
||||||
|
shopId
|
||||||
|
name
|
||||||
|
items {
|
||||||
|
navigationItem {
|
||||||
|
data {
|
||||||
|
...NavigationItemFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items {
|
||||||
|
navigationItem {
|
||||||
|
data {
|
||||||
|
...NavigationItemFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items {
|
||||||
|
navigationItem {
|
||||||
|
data {
|
||||||
|
...NavigationItemFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment NavigationItemFields on NavigationItemData {
|
||||||
|
contentForLanguage
|
||||||
|
classNames
|
||||||
|
url
|
||||||
|
isUrlRelative
|
||||||
|
shouldOpenInNewWindow
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default getPrimaryShopQuery
|
181
packages/opencommerce/src/api/queries/get-product-query.ts
Normal file
181
packages/opencommerce/src/api/queries/get-product-query.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
const getProductQuery = /* GraphQL */ `
|
||||||
|
query getProductBySlug($slug: String!) {
|
||||||
|
catalogItemProduct(slugOrId: $slug) {
|
||||||
|
product {
|
||||||
|
_id
|
||||||
|
productId
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
description
|
||||||
|
vendor
|
||||||
|
isLowQuantity
|
||||||
|
isSoldOut
|
||||||
|
isBackorder
|
||||||
|
metafields {
|
||||||
|
description
|
||||||
|
key
|
||||||
|
namespace
|
||||||
|
scope
|
||||||
|
value
|
||||||
|
valueType
|
||||||
|
}
|
||||||
|
pricing {
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayPrice
|
||||||
|
minPrice
|
||||||
|
maxPrice
|
||||||
|
}
|
||||||
|
shop {
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primaryImage {
|
||||||
|
URLs {
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
original
|
||||||
|
small
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
|
priority
|
||||||
|
productId
|
||||||
|
variantId
|
||||||
|
}
|
||||||
|
media {
|
||||||
|
priority
|
||||||
|
productId
|
||||||
|
variantId
|
||||||
|
URLs {
|
||||||
|
thumbnail
|
||||||
|
small
|
||||||
|
medium
|
||||||
|
large
|
||||||
|
original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
variants {
|
||||||
|
_id
|
||||||
|
variantId
|
||||||
|
attributeLabel
|
||||||
|
title
|
||||||
|
optionTitle
|
||||||
|
index
|
||||||
|
pricing {
|
||||||
|
compareAtPrice {
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
price
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayPrice
|
||||||
|
}
|
||||||
|
canBackorder
|
||||||
|
inventoryAvailableToSell
|
||||||
|
isBackorder
|
||||||
|
isSoldOut
|
||||||
|
isLowQuantity
|
||||||
|
options {
|
||||||
|
_id
|
||||||
|
variantId
|
||||||
|
attributeLabel
|
||||||
|
title
|
||||||
|
index
|
||||||
|
pricing {
|
||||||
|
compareAtPrice {
|
||||||
|
displayAmount
|
||||||
|
}
|
||||||
|
price
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
displayPrice
|
||||||
|
}
|
||||||
|
optionTitle
|
||||||
|
canBackorder
|
||||||
|
inventoryAvailableToSell
|
||||||
|
isBackorder
|
||||||
|
isSoldOut
|
||||||
|
isLowQuantity
|
||||||
|
media {
|
||||||
|
priority
|
||||||
|
productId
|
||||||
|
variantId
|
||||||
|
URLs {
|
||||||
|
thumbnail
|
||||||
|
small
|
||||||
|
medium
|
||||||
|
large
|
||||||
|
original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metafields {
|
||||||
|
description
|
||||||
|
key
|
||||||
|
namespace
|
||||||
|
scope
|
||||||
|
value
|
||||||
|
valueType
|
||||||
|
}
|
||||||
|
primaryImage {
|
||||||
|
URLs {
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
original
|
||||||
|
small
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
|
priority
|
||||||
|
productId
|
||||||
|
variantId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
media {
|
||||||
|
priority
|
||||||
|
productId
|
||||||
|
variantId
|
||||||
|
URLs {
|
||||||
|
thumbnail
|
||||||
|
small
|
||||||
|
medium
|
||||||
|
large
|
||||||
|
original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metafields {
|
||||||
|
description
|
||||||
|
key
|
||||||
|
namespace
|
||||||
|
scope
|
||||||
|
value
|
||||||
|
valueType
|
||||||
|
}
|
||||||
|
primaryImage {
|
||||||
|
URLs {
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
original
|
||||||
|
small
|
||||||
|
thumbnail
|
||||||
|
}
|
||||||
|
priority
|
||||||
|
productId
|
||||||
|
variantId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default getProductQuery
|
@ -0,0 +1,11 @@
|
|||||||
|
const getShopCurrencyQuery = /* GraphQL */ `
|
||||||
|
query getShopCurrency($id: ID!) {
|
||||||
|
shop(id: $id) {
|
||||||
|
currency {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default getShopCurrencyQuery
|
14
packages/opencommerce/src/api/queries/get-tags-query.ts
Normal file
14
packages/opencommerce/src/api/queries/get-tags-query.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const getTagsQuery = /* GraphQL */ `
|
||||||
|
query getTags($first: ConnectionLimitInt!, $shopId: ID!) {
|
||||||
|
tags(first: $first, shopId: $shopId) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
_id
|
||||||
|
displayTitle
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export default getTagsQuery
|
10
packages/opencommerce/src/api/queries/get-vendors-query.ts
Normal file
10
packages/opencommerce/src/api/queries/get-vendors-query.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const getAllProductVendors = /* GraphQL */ `
|
||||||
|
query getAllProductVendors($shopIds: [ID]!) {
|
||||||
|
vendors(shopIds: $shopIds) {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export default getAllProductVendors
|
39
packages/opencommerce/src/api/utils/fetch-grapql-api.ts
Normal file
39
packages/opencommerce/src/api/utils/fetch-grapql-api.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||||
|
import type { GraphQLFetcher } from '@vercel/commerce/api'
|
||||||
|
import type { OpenCommerceConfig } from '../index'
|
||||||
|
import fetch from './fetch'
|
||||||
|
|
||||||
|
const fetchGraphqlApi: (
|
||||||
|
getConfig: () => OpenCommerceConfig
|
||||||
|
) => GraphQLFetcher =
|
||||||
|
(getConfig) =>
|
||||||
|
async (query: string, { variables, preview } = {}, fetchOptions) => {
|
||||||
|
// log.warn(query)
|
||||||
|
const config = getConfig()
|
||||||
|
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
||||||
|
...fetchOptions,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...fetchOptions?.headers,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.errors) {
|
||||||
|
throw new FetcherError({
|
||||||
|
errors: json.errors ?? [
|
||||||
|
{ message: 'Failed to fetch OpenCommerce API' },
|
||||||
|
],
|
||||||
|
status: res.status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: json.data, res }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default fetchGraphqlApi
|
3
packages/opencommerce/src/api/utils/fetch.ts
Normal file
3
packages/opencommerce/src/api/utils/fetch.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import vercelFetch from '@vercel/fetch'
|
||||||
|
|
||||||
|
export default vercelFetch()
|
5
packages/opencommerce/src/api/utils/filter-edges.ts
Normal file
5
packages/opencommerce/src/api/utils/filter-edges.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default function filterEdges<T>(
|
||||||
|
edges: (T | null | undefined)[] | null | undefined
|
||||||
|
) {
|
||||||
|
return edges?.filter((edge): edge is T => !!edge) ?? []
|
||||||
|
}
|
20
packages/opencommerce/src/api/utils/get-cart-cookie.ts
Normal file
20
packages/opencommerce/src/api/utils/get-cart-cookie.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { serialize, CookieSerializeOptions } from 'cookie'
|
||||||
|
|
||||||
|
export default function getCartCookie(
|
||||||
|
name: string,
|
||||||
|
cartId?: string,
|
||||||
|
maxAge?: number
|
||||||
|
) {
|
||||||
|
const options: CookieSerializeOptions =
|
||||||
|
cartId && maxAge
|
||||||
|
? {
|
||||||
|
maxAge,
|
||||||
|
expires: new Date(Date.now() + maxAge * 1000),
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'lax',
|
||||||
|
}
|
||||||
|
: { maxAge: -1, path: '/' } // Removes the cookie
|
||||||
|
|
||||||
|
return serialize(name, cartId || '', options)
|
||||||
|
}
|
36
packages/opencommerce/src/api/utils/get-search-variables.ts
Normal file
36
packages/opencommerce/src/api/utils/get-search-variables.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export type SearchProductsInput = {
|
||||||
|
search?: string
|
||||||
|
categoryId?: number | string
|
||||||
|
brandId?: number | string
|
||||||
|
locale?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSearchVariables = ({
|
||||||
|
brandId,
|
||||||
|
search,
|
||||||
|
categoryId,
|
||||||
|
}: SearchProductsInput) => {
|
||||||
|
let searchQuery = ''
|
||||||
|
let tagIdsParam = {}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
searchQuery += search
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brandId) {
|
||||||
|
searchQuery += `${search ? ' ' : ''}${brandId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryId) {
|
||||||
|
tagIdsParam = {
|
||||||
|
tagIds: [categoryId],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchQuery,
|
||||||
|
...tagIdsParam,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getSearchVariables
|
25
packages/opencommerce/src/api/utils/get-sort-variables.ts
Normal file
25
packages/opencommerce/src/api/utils/get-sort-variables.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
type SortByField = 'minPrice' | 'featured' | 'createdAt'
|
||||||
|
|
||||||
|
const getSortVariables = (sort?: string) => {
|
||||||
|
if (!sort) return null
|
||||||
|
const [_sort, direction] = sort.split('-')
|
||||||
|
|
||||||
|
const SORT: { [key: string]: SortByField | undefined } = {
|
||||||
|
price: 'minPrice',
|
||||||
|
trending: 'featured',
|
||||||
|
latest: 'createdAt',
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortValue = SORT[_sort]
|
||||||
|
|
||||||
|
if (sortValue && direction) {
|
||||||
|
return {
|
||||||
|
sortBy: sortValue,
|
||||||
|
sortOrder: direction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getSortVariables
|
7
packages/opencommerce/src/api/utils/types.ts
Normal file
7
packages/opencommerce/src/api/utils/types.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export type RecursivePartial<T> = {
|
||||||
|
[P in keyof T]?: RecursivePartial<T[P]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RecursiveRequired<T> = {
|
||||||
|
[P in keyof T]-?: RecursiveRequired<T[P]>
|
||||||
|
}
|
3
packages/opencommerce/src/auth/index.ts
Normal file
3
packages/opencommerce/src/auth/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as useLogin } from './use-login'
|
||||||
|
export { default as useLogout } from './use-logout'
|
||||||
|
export { default as useSignup } from './use-signup'
|
16
packages/opencommerce/src/auth/use-login.tsx
Normal file
16
packages/opencommerce/src/auth/use-login.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||||
|
import useLogin, { UseLogin } from '@vercel/commerce/auth/use-login'
|
||||||
|
|
||||||
|
export default useLogin as UseLogin<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
useHook: () => () => {
|
||||||
|
return async function () {}
|
||||||
|
},
|
||||||
|
}
|
17
packages/opencommerce/src/auth/use-logout.tsx
Normal file
17
packages/opencommerce/src/auth/use-logout.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||||
|
import useLogout, { UseLogout } from '@vercel/commerce/auth/use-logout'
|
||||||
|
|
||||||
|
export default useLogout as UseLogout<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
useHook:
|
||||||
|
({ fetch }) =>
|
||||||
|
() =>
|
||||||
|
async () => {},
|
||||||
|
}
|
19
packages/opencommerce/src/auth/use-signup.tsx
Normal file
19
packages/opencommerce/src/auth/use-signup.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import useCustomer from '../customer/use-customer'
|
||||||
|
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||||
|
import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup'
|
||||||
|
|
||||||
|
export default useSignup as UseSignup<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
useHook:
|
||||||
|
({ fetch }) =>
|
||||||
|
() =>
|
||||||
|
() => {},
|
||||||
|
}
|
4
packages/opencommerce/src/cart/index.ts
Normal file
4
packages/opencommerce/src/cart/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { default as useCart } from './use-cart'
|
||||||
|
export { default as useAddItem } from './use-add-item'
|
||||||
|
export { default as useRemoveItem } from './use-remove-item'
|
||||||
|
export { default as useUpdateItem } from './use-update-item'
|
43
packages/opencommerce/src/cart/use-add-item.tsx
Normal file
43
packages/opencommerce/src/cart/use-add-item.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import useAddItem, { UseAddItem } from '@vercel/commerce/cart/use-add-item'
|
||||||
|
import type { AddItemHook } from '@vercel/commerce/types/cart'
|
||||||
|
import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||||
|
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||||
|
import { CartTypes } from '../types/cart'
|
||||||
|
import useCart from './use-cart'
|
||||||
|
|
||||||
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<AddItemHook<CartTypes>> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/cart',
|
||||||
|
method: 'POST',
|
||||||
|
},
|
||||||
|
async fetcher({ input: item, options, fetch }) {
|
||||||
|
if (
|
||||||
|
item.quantity &&
|
||||||
|
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||||
|
) {
|
||||||
|
throw new CommerceError({
|
||||||
|
message: 'The item quantity has to be a valid integer greater than 0',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetch({ ...options, body: { item } })
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
useHook:
|
||||||
|
({ fetch }) =>
|
||||||
|
() => {
|
||||||
|
const { mutate } = useCart()
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async function addItem(input) {
|
||||||
|
const data = await fetch({ input })
|
||||||
|
await mutate(data, false)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
[fetch, mutate]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
33
packages/opencommerce/src/cart/use-cart.tsx
Normal file
33
packages/opencommerce/src/cart/use-cart.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||||
|
import useCart, { UseCart } from '@vercel/commerce/cart/use-cart'
|
||||||
|
import type { GetCartHook } from '@vercel/commerce/types/cart'
|
||||||
|
import { CartTypes } from '../types/cart'
|
||||||
|
|
||||||
|
export default useCart as UseCart<typeof handler>
|
||||||
|
|
||||||
|
export const handler: SWRHook<GetCartHook<CartTypes>> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/cart',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
useHook:
|
||||||
|
({ useData }) =>
|
||||||
|
(input) => {
|
||||||
|
const response = useData({
|
||||||
|
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||||
|
})
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
Object.create(response, {
|
||||||
|
isEmpty: {
|
||||||
|
get() {
|
||||||
|
return (response.data?.lineItems.length ?? 0) <= 0
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[response]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
56
packages/opencommerce/src/cart/use-remove-item.tsx
Normal file
56
packages/opencommerce/src/cart/use-remove-item.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import {
|
||||||
|
HookFetcherContext,
|
||||||
|
MutationHook,
|
||||||
|
MutationHookContext,
|
||||||
|
} from '@vercel/commerce/utils/types'
|
||||||
|
import useRemoveItem, {
|
||||||
|
UseRemoveItem,
|
||||||
|
} from '@vercel/commerce/cart/use-remove-item'
|
||||||
|
import type {
|
||||||
|
LineItem,
|
||||||
|
RemoveItemHook,
|
||||||
|
Cart,
|
||||||
|
} from '@vercel/commerce/types/cart'
|
||||||
|
import { ValidationError } from '@vercel/commerce/utils/errors'
|
||||||
|
import useCart from './use-cart'
|
||||||
|
|
||||||
|
export type RemoveItemActionInput<T = any> = T extends LineItem
|
||||||
|
? Partial<RemoveItemHook['actionInput']>
|
||||||
|
: RemoveItemHook['actionInput']
|
||||||
|
|
||||||
|
export type RemoveItemFn<T = any> = T extends LineItem
|
||||||
|
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
|
||||||
|
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
|
||||||
|
|
||||||
|
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<RemoveItemHook> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/cart',
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
async fetcher({ input: { itemId }, options, fetch }) {
|
||||||
|
return await fetch({ ...options, body: { itemId } })
|
||||||
|
},
|
||||||
|
useHook:
|
||||||
|
({ fetch }: MutationHookContext<RemoveItemHook>) =>
|
||||||
|
<T extends LineItem | undefined = undefined>(ctx: { item?: T } = {}) => {
|
||||||
|
const { item } = ctx
|
||||||
|
const { mutate } = useCart()
|
||||||
|
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
||||||
|
const itemId = input?.id ?? item?.id
|
||||||
|
if (!itemId) {
|
||||||
|
throw new ValidationError({
|
||||||
|
message: 'Invalid input used for this operation',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetch({ input: { itemId } })
|
||||||
|
await mutate(data, false)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
|
||||||
|
},
|
||||||
|
}
|
76
packages/opencommerce/src/cart/use-update-item.tsx
Normal file
76
packages/opencommerce/src/cart/use-update-item.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import debounce from 'lodash.debounce'
|
||||||
|
import { MutationHook, MutationHookContext } from '@vercel/commerce/utils/types'
|
||||||
|
import useUpdateItem, {
|
||||||
|
UseUpdateItem,
|
||||||
|
} from '@vercel/commerce/cart/use-update-item'
|
||||||
|
import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart'
|
||||||
|
import { ValidationError } from '@vercel/commerce/utils/errors'
|
||||||
|
import { handler as removeItemHandler } from './use-remove-item'
|
||||||
|
import useCart from './use-cart'
|
||||||
|
|
||||||
|
export type UpdateItemActionInput<T = any> = T extends LineItem
|
||||||
|
? Partial<UpdateItemHook['actionInput']>
|
||||||
|
: UpdateItemHook['actionInput']
|
||||||
|
|
||||||
|
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<UpdateItemHook> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/cart',
|
||||||
|
method: 'PUT',
|
||||||
|
},
|
||||||
|
async fetcher({ input: { itemId, item }, options, fetch }) {
|
||||||
|
if (Number.isInteger(item.quantity)) {
|
||||||
|
// Also allow the update hook to remove an item if the quantity is lower than 1
|
||||||
|
if (item.quantity! < 1) {
|
||||||
|
return removeItemHandler.fetcher!({
|
||||||
|
options: removeItemHandler.fetchOptions,
|
||||||
|
input: { itemId },
|
||||||
|
fetch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (item.quantity) {
|
||||||
|
throw new ValidationError({
|
||||||
|
message: 'The item quantity has to be a valid integer',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fetch({
|
||||||
|
...options,
|
||||||
|
body: { itemId, item },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
useHook:
|
||||||
|
({ fetch }: MutationHookContext<UpdateItemHook>) =>
|
||||||
|
<T extends LineItem | undefined = undefined>(
|
||||||
|
ctx: { item?: T; wait?: number } = {}
|
||||||
|
) => {
|
||||||
|
const { item, wait } = ctx
|
||||||
|
const { mutate } = useCart()
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
debounce(async (input: UpdateItemActionInput) => {
|
||||||
|
const itemId = input.id ?? item?.id
|
||||||
|
const productId = input.productId ?? item?.productId
|
||||||
|
const variantId = input.productId ?? item?.variantId
|
||||||
|
|
||||||
|
if (!itemId || !productId || !variantId) {
|
||||||
|
throw new ValidationError({
|
||||||
|
message: 'Invalid input used for this operation',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetch({
|
||||||
|
input: {
|
||||||
|
itemId,
|
||||||
|
item: { productId, variantId, quantity: input.quantity },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await mutate(data, false)
|
||||||
|
return data
|
||||||
|
}, wait ?? 500),
|
||||||
|
[fetch, mutate]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
2
packages/opencommerce/src/checkout/index.ts
Normal file
2
packages/opencommerce/src/checkout/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as useCheckout } from './use-checkout'
|
||||||
|
export { default as useSubmitCheckout } from './use-submit-checkout'
|
64
packages/opencommerce/src/checkout/use-checkout.tsx
Normal file
64
packages/opencommerce/src/checkout/use-checkout.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import type { GetCheckoutHook } from '@vercel/commerce/types/checkout'
|
||||||
|
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||||
|
import useCheckout, {
|
||||||
|
UseCheckout,
|
||||||
|
} from '@vercel/commerce/checkout/use-checkout'
|
||||||
|
import useSubmitCheckout from './use-submit-checkout'
|
||||||
|
import { useCheckoutContext } from '@components/checkout/context'
|
||||||
|
import { useCart } from '../cart'
|
||||||
|
|
||||||
|
export default useCheckout as UseCheckout<typeof handler>
|
||||||
|
|
||||||
|
export const handler: SWRHook<GetCheckoutHook> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
method: '',
|
||||||
|
},
|
||||||
|
useHook: () =>
|
||||||
|
function useHook() {
|
||||||
|
const { data: cart } = useCart()
|
||||||
|
const shippingTypeMethod =
|
||||||
|
cart?.checkout?.fulfillmentGroups &&
|
||||||
|
cart.checkout.fulfillmentGroups.find(
|
||||||
|
(group) => group?.type === 'shipping'
|
||||||
|
)
|
||||||
|
const hasShippingMethods =
|
||||||
|
!!shippingTypeMethod?.availableFulfillmentOptions.length
|
||||||
|
|
||||||
|
const { addressFields } = useCheckoutContext()
|
||||||
|
|
||||||
|
const { shippingMethod, ...restAddressFields } = addressFields
|
||||||
|
|
||||||
|
const hasEnteredAddress = Object.values(restAddressFields).some(
|
||||||
|
(fieldValue) => !!fieldValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = useMemo(
|
||||||
|
() => ({
|
||||||
|
data: {
|
||||||
|
// example payment plugin does not need payment info
|
||||||
|
hasPayment: true,
|
||||||
|
hasShipping: hasEnteredAddress,
|
||||||
|
hasShippingMethods,
|
||||||
|
hasSelectedShippingMethod: !!shippingMethod?.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[hasEnteredAddress, hasShippingMethods, shippingMethod]
|
||||||
|
)
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
Object.create(response, {
|
||||||
|
submit: {
|
||||||
|
get() {
|
||||||
|
return useSubmitCheckout
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[response, useSubmitCheckout]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
32
packages/opencommerce/src/checkout/use-submit-checkout.ts
Normal file
32
packages/opencommerce/src/checkout/use-submit-checkout.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
import type { SubmitCheckoutHook } from '@vercel/commerce/types/checkout'
|
||||||
|
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||||
|
import useSubmitCheckout, {
|
||||||
|
UseSubmitCheckout,
|
||||||
|
} from '@vercel/commerce/checkout/use-submit-checkout'
|
||||||
|
|
||||||
|
export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<SubmitCheckoutHook> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/checkout',
|
||||||
|
method: 'POST',
|
||||||
|
},
|
||||||
|
async fetcher({ input: item, options, fetch }) {
|
||||||
|
const data = await fetch({
|
||||||
|
...options,
|
||||||
|
body: { item },
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
useHook: ({ fetch }) =>
|
||||||
|
function useHook() {
|
||||||
|
return useCallback(async function onSubmitCheckout(input) {
|
||||||
|
const data = await fetch({
|
||||||
|
input,
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}, [])
|
||||||
|
},
|
||||||
|
}
|
7
packages/opencommerce/src/commerce.config.json
Normal file
7
packages/opencommerce/src/commerce.config.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"provider": "opencommerce",
|
||||||
|
"features": {
|
||||||
|
"customCheckout": true,
|
||||||
|
"customNavigation": true
|
||||||
|
}
|
||||||
|
}
|
2
packages/opencommerce/src/customer/address/index.ts
Normal file
2
packages/opencommerce/src/customer/address/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as useAddItem } from './use-add-item'
|
||||||
|
export { default as useUpdateItem } from './use-update-item'
|
37
packages/opencommerce/src/customer/address/use-add-item.tsx
Normal file
37
packages/opencommerce/src/customer/address/use-add-item.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { AddItemHook } from '@vercel/commerce/types/customer/address'
|
||||||
|
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import useAddItem, {
|
||||||
|
UseAddItem,
|
||||||
|
} from '@vercel/commerce/customer/address/use-add-item'
|
||||||
|
import { useCheckoutContext } from '@components/checkout/context'
|
||||||
|
import { CustomerAddressTypes } from '../../types/customer/address'
|
||||||
|
|
||||||
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<AddItemHook<CustomerAddressTypes>> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/customer/address',
|
||||||
|
method: 'POST',
|
||||||
|
},
|
||||||
|
async fetcher({ input: item, options, fetch }) {
|
||||||
|
const data = await fetch({
|
||||||
|
...options,
|
||||||
|
body: { item },
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
useHook: ({ fetch }) =>
|
||||||
|
function useHook() {
|
||||||
|
const { setAddressFields, addressFields } = useCheckoutContext()
|
||||||
|
return useCallback(
|
||||||
|
async function addItem(input) {
|
||||||
|
await fetch({ input })
|
||||||
|
setAddressFields({ ...addressFields, ...input })
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
[setAddressFields]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import type { UpdateItemHook } from '@vercel/commerce/types/customer/address'
|
||||||
|
import type {
|
||||||
|
MutationHook,
|
||||||
|
MutationHookContext,
|
||||||
|
} from '@vercel/commerce/utils/types'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import useUpdateItem, {
|
||||||
|
UseUpdateItem,
|
||||||
|
} from '@vercel/commerce/customer/address/use-update-item'
|
||||||
|
import { useCheckoutContext } from '@components/checkout/context'
|
||||||
|
import { CustomerAddressTypes } from '../../types/customer/address'
|
||||||
|
|
||||||
|
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<UpdateItemHook<CustomerAddressTypes>> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/customer/address',
|
||||||
|
method: 'PUT',
|
||||||
|
},
|
||||||
|
async fetcher({ input: { item, itemId }, options, fetch }) {
|
||||||
|
const data = await fetch({
|
||||||
|
...options,
|
||||||
|
body: { item, itemId },
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
useHook: ({ fetch }) =>
|
||||||
|
function useHook() {
|
||||||
|
const { setAddressFields, addressFields } = useCheckoutContext()
|
||||||
|
return useCallback(
|
||||||
|
async function updateItem(input) {
|
||||||
|
const { id, ...rest } = input
|
||||||
|
await fetch({ input: { item: rest, itemId: id } })
|
||||||
|
setAddressFields({
|
||||||
|
...addressFields,
|
||||||
|
shippingMethod: rest.shippingMethod,
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
[setAddressFields]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
2
packages/opencommerce/src/customer/card/index.ts
Normal file
2
packages/opencommerce/src/customer/card/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as useAddItem } from './use-add-item'
|
||||||
|
export { default as useCards } from './use-cards'
|
27
packages/opencommerce/src/customer/card/use-add-item.tsx
Normal file
27
packages/opencommerce/src/customer/card/use-add-item.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { AddItemHook } from '@vercel/commerce/types/customer/card'
|
||||||
|
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import useAddItem, {
|
||||||
|
UseAddItem,
|
||||||
|
} from '@vercel/commerce/customer/card/use-add-item'
|
||||||
|
import { useCheckoutContext } from '@components/checkout/context'
|
||||||
|
|
||||||
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<AddItemHook> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '',
|
||||||
|
method: '',
|
||||||
|
},
|
||||||
|
useHook: () =>
|
||||||
|
function useHook() {
|
||||||
|
const { setCardFields } = useCheckoutContext()
|
||||||
|
return useCallback(
|
||||||
|
async function addItem(input) {
|
||||||
|
setCardFields(input)
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
[setCardFields]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
32
packages/opencommerce/src/customer/card/use-cards.ts
Normal file
32
packages/opencommerce/src/customer/card/use-cards.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import type { GetCardsHook } from '@vercel/commerce/types/customer/card'
|
||||||
|
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||||
|
import useCard, { UseCards } from '@vercel/commerce/customer/card/use-cards'
|
||||||
|
|
||||||
|
export default useCard as UseCards<typeof handler>
|
||||||
|
|
||||||
|
export const handler: SWRHook<GetCardsHook> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/customer/card',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
useHook: ({ useData }) =>
|
||||||
|
function useHook(input) {
|
||||||
|
const response = useData({
|
||||||
|
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||||
|
})
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
Object.create(response, {
|
||||||
|
isEmpty: {
|
||||||
|
get() {
|
||||||
|
return (response.data?.length ?? 0) <= 0
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[response]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
1
packages/opencommerce/src/customer/index.ts
Normal file
1
packages/opencommerce/src/customer/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as useCustomer } from './use-customer'
|
17
packages/opencommerce/src/customer/use-customer.tsx
Normal file
17
packages/opencommerce/src/customer/use-customer.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||||
|
import useCustomer, {
|
||||||
|
UseCustomer,
|
||||||
|
} from '@vercel/commerce/customer/use-customer'
|
||||||
|
|
||||||
|
export default useCustomer as UseCustomer<typeof handler>
|
||||||
|
export const handler: SWRHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher({ input, options, fetch }) {},
|
||||||
|
useHook: () => () => {
|
||||||
|
return async function addItem() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
41
packages/opencommerce/src/fetcher.ts
Normal file
41
packages/opencommerce/src/fetcher.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { Fetcher } from '@vercel/commerce/utils/types'
|
||||||
|
import { FetcherError } from '@vercel/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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher: Fetcher = async ({
|
||||||
|
url,
|
||||||
|
method = 'GET',
|
||||||
|
variables,
|
||||||
|
body: bodyObj,
|
||||||
|
}) => {
|
||||||
|
const hasBody = Boolean(variables || bodyObj)
|
||||||
|
const body = hasBody
|
||||||
|
? JSON.stringify(variables ? { variables } : bodyObj)
|
||||||
|
: undefined
|
||||||
|
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
||||||
|
const res = await fetch(url!, { method, body, headers })
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const { data } = await res.json()
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
throw await getError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default fetcher
|
12
packages/opencommerce/src/index.ts
Normal file
12
packages/opencommerce/src/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import {
|
||||||
|
getCommerceProvider,
|
||||||
|
useCommerce as useCoreCommerce,
|
||||||
|
} from '@vercel/commerce'
|
||||||
|
import { openCommerceProvider, OpenCommerceProvider } from './provider'
|
||||||
|
|
||||||
|
export { openCommerceProvider }
|
||||||
|
export type { OpenCommerceProvider }
|
||||||
|
|
||||||
|
export const CommerceProvider = getCommerceProvider(openCommerceProvider)
|
||||||
|
|
||||||
|
export const useCommerce = () => useCoreCommerce<OpenCommerceProvider>()
|
8
packages/opencommerce/src/next.config.cjs
Normal file
8
packages/opencommerce/src/next.config.cjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const commerce = require('./commerce.config.json')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
commerce,
|
||||||
|
images: {
|
||||||
|
domains: [process.env.OPENCOMMERCE_IMAGE_DOMAIN],
|
||||||
|
},
|
||||||
|
}
|
2
packages/opencommerce/src/product/index.ts
Normal file
2
packages/opencommerce/src/product/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as usePrice } from './use-price'
|
||||||
|
export { default as useSearch } from './use-search'
|
2
packages/opencommerce/src/product/use-price.tsx
Normal file
2
packages/opencommerce/src/product/use-price.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from '@vercel/commerce/product/use-price'
|
||||||
|
export { default } from '@vercel/commerce/product/use-price'
|
45
packages/opencommerce/src/product/use-search.tsx
Normal file
45
packages/opencommerce/src/product/use-search.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||||
|
import useSearch, { UseSearch } from '@vercel/commerce/product/use-search'
|
||||||
|
import type { SearchProductsHook } from '../types/product'
|
||||||
|
|
||||||
|
export default useSearch as UseSearch<typeof handler>
|
||||||
|
|
||||||
|
export const handler: SWRHook<SearchProductsHook> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/catalog/products',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
async fetcher({
|
||||||
|
input: { search, categoryId, brandId, sort },
|
||||||
|
options,
|
||||||
|
fetch,
|
||||||
|
}) {
|
||||||
|
// Use a dummy base as we only care about the relative path
|
||||||
|
const url = new URL(options.url!, 'http://a')
|
||||||
|
if (search) url.searchParams.set('search', search)
|
||||||
|
if (categoryId) url.searchParams.set('categoryId', String(categoryId))
|
||||||
|
if (brandId) url.searchParams.set('brandId', String(brandId))
|
||||||
|
if (sort) url.searchParams.set('sort', sort)
|
||||||
|
|
||||||
|
return fetch({
|
||||||
|
url: url.pathname + url.search,
|
||||||
|
method: options.method,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
useHook:
|
||||||
|
({ useData }) =>
|
||||||
|
(input = {}) => {
|
||||||
|
return useData({
|
||||||
|
input: [
|
||||||
|
['search', input.search],
|
||||||
|
['categoryId', input.categoryId],
|
||||||
|
['brandId', input.brandId],
|
||||||
|
['sort', input.sort],
|
||||||
|
],
|
||||||
|
swrOptions: {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
...input.swrOptions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
36
packages/opencommerce/src/provider.ts
Normal file
36
packages/opencommerce/src/provider.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import fetcher from './fetcher'
|
||||||
|
import { handler as useCart } from './cart/use-cart'
|
||||||
|
import { handler as useAddItem } from './cart/use-add-item'
|
||||||
|
import { handler as useUpdateItem } from './cart/use-update-item'
|
||||||
|
import { handler as useRemoveItem } from './cart/use-remove-item'
|
||||||
|
import { handler as useCustomer } from './customer/use-customer'
|
||||||
|
import { handler as useSearch } from './product/use-search'
|
||||||
|
import { handler as useLogin } from './auth/use-login'
|
||||||
|
import { handler as useLogout } from './auth/use-logout'
|
||||||
|
import { handler as useSignup } from './auth/use-signup'
|
||||||
|
import { handler as useCheckout } from './checkout/use-checkout'
|
||||||
|
import { handler as useSubmitCheckout } from './checkout/use-submit-checkout'
|
||||||
|
import { handler as useAddCardItem } from './customer/card/use-add-item'
|
||||||
|
import { handler as useCards } from './customer/card/use-cards'
|
||||||
|
import { handler as useAddAddressItem } from './customer/address/use-add-item'
|
||||||
|
import { handler as useUpdateAddressItem } from './customer/address/use-update-item'
|
||||||
|
|
||||||
|
export const openCommerceProvider = {
|
||||||
|
locale: 'en-us',
|
||||||
|
cartCookie: 'opencommerce_cartId',
|
||||||
|
fetcher,
|
||||||
|
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||||
|
customer: {
|
||||||
|
useCustomer,
|
||||||
|
card: { useCards, useAddItem: useAddCardItem },
|
||||||
|
address: {
|
||||||
|
useAddItem: useAddAddressItem,
|
||||||
|
useUpdateItem: useUpdateAddressItem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
products: { useSearch },
|
||||||
|
auth: { useLogin, useLogout, useSignup },
|
||||||
|
checkout: { useCheckout, useSubmitCheckout },
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpenCommerceProvider = typeof openCommerceProvider
|
21
packages/opencommerce/src/types/cart.ts
Normal file
21
packages/opencommerce/src/types/cart.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as Core from '@vercel/commerce/types/cart'
|
||||||
|
import { Checkout } from '../../schema'
|
||||||
|
import { ProductVariant } from './product'
|
||||||
|
|
||||||
|
export * from '@vercel/commerce/types/cart'
|
||||||
|
|
||||||
|
export type Cart = Core.Cart & {
|
||||||
|
checkout?: Checkout
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CartItemBody = Core.CartItemBody & {
|
||||||
|
currencyCode?: string
|
||||||
|
variant?: ProductVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CartTypes = Core.CartTypes & {
|
||||||
|
itemBody: CartItemBody
|
||||||
|
cart?: Cart
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CartSchema = Core.CartSchema<CartTypes>
|
1
packages/opencommerce/src/types/checkout.ts
Normal file
1
packages/opencommerce/src/types/checkout.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@vercel/commerce/types/checkout'
|
14
packages/opencommerce/src/types/customer/address.ts
Normal file
14
packages/opencommerce/src/types/customer/address.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import * as Core from '@vercel/commerce/types/customer/address'
|
||||||
|
|
||||||
|
export * from '@vercel/commerce/types/customer/address'
|
||||||
|
|
||||||
|
export type AddressFields = Core.AddressFields & {
|
||||||
|
shippingMethod?: {
|
||||||
|
id: string
|
||||||
|
fulfillmentGroupId: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerAddressTypes = Core.CustomerAddressTypes & {
|
||||||
|
fields: AddressFields
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user