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
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -1,4 +1,4 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
}
|
@ -5,7 +5,9 @@ import type {
|
||||
HookFetcherContext,
|
||||
} from '@vercel/commerce/utils/types'
|
||||
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 { handler as removeItemHandler } from './use-remove-item'
|
||||
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))
|
||||
- Commerce.js ([packages/commercejs](../commercejs))
|
||||
- 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:
|
||||
|
||||
- `src`
|
||||
|
@ -1 +1 @@
|
||||
COMMERCE_PROVIDER=@vercel/commerce-local
|
||||
COMMERCE_PROVIDER=@vercel/commerce-local
|
||||
|
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>
|
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