Monorepo with Turborepo (#651)

* Moved everything

* Figuring out how to make imports work

* Updated exports

* Added missing exports

* Added @vercel/commerce-local to `site`

* Updated commerce config

* Updated exports and commerce config

* Updated commerce hoc

* Fixed exports in local

* Added publish config

* Updated imports in site

* It's actually working

* Don't use debugger in dev for better speeds

* Improved DX when editing packages

* Set up eslint with husky

* Updated prettier config

* Added prettier setup to every package

* Moved bigcommerce

* Moved Bigcommerce to src and package updates

* Updated setup of bigcommerce

* Moved definitions script

* Moved commercejs

* Move to src

* Fixed types in commercejs

* Moved kibocommerce

* Moved kibocommerce to src

* Added package/tsconfig to kibocommerce

* Fixed imports and other things

* Moved ordercloud

* Moved ordercloud to src

* Fixed imports

* Added missing prettier files

* Moved Saleor

* Moved Saleor to src

* Fixed imports

* Replaced all imports to @commerce

* Added prettierignore/rc to all providers

* Moved shopify to src

* Build shopify in packages

* Moved Spree

* Moved spree to src

* Updated spree

* Moved swell

* Moved swell to src

* Fixed type imports in swell

* Moved Vendure to packages

* Moved vendure to src

* Fixed imports in vendure

* Added codegen to saleor

* Updated codegen setup for shopify

* Added codegen to vendure

* Added codegen to kibocommerce

* Added all packages to site's deps

* Updated codegen setup in bigcommerce

* Minor fixes

* Updated providers' names in site

* Updated packages based on Bel's changes

* Updated turbo to latest

* Fixed ts complains

* Set npm engine in root

* New lockfile install

* remove engines

* Regen lockfile

* Switched from npm to yarn

* Updated typesVersions in all packages

* Moved dep

* Updated SWR to the just released 1.2.0

* Removed "isolatedModules" from packages

* Updated list of providers and default

* Updated swell declaration

* Removed next import from kibocommerce

* Added COMMERCE_PROVIDER log

* Added another log

* Updated turbo config

* Updated docs

* Removed test logs

Co-authored-by: Jared Palmer <jared@jaredpalmer.com>
This commit is contained in:
Luis Alvarez D
2022-02-01 14:14:05 -05:00
committed by GitHub
parent d0ef346189
commit 0afe686fe9
1326 changed files with 9109 additions and 19494 deletions

View File

@@ -0,0 +1,6 @@
COMMERCE_PROVIDER=ordercloud
ORDERCLOUD_BUYER_CLIENT_ID=
ORDERCLOUD_MIDDLEWARE_CLIENT_ID=
ORDERCLOUD_MIDDLEWARE_CLIENT_SECRET=
STRIPE_SECRET=

View File

@@ -0,0 +1,2 @@
node_modules
dist

View File

@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false
}

View File

@@ -0,0 +1,3 @@
# Next.js Ordercloud Provider
Create your own store from [here](https://nextjs.org/commerce)

View File

@@ -0,0 +1,73 @@
{
"name": "@vercel/commerce-ordercloud",
"version": "0.0.1",
"license": "MIT",
"scripts": {
"build": "rm -fr dist/* && tsc",
"dev": "npm run build -- --watch",
"prettier-fix": "prettier --write ."
},
"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",
"stripe": "^8.197.0"
},
"peerDependencies": {
"next": "^12",
"react": "^17",
"react-dom": "^17"
},
"devDependencies": {
"@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",
"typescript": "^4.5.4"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx,json}": [
"prettier --write",
"git add"
]
}
}

View File

@@ -0,0 +1,99 @@
import type { CartEndpoint } from '.'
import type { RawVariant } from '../../../types/product'
import type { OrdercloudLineItem } from '../../../types/cart'
import { serialize } from 'cookie'
import { formatCart } from '../../utils/cart'
const addItem: CartEndpoint['handlers']['addItem'] = async ({
res,
body: { cartId, item },
config: { restBuyerFetch, cartCookie, tokenCookie },
}) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Store token
let token
// Set the quantity if not present
if (!item.quantity) item.quantity = 1
// Create an order if it doesn't exist
if (!cartId) {
const { ID, meta } = await restBuyerFetch(
'POST',
`/orders/Outgoing`,
{}
).then((response: { ID: string; meta: { token: string } }) => response)
// Set the cart id and token
cartId = ID
token = meta.token
// Set the cart and token cookie
res.setHeader('Set-Cookie', [
serialize(tokenCookie, meta.token, {
maxAge: 60 * 60 * 24 * 30,
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
}),
serialize(cartCookie, cartId, {
maxAge: 60 * 60 * 24 * 30,
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
}),
])
}
// Store specs
let specs: RawVariant['Specs'] = []
// If a variant is present, fetch its specs
if (item.variantId) {
specs = await restBuyerFetch(
'GET',
`/me/products/${item.productId}/variants/${item.variantId}`,
null,
{ token }
).then((res: RawVariant) => res.Specs)
}
// Add the item to the order
await restBuyerFetch(
'POST',
`/orders/Outgoing/${cartId}/lineitems`,
{
ProductID: item.productId,
Quantity: item.quantity,
Specs: specs,
},
{ token }
)
// Get cart
const [cart, lineItems] = await Promise.all([
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
token,
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
])
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
}
export default addItem

View File

@@ -0,0 +1,65 @@
import type { OrdercloudLineItem } from '../../../types/cart'
import type { CartEndpoint } from '.'
import { serialize } from 'cookie'
import { formatCart } from '../../utils/cart'
// Return current cart info
const getCart: CartEndpoint['handlers']['getCart'] = async ({
req,
res,
body: { cartId },
config: { restBuyerFetch, cartCookie, tokenCookie },
}) => {
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
try {
// Get token from cookies
const token = req.cookies[tokenCookie]
// Get cart
const cart = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}`,
null,
{ token }
)
// Get line items
const lineItems = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}/lineitems`,
null,
{ token }
).then((response: { Items: OrdercloudLineItem[] }) => response.Items)
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
} catch (error) {
// Reset cart and token cookie
res.setHeader('Set-Cookie', [
serialize(cartCookie, cartId, {
maxAge: -1,
path: '/',
}),
serialize(tokenCookie, cartId, {
maxAge: -1,
path: '/',
}),
])
// Return empty cart
res.status(200).json({ data: null, errors: [] })
}
}
export default getCart

View File

@@ -0,0 +1,28 @@
import type { CartSchema } from '../../../types/cart'
import type { OrdercloudAPI } from '../..'
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
import getCart from './get-cart'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CartAPI = GetAPISchema<OrdercloudAPI, CartSchema>
export type CartEndpoint = CartAPI['endpoint']
export const handlers: CartEndpoint['handlers'] = {
getCart,
addItem,
updateItem,
removeItem,
}
const cartApi = createEndpoint<CartAPI>({
handler: cartEndpoint,
handlers,
})
export default cartApi

View File

@@ -0,0 +1,45 @@
import type { CartEndpoint } from '.'
import { formatCart } from '../../utils/cart'
import { OrdercloudLineItem } from '../../../types/cart'
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
req,
res,
body: { cartId, itemId },
config: { restBuyerFetch, tokenCookie },
}) => {
if (!cartId || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Remove the item to the order
await restBuyerFetch(
'DELETE',
`/orders/Outgoing/${cartId}/lineitems/${itemId}`,
null,
{ token }
)
// Get cart
const [cart, lineItems] = await Promise.all([
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
token,
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
])
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
}
export default removeItem

View File

@@ -0,0 +1,63 @@
import type { OrdercloudLineItem } from '../../../types/cart'
import type { RawVariant } from '../../../types/product'
import type { CartEndpoint } from '.'
import { formatCart } from '../../utils/cart'
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
req,
res,
body: { cartId, itemId, item },
config: { restBuyerFetch, tokenCookie },
}) => {
if (!cartId || !itemId || !item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Store specs
let specs: RawVariant['Specs'] = []
// If a variant is present, fetch its specs
if (item.variantId) {
specs = await restBuyerFetch(
'GET',
`/me/products/${item.productId}/variants/${item.variantId}`,
null,
{ token }
).then((res: RawVariant) => res.Specs)
}
// Add the item to the order
await restBuyerFetch(
'PATCH',
`/orders/Outgoing/${cartId}/lineitems/${itemId}`,
{
ProductID: item.productId,
Quantity: item.quantity,
Specs: specs,
},
{ token }
)
// Get cart
const [cart, lineItems] = await Promise.all([
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
token,
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
])
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
}
export default updateItem

View File

@@ -0,0 +1,37 @@
import { normalize as normalizeProduct } from '../../../../utils/product'
import { ProductsEndpoint } from '.'
// Get products for the product list page. Search and category filter implemented. Sort and brand filter not implemented.
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
req,
res,
body: { search, categoryId, brandId, sort },
config: { restBuyerFetch, cartCookie, tokenCookie },
}) => {
//Use a dummy base as we only care about the relative path
const url = new URL('/me/products', 'http://a')
if (search) {
url.searchParams.set('search', search)
}
if (categoryId) {
url.searchParams.set('categoryID', String(categoryId))
}
// Get token from cookies
const token = req.cookies[tokenCookie]
var rawProducts = await restBuyerFetch(
'GET',
url.pathname + url.search,
null,
{ token }
)
const products = rawProducts.Items.map(normalizeProduct)
const found = rawProducts?.Items?.length > 0
res.status(200).json({ data: { products, found } })
}
export default getProducts

View File

@@ -0,0 +1,19 @@
import type { OrdercloudAPI } from '../../../../api'
import { createEndpoint, GetAPISchema } from '@vercel/commerce/api'
import { ProductsSchema } from '@vercel/commerce/types/product'
import getProducts from './get-products'
import productsEndpoint from '@vercel/commerce/api/endpoints/catalog/products'
export type ProductsAPI = GetAPISchema<OrdercloudAPI, ProductsSchema>
export type ProductsEndpoint = ProductsAPI['endpoint']
export const handlers: ProductsEndpoint['handlers'] = { getProducts }
const productsApi = createEndpoint<ProductsAPI>({
handler: productsEndpoint,
handlers,
})
export default productsApi

View File

@@ -0,0 +1,47 @@
import type { CheckoutEndpoint } from '.'
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
req,
res,
body: { cartId },
config: { restBuyerFetch, tokenCookie },
}) => {
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing cookie' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Register credit card
const payments = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}/payments`,
null,
{ token }
).then((response: { Items: unknown[] }) => response.Items)
const address = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}`,
null,
{ token }
).then(
(response: { ShippingAddressID: string }) => response.ShippingAddressID
)
// Return cart and errors
res.status(200).json({
data: {
hasPayment: payments.length > 0,
hasShipping: Boolean(address),
},
errors: [],
})
}
export default getCheckout

View File

@@ -0,0 +1,23 @@
import type { CheckoutSchema } from '../../../types/checkout'
import type { OrdercloudAPI } from '../..'
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
import getCheckout from './get-checkout'
import submitCheckout from './submit-checkout'
export type CheckoutAPI = GetAPISchema<OrdercloudAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = {
getCheckout,
submitCheckout,
}
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

View File

@@ -0,0 +1,32 @@
import type { CheckoutEndpoint } from '.'
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
req,
res,
body: { cartId },
config: { restBuyerFetch, tokenCookie },
}) => {
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Submit order
await restBuyerFetch(
'POST',
`/orders/Outgoing/${cartId}/submit`,
{},
{ token }
)
// Return cart and errors
res.status(200).json({ data: null, errors: [] })
}
export default submitCheckout

View File

@@ -0,0 +1,47 @@
import type { CustomerAddressEndpoint } from '.'
const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
res,
body: { item, cartId },
config: { restBuyerFetch },
}) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
// Register address
const address = await restBuyerFetch('POST', `/me/addresses`, {
AddressName: 'main address',
CompanyName: item.company,
FirstName: item.firstName,
LastName: item.lastName,
Street1: item.streetNumber,
Street2: item.streetNumber,
City: item.city,
State: item.city,
Zip: item.zipCode,
Country: item.country.slice(0, 2).toLowerCase(),
Shipping: true,
}).then((response: { ID: string }) => response.ID)
// Assign address to order
await restBuyerFetch('PATCH', `/orders/Outgoing/${cartId}`, {
ShippingAddressID: address,
})
return res.status(200).json({ data: null, errors: [] })
}
export default addItem

View File

@@ -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

View File

@@ -0,0 +1,30 @@
import type { CustomerAddressSchema } from '../../../../types/customer/address'
import type { OrdercloudAPI } 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<
OrdercloudAPI,
CustomerAddressSchema
>
export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint']
export const handlers: CustomerAddressEndpoint['handlers'] = {
getAddresses,
addItem,
updateItem,
removeItem,
}
const customerAddressApi = createEndpoint<CustomerAddressAPI>({
handler: customerAddressEndpoint,
handlers,
})
export default customerAddressApi

View File

@@ -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

View File

@@ -0,0 +1,9 @@
import type { CustomerAddressEndpoint } from '.'
const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default updateItem

View File

@@ -0,0 +1,74 @@
import type { CustomerCardEndpoint } from '.'
import type { OredercloudCreditCard } from '../../../../types/customer/card'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET as string, {
apiVersion: '2020-08-27',
})
const addItem: CustomerCardEndpoint['handlers']['addItem'] = async ({
res,
body: { item, cartId },
config: { restBuyerFetch, restMiddlewareFetch },
}) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
// Get token
const token = await stripe.tokens
.create({
card: {
number: item.cardNumber,
exp_month: item.cardExpireDate.split('/')[0],
exp_year: item.cardExpireDate.split('/')[1],
cvc: item.cardCvc,
},
})
.then((res: { id: string }) => res.id)
// Register credit card
const creditCard = await restBuyerFetch('POST', `/me/creditcards`, {
Token: token,
CardType: 'credit',
PartialAccountNumber: item.cardNumber.slice(-4),
CardholderName: item.cardHolder,
ExpirationDate: item.cardExpireDate,
}).then((response: OredercloudCreditCard) => response.ID)
// Assign payment to order
const payment = await restBuyerFetch(
'POST',
`/orders/All/${cartId}/payments`,
{
Type: 'CreditCard',
CreditCardID: creditCard,
}
).then((response: { ID: string }) => response.ID)
// Accept payment to order
await restMiddlewareFetch(
'PATCH',
`/orders/All/${cartId}/payments/${payment}`,
{
Accepted: true,
}
)
return res.status(200).json({ data: null, errors: [] })
}
export default addItem

View File

@@ -0,0 +1,9 @@
import type { CustomerCardEndpoint } from '.'
const getCards: CustomerCardEndpoint['handlers']['getCards'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default getCards

View File

@@ -0,0 +1,27 @@
import type { CustomerCardSchema } from '../../../../types/customer/card'
import type { OrdercloudAPI } from '../../..'
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import customerCardEndpoint from '@vercel/commerce/api/endpoints/customer/card'
import getCards from './get-cards'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CustomerCardAPI = GetAPISchema<OrdercloudAPI, CustomerCardSchema>
export type CustomerCardEndpoint = CustomerCardAPI['endpoint']
export const handlers: CustomerCardEndpoint['handlers'] = {
getCards,
addItem,
updateItem,
removeItem,
}
const customerCardApi = createEndpoint<CustomerCardAPI>({
handler: customerCardEndpoint,
handlers,
})
export default customerCardApi

View File

@@ -0,0 +1,9 @@
import type { CustomerCardEndpoint } from '.'
const removeItem: CustomerCardEndpoint['handlers']['removeItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default removeItem

View File

@@ -0,0 +1,9 @@
import type { CustomerCardEndpoint } from '.'
const updateItem: CustomerCardEndpoint['handlers']['updateItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default updateItem

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1,71 @@
import type { CommerceAPI, CommerceAPIConfig } from '@vercel/commerce/api'
import { getCommerceApi as commerceApi } from '@vercel/commerce/api'
import { createBuyerFetcher, createMiddlewareFetcher } from './utils/fetch-rest'
import createGraphqlFetcher from './utils/fetch-graphql'
import getAllPages from './operations/get-all-pages'
import getPage from './operations/get-page'
import getSiteInfo from './operations/get-site-info'
import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products'
import getProduct from './operations/get-product'
import {
API_URL,
API_VERSION,
CART_COOKIE,
CUSTOMER_COOKIE,
TOKEN_COOKIE,
} from '../constants'
export interface OrdercloudConfig extends CommerceAPIConfig {
restBuyerFetch: <T>(
method: string,
resource: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T>
restMiddlewareFetch: <T>(
method: string,
resource: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T>
apiVersion: string
tokenCookie: string
}
const config: OrdercloudConfig = {
commerceUrl: API_URL,
apiToken: '',
apiVersion: API_VERSION,
cartCookie: CART_COOKIE,
customerCookie: CUSTOMER_COOKIE,
tokenCookie: TOKEN_COOKIE,
cartCookieMaxAge: 2592000,
restBuyerFetch: createBuyerFetcher(() => getCommerceApi().getConfig()),
restMiddlewareFetch: createMiddlewareFetcher(() =>
getCommerceApi().getConfig()
),
fetch: createGraphqlFetcher(() => getCommerceApi().getConfig()),
}
const operations = {
getAllPages,
getPage,
getSiteInfo,
getAllProductPaths,
getAllProducts,
getProduct,
}
export const provider = { config, operations }
export type Provider = typeof provider
export type OrdercloudAPI<P extends Provider = Provider> = CommerceAPI<P | any>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): OrdercloudAPI<P> {
return commerceApi(customProvider as any)
}

View File

@@ -0,0 +1,22 @@
import type { OrdercloudConfig } from '../'
import { GetAllPagesOperation } from '@vercel/commerce/types/page'
export type Page = { url: string }
export type GetAllPagesResult = { pages: Page[] }
export default function getAllPagesOperation() {
async function getAllPages<T extends GetAllPagesOperation>({
config,
preview,
}: {
url?: string
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
return Promise.resolve({
pages: [],
})
}
return getAllPages
}

View File

@@ -0,0 +1,34 @@
import type { OperationContext } from '@vercel/commerce/api/operations'
import type { GetAllProductPathsOperation } from '@vercel/commerce/types/product'
import type { RawProduct } from '../../types/product'
import type { OrdercloudConfig, Provider } from '../'
export type GetAllProductPathsResult = {
products: Array<{ path: string }>
}
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
config,
}: {
config?: Partial<OrdercloudConfig>
} = {}): Promise<T['data']> {
// Get fetch from the config
const { restBuyerFetch } = commerce.getConfig(config)
// Get all products
const rawProducts: RawProduct[] = await restBuyerFetch<{
Items: RawProduct[]
}>('GET', '/me/products').then((response) => response.Items)
return {
// Match a path for every product retrieved
products: rawProducts.map((product) => ({ path: `/${product.ID}` })),
}
}
return getAllProductPaths
}

View File

@@ -0,0 +1,35 @@
import type { GetAllProductsOperation } from '@vercel/commerce/types/product'
import type { OperationContext } from '@vercel/commerce/api/operations'
import type { RawProduct } from '../../types/product'
import type { OrdercloudConfig, Provider } from '../index'
import { normalize as normalizeProduct } from '../../utils/product'
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>({
config,
}: {
query?: string
variables?: T['variables']
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
// Get fetch from the config
const { restBuyerFetch } = commerce.getConfig(config)
// Get all products
const rawProducts: RawProduct[] = await restBuyerFetch<{
Items: RawProduct[]
}>('GET', '/me/products').then((response) => response.Items)
return {
// Normalize products to commerce schema
products: rawProducts.map(normalizeProduct),
}
}
return getAllProducts
}

View File

@@ -0,0 +1,15 @@
import { GetPageOperation } from '@vercel/commerce/types/page'
export type Page = any
export type GetPageResult = { page?: Page }
export type PageVariables = {
id: number
}
export default function getPageOperation() {
async function getPage<T extends GetPageOperation>(): Promise<T['data']> {
return Promise.resolve({})
}
return getPage
}

View File

@@ -0,0 +1,60 @@
import type { OperationContext } from '@vercel/commerce/api/operations'
import type { GetProductOperation } from '@vercel/commerce/types/product'
import type { RawProduct, RawSpec, RawVariant } from '../../types/product'
import type { OrdercloudConfig, Provider } from '../index'
import { normalize as normalizeProduct } from '../../utils/product'
export default function getProductOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct<T extends GetProductOperation>({
config,
variables,
}: {
query?: string
variables?: T['variables']
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
// Get fetch from the config
const { restBuyerFetch } = commerce.getConfig(config)
// Get a single product
const productPromise = restBuyerFetch<RawProduct>(
'GET',
`/me/products/${variables?.slug}`
)
// Get product specs
const specsPromise = restBuyerFetch<{ Items: RawSpec[] }>(
'GET',
`/me/products/${variables?.slug}/specs`
).then((res) => res.Items)
// Get product variants
const variantsPromise = restBuyerFetch<{ Items: RawVariant[] }>(
'GET',
`/me/products/${variables?.slug}/variants`
).then((res) => res.Items)
// Execute all promises in parallel
const [product, specs, variants] = await Promise.all([
productPromise,
specsPromise,
variantsPromise,
])
// Hydrate product
product.xp.Specs = specs
product.xp.Variants = variants
return {
// Normalize product to commerce schema
product: normalizeProduct(product),
}
}
return getProduct
}

View File

@@ -0,0 +1,46 @@
import type { OperationContext } from '@vercel/commerce/api/operations'
import type { Category, GetSiteInfoOperation } from '@vercel/commerce/types/site'
import type { RawCategory } from '../../types/category'
import type { OrdercloudConfig, Provider } from '../index'
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[]
brands: any[]
}
> = T
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo<T extends GetSiteInfoOperation>({
config,
}: {
query?: string
variables?: any
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
// Get fetch from the config
const { restBuyerFetch } = commerce.getConfig(config)
// Get list of categories
const rawCategories: RawCategory[] = await restBuyerFetch<{
Items: RawCategory[]
}>('GET', `/me/categories`).then((response) => response.Items)
return {
// Normalize categories
categories: rawCategories.map((category) => ({
id: category.ID,
name: category.Name,
slug: category.ID,
path: `/${category.ID}`,
})),
brands: [],
}
}
return getSiteInfo
}

View File

@@ -0,0 +1,6 @@
export { default as getAllPages } from './get-all-pages'
export { default as getPage } from './get-page'
export { default as getSiteInfo } from './get-site-info'
export { default as getProduct } from './get-product'
export { default as getAllProducts } from './get-all-products'
export { default as getAllProductPaths } from './get-all-product-paths'

View File

@@ -0,0 +1,41 @@
import type { Cart, OrdercloudCart, OrdercloudLineItem } from '../../types/cart'
export function formatCart(
cart: OrdercloudCart,
lineItems: OrdercloudLineItem[]
): Cart {
return {
id: cart.ID,
customerId: cart.FromUserID,
email: cart.FromUser.Email,
createdAt: cart.DateCreated,
currency: {
code: cart.FromUser?.xp?.currency ?? 'USD',
},
taxesIncluded: cart.TaxCost === 0,
lineItems: lineItems.map((lineItem) => ({
id: lineItem.ID,
variantId: lineItem.Variant ? String(lineItem.Variant.ID) : '',
productId: lineItem.ProductID,
name: lineItem.Product.Name,
quantity: lineItem.Quantity,
discounts: [],
path: lineItem.ProductID,
variant: {
id: lineItem.Variant ? String(lineItem.Variant.ID) : '',
sku: lineItem.ID,
name: lineItem.Product.Name,
image: {
url: lineItem.Product.xp?.Images?.[0]?.url,
},
requiresShipping: Boolean(lineItem.ShippingAddress),
price: lineItem.UnitPrice,
listPrice: lineItem.UnitPrice,
},
})),
lineItemsSubtotalPrice: cart.Subtotal,
subtotalPrice: cart.Subtotal,
totalPrice: cart.Total,
discounts: [],
}
}

View File

@@ -0,0 +1,14 @@
import type { GraphQLFetcher } from '@vercel/commerce/api'
import type { OrdercloudConfig } from '../'
import { FetcherError } from '@vercel/commerce/utils/errors'
const fetchGraphqlApi: (getConfig: () => OrdercloudConfig) => GraphQLFetcher =
() => async () => {
throw new FetcherError({
errors: [{ message: 'GraphQL fetch is not implemented' }],
status: 500,
})
}
export default fetchGraphqlApi

View File

@@ -0,0 +1,180 @@
import vercelFetch from '@vercel/fetch'
import { FetcherError } from '@vercel/commerce/utils/errors'
import { CustomNodeJsGlobal } from '../../types/node';
import { OrdercloudConfig } from '../index'
// Get an instance to vercel fetch
const fetch = vercelFetch()
// Get token util
async function getToken({
baseUrl,
clientId,
clientSecret,
}: {
baseUrl: string
clientId: string
clientSecret?: string
}): Promise<string> {
// If not, get a new one and store it
const authResponse = await fetch(`${baseUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: `client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`,
})
// If something failed getting the auth response
if (!authResponse.ok) {
// Get the body of it
const error = await authResponse.json()
// And return an error
throw new FetcherError({
errors: [{ message: error.error_description.Code }],
status: error.error_description.HttpStatus,
})
}
// Return the token
return authResponse
.json()
.then((response: { access_token: string }) => response.access_token)
}
export async function fetchData<T>(opts: {
token: string
path: string
method: string
config: OrdercloudConfig
fetchOptions?: Record<string, any>
body?: Record<string, unknown>
}): Promise<T> {
// Destructure opts
const { path, body, fetchOptions, config, token, method = 'GET' } = opts
// Do the request with the correct headers
const dataResponse = await fetch(
`${config.commerceUrl}/${config.apiVersion}${path}`,
{
...fetchOptions,
method,
headers: {
...fetchOptions?.headers,
'Content-Type': 'application/json',
accept: 'application/json, text/plain, */*',
authorization: `Bearer ${token}`,
},
body: body ? JSON.stringify(body) : undefined,
}
)
// If something failed getting the data response
if (!dataResponse.ok) {
// Get the body of it
const error = await dataResponse.textConverted()
// And return an error
throw new FetcherError({
errors: [{ message: error || dataResponse.statusText }],
status: dataResponse.status,
})
}
try {
// Return data response as json
return (await dataResponse.json()) as Promise<T>
} catch (error) {
// If response is empty return it as text
return null as unknown as Promise<T>
}
}
export const createMiddlewareFetcher: (
getConfig: () => OrdercloudConfig
) => <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T> =
(getConfig) =>
async <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => {
// Get provider config
const config = getConfig()
// Get a token
const token = await getToken({
baseUrl: config.commerceUrl,
clientId: process.env.ORDERCLOUD_MIDDLEWARE_CLIENT_ID as string,
clientSecret: process.env.ORDERCLOUD_MIDDLEWARE_CLIENT_SECRET,
})
// Return the data and specify the expected type
return fetchData<T>({
token,
fetchOptions,
method,
config,
path,
body,
})
}
export const createBuyerFetcher: (
getConfig: () => OrdercloudConfig
) => <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T> =
(getConfig) =>
async <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => {
const customGlobal = global as unknown as CustomNodeJsGlobal;
// Get provider config
const config = getConfig()
// If a token was passed, set it on global
if (fetchOptions?.token) {
customGlobal.token = fetchOptions.token
}
// Get a token
if (!customGlobal.token) {
customGlobal.token = await getToken({
baseUrl: config.commerceUrl,
clientId: process.env.ORDERCLOUD_BUYER_CLIENT_ID as string,
})
}
// Return the data and specify the expected type
const data = await fetchData<T>({
token: customGlobal.token as string,
fetchOptions,
config,
method,
path,
body,
})
return {
...data,
meta: { token: customGlobal.token as string },
}
}

View 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'

View 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 () {}
},
}

View 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 () => {},
}

View 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 }) =>
() =>
() => {},
}

View 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'

View File

@@ -0,0 +1,48 @@
import type { AddItemHook } from '@vercel/commerce/types/cart'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import { CommerceError } from '@vercel/commerce/utils/errors'
import useAddItem, { UseAddItem } from '@vercel/commerce/cart/use-add-item'
import useCart from './use-cart'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
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 }) =>
function useHook() {
const { mutate } = useCart()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,33 @@
import type { GetCartHook } from '@vercel/commerce/types/cart'
import { useMemo } from 'react'
import { SWRHook } from '@vercel/commerce/utils/types'
import useCart, { UseCart } from '@vercel/commerce/cart/use-cart'
export default useCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = {
fetchOptions: {
url: '/api/cart',
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?.lineItems?.length ?? 0) <= 0
},
enumerable: true,
},
}),
[response]
)
},
}

View File

@@ -0,0 +1,60 @@
import type {
MutationHookContext,
HookFetcherContext,
} from '@vercel/commerce/utils/types'
import type { Cart, LineItem, RemoveItemHook } from '@vercel/commerce/types/cart'
import { useCallback } from 'react'
import { ValidationError } from '@vercel/commerce/utils/errors'
import useRemoveItem, { UseRemoveItem } from '@vercel/commerce/cart/use-remove-item'
import useCart from './use-cart'
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
export type RemoveItemActionInput<T = any> = T extends LineItem
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/cart',
method: 'DELETE',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
return await fetch({ ...options, body: { itemId } })
},
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) =>
function useHook<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])
},
}

View File

@@ -0,0 +1,93 @@
import type {
HookFetcherContext,
MutationHookContext,
} from '@vercel/commerce/utils/types'
import type { UpdateItemHook, LineItem } from '@vercel/commerce/types/cart'
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import { MutationHook } from '@vercel/commerce/utils/types'
import { ValidationError } from '@vercel/commerce/utils/errors'
import useUpdateItem, { UseUpdateItem } from '@vercel/commerce/cart/use-update-item'
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<any>
export const handler: MutationHook<any> = {
fetchOptions: {
url: '/api/cart',
method: 'PUT',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateItemHook>) {
if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) {
return removeItemHandler.fetcher({
options: removeItemHandler.fetchOptions,
input: { itemId },
fetch,
})
}
} else if (item.quantity) {
throw new ValidationError({
message: 'The item quantity has to be a valid integer',
})
}
return await fetch({
...options,
body: { itemId, item },
})
},
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) =>
function useHook<T extends LineItem | undefined = undefined>(
ctx: {
item?: T
wait?: number
} = {}
) {
const { item } = ctx
const { mutate } = useCart() as any
return useCallback(
debounce(async (input: UpdateItemActionInput<T>) => {
const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId
if (!itemId || !productId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({
input: {
itemId,
item: {
productId,
variantId: variantId || '',
quantity: input.quantity,
},
},
})
await mutate(data, false)
return data
}, ctx.wait ?? 500),
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,2 @@
export { default as useSubmitCheckout } from './use-submit-checkout'
export { default as useCheckout } from './use-checkout'

View File

@@ -0,0 +1,41 @@
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'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<GetCheckoutHook> = {
fetchOptions: {
url: '/api/checkout',
method: 'GET',
},
useHook: ({ useData }) =>
function useHook(input) {
const submit = useSubmitCheckout()
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems?.length ?? 0) <= 0
},
enumerable: true,
},
submit: {
get() {
return submit
},
enumerable: true,
},
}),
[response, submit]
)
},
}

View File

@@ -0,0 +1,38 @@
import type { SubmitCheckoutHook } from '@vercel/commerce/types/checkout'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
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 }) {
// @TODO: Make form validations in here, import generic error like import { CommerceError } from '@vercel/commerce/utils/errors'
// Get payment and delivery information in here
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
},
[fetch]
)
},
}

View File

@@ -0,0 +1,10 @@
{
"provider": "ordercloud",
"features": {
"wishlist": false,
"cart": true,
"search": true,
"customerAuth": false,
"customCheckout": true
}
}

View File

@@ -0,0 +1,6 @@
export const CART_COOKIE = 'ordercloud.cart'
export const TOKEN_COOKIE = 'ordercloud.token'
export const CUSTOMER_COOKIE = 'ordercloud.customer'
export const API_URL = 'https://sandboxapi.ordercloud.io'
export const API_VERSION = 'v1'
export const LOCALE = 'en-us'

View File

@@ -0,0 +1,4 @@
export { default as useAddresses } from './use-addresses'
export { default as useAddItem } from './use-add-item'
export { default as useRemoveItem } from './use-remove-item'
export { default as useUpdateItem } from './use-update-item'

View File

@@ -0,0 +1,38 @@
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 useAddresses from './use-addresses'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
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 { mutate } = useAddresses()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate([data], false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,35 @@
import type { GetAddressesHook } from '@vercel/commerce/types/customer/address'
import { useMemo } from 'react'
import { SWRHook } from '@vercel/commerce/utils/types'
import useAddresses, {
UseAddresses,
} from '@vercel/commerce/customer/address/use-addresses'
export default useAddresses as UseAddresses<typeof handler>
export const handler: SWRHook<GetAddressesHook> = {
fetchOptions: {
url: '/api/customer/address',
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]
)
},
}

View File

@@ -0,0 +1,62 @@
import type {
MutationHookContext,
HookFetcherContext,
} from '@vercel/commerce/utils/types'
import type { Address, RemoveItemHook } from '@vercel/commerce/types/customer/address'
import { useCallback } from 'react'
import { ValidationError } from '@vercel/commerce/utils/errors'
import useRemoveItem, {
UseRemoveItem,
} from '@vercel/commerce/customer/address/use-remove-item'
import useAddresses from './use-addresses'
export type RemoveItemFn<T = any> = T extends Address
? (input?: RemoveItemActionInput<T>) => Promise<Address | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Address | null>
export type RemoveItemActionInput<T = any> = T extends Address
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/customer/address',
method: 'DELETE',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
return await fetch({ ...options, body: { itemId } })
},
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) =>
function useHook<T extends Address | undefined = undefined>(
ctx: { item?: T } = {}
) {
const { item } = ctx
const { mutate } = useAddresses()
const removeItem: RemoveItemFn<Address> = 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([], false)
return data
}
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
}

View File

@@ -0,0 +1,52 @@
import type {
HookFetcherContext,
MutationHookContext,
} from '@vercel/commerce/utils/types'
import type { UpdateItemHook, Address } from '@vercel/commerce/types/customer/address'
import { useCallback } from 'react'
import { MutationHook } from '@vercel/commerce/utils/types'
import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/customer/address/use-update-item'
import useAddresses from './use-addresses'
export type UpdateItemActionInput<T = any> = T extends Address
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = {
fetchOptions: {
url: '/api/customer/address',
method: 'PUT',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateItemHook>) {
return await fetch({
...options,
body: { itemId, item },
})
},
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) =>
function useHook() {
const { mutate } = useAddresses()
return useCallback(
async function updateItem(input) {
const data = await fetch({ input })
await mutate([], false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,4 @@
export { default as useCards } from './use-cards'
export { default as useAddItem } from './use-add-item'
export { default as useRemoveItem } from './use-remove-item'
export { default as useUpdateItem } from './use-update-item'

View File

@@ -0,0 +1,38 @@
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 useCards from './use-cards'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '/api/customer/card',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {
const data = await fetch({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
const { mutate } = useCards()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate([data], false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,33 @@
import type { GetCardsHook } from '@vercel/commerce/types/customer/card'
import { useMemo } from 'react'
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]
)
},
}

View File

@@ -0,0 +1,62 @@
import type {
MutationHookContext,
HookFetcherContext,
} from '@vercel/commerce/utils/types'
import type { Card, RemoveItemHook } from '@vercel/commerce/types/customer/card'
import { useCallback } from 'react'
import { ValidationError } from '@vercel/commerce/utils/errors'
import useRemoveItem, {
UseRemoveItem,
} from '@vercel/commerce/customer/card/use-remove-item'
import useCards from './use-cards'
export type RemoveItemFn<T = any> = T extends Card
? (input?: RemoveItemActionInput<T>) => Promise<Card | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Card | null>
export type RemoveItemActionInput<T = any> = T extends Card
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/customer/card',
method: 'DELETE',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
return await fetch({ ...options, body: { itemId } })
},
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) =>
function useHook<T extends Card | undefined = undefined>(
ctx: { item?: T } = {}
) {
const { item } = ctx
const { mutate } = useCards()
const removeItem: RemoveItemFn<Card> = 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([], false)
return data
}
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
}

View File

@@ -0,0 +1,52 @@
import type {
HookFetcherContext,
MutationHookContext,
} from '@vercel/commerce/utils/types'
import type { UpdateItemHook, Card } from '@vercel/commerce/types/customer/card'
import { useCallback } from 'react'
import { MutationHook } from '@vercel/commerce/utils/types'
import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/customer/card/use-update-item'
import useCards from './use-cards'
export type UpdateItemActionInput<T = any> = T extends Card
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = {
fetchOptions: {
url: '/api/customer/card',
method: 'PUT',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateItemHook>) {
return await fetch({
...options,
body: { itemId, item },
})
},
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) =>
function useHook() {
const { mutate } = useCards()
return useCallback(
async function updateItem(input) {
const data = await fetch({ input })
await mutate([], false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1 @@
export { default as useCustomer } from './use-customer'

View File

@@ -0,0 +1,15 @@
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 {}
}
},
}

View File

@@ -0,0 +1,17 @@
import { Fetcher } from '@vercel/commerce/utils/types'
const clientFetcher: Fetcher = async ({ method, url, body }) => {
const response = await fetch(url!, {
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => response.json())
.then((response) => response.data)
return response
}
export default clientFetcher

View File

@@ -0,0 +1,9 @@
import { ordercloudProvider, OrdercloudProvider } from './provider'
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@vercel/commerce'
export { ordercloudProvider }
export type { OrdercloudProvider }
export const CommerceProvider = getCommerceProvider(ordercloudProvider)
export const useCommerce = () => useCoreCommerce()

View File

@@ -0,0 +1,8 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
images: {
domains: ['localhost', 'ocdevops.blob.core.windows.net'],
},
}

View File

@@ -0,0 +1,2 @@
export { default as usePrice } from './use-price'
export { default as useSearch } from './use-search'

View File

@@ -0,0 +1,2 @@
export * from '@vercel/commerce/product/use-price'
export { default } from '@vercel/commerce/product/use-price'

View File

@@ -0,0 +1,41 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useSearch, { UseSearch } from '@vercel/commerce/product/use-search'
import { SearchProductsHook } from '@vercel/commerce/types/product'
export default useSearch as UseSearch<typeof handler>
export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: {
url: '/api/catalog/products',
method: 'GET',
},
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', String(search))
if (categoryId) url.searchParams.set('categoryId', String(categoryId))
if (brandId) url.searchParams.set('brandId', String(brandId))
if (sort) url.searchParams.set('sort', String(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,
},
})
},
}

View File

@@ -0,0 +1,62 @@
import { handler as useCart } from './cart/use-cart'
import { handler as useAddCartItem } from './cart/use-add-item'
import { handler as useUpdateCartItem } from './cart/use-update-item'
import { handler as useRemoveCartItem } 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 useCards } from './customer/card/use-cards'
import { handler as useAddCardItem } from './customer/card/use-add-item'
import { handler as useUpdateCardItem } from './customer/card/use-update-item'
import { handler as useRemoveCardItem } from './customer/card/use-remove-item'
import { handler as useAddresses } from './customer/address/use-addresses'
import { handler as useAddAddressItem } from './customer/address/use-add-item'
import { handler as useUpdateAddressItem } from './customer/address/use-update-item'
import { handler as useRemoveAddressItem } from './customer/address/use-remove-item'
import { CART_COOKIE, LOCALE } from './constants'
import { default as fetcher } from './fetcher'
export const ordercloudProvider = {
locale: LOCALE,
cartCookie: CART_COOKIE,
fetcher,
cart: {
useCart,
useAddItem: useAddCartItem,
useUpdateItem: useUpdateCartItem,
useRemoveItem: useRemoveCartItem,
},
checkout: {
useCheckout,
useSubmitCheckout,
},
customer: {
useCustomer,
card: {
useCards,
useAddItem: useAddCardItem,
useUpdateItem: useUpdateCardItem,
useRemoveItem: useRemoveCardItem,
},
address: {
useAddresses,
useAddItem: useAddAddressItem,
useUpdateItem: useUpdateAddressItem,
useRemoveItem: useRemoveAddressItem,
},
},
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}
export type OrdercloudProvider = typeof ordercloudProvider

View File

@@ -0,0 +1,126 @@
import * as Core from '@vercel/commerce/types/cart'
export * from '@vercel/commerce/types/cart'
export interface OrdercloudCart {
ID: string
FromUser: {
ID: string
Username: string
Password: null
FirstName: string
LastName: string
Email: string
Phone: null
TermsAccepted: null
Active: true
xp: {
something: string
currency: string
}
AvailableRoles: null
DateCreated: string
PasswordLastSetDate: null
}
FromCompanyID: string
ToCompanyID: string
FromUserID: string
BillingAddressID: null
BillingAddress: null
ShippingAddressID: null
Comments: null
LineItemCount: number
Status: string
DateCreated: string
DateSubmitted: null
DateApproved: null
DateDeclined: null
DateCanceled: null
DateCompleted: null
LastUpdated: string
Subtotal: number
ShippingCost: number
TaxCost: number
PromotionDiscount: number
Total: number
IsSubmitted: false
xp: {
productId: string
variantId: string
quantity: 1
}
}
export interface OrdercloudLineItem {
ID: string
ProductID: string
Quantity: 1
DateAdded: string
QuantityShipped: number
UnitPrice: number
PromotionDiscount: number
LineTotal: number
LineSubtotal: number
CostCenter: null
DateNeeded: null
ShippingAccount: null
ShippingAddressID: null
ShipFromAddressID: null
Product: {
ID: string
Name: string
Description: string
QuantityMultiplier: number
ShipWeight: number
ShipHeight: null
ShipWidth: null
ShipLength: null
xp: {
Images: {
url: string
}[]
}
}
Variant: null | {
ID: string
Name: null
Description: null
ShipWeight: null
ShipHeight: null
ShipWidth: null
ShipLength: null
xp: null
}
ShippingAddress: null
ShipFromAddress: null
SupplierID: null
Specs: []
xp: null
}
/**
* Extend core cart types
*/
export type Cart = Core.Cart & {
lineItems: Core.LineItem[]
url?: string
}
export type CartTypes = Core.CartTypes
export type CartHooks = Core.CartHooks<CartTypes>
export type GetCartHook = CartHooks['getCart']
export type AddItemHook = CartHooks['addItem']
export type UpdateItemHook = CartHooks['updateItem']
export type RemoveItemHook = CartHooks['removeItem']
export type CartSchema = Core.CartSchema<CartTypes>
export type CartHandlers = Core.CartHandlers<CartTypes>
export type GetCartHandler = CartHandlers['getCart']
export type AddItemHandler = CartHandlers['addItem']
export type UpdateItemHandler = CartHandlers['updateItem']
export type RemoveItemHandler = CartHandlers['removeItem']

View File

@@ -0,0 +1,10 @@
export interface RawCategory {
ID: string
Name: string
Description: null | string
ListOrder: number
Active: boolean
ParentID: null
ChildCount: number
xp: null
}

View File

@@ -0,0 +1,4 @@
import * as Core from '@vercel/commerce/types/checkout'
export type CheckoutTypes = Core.CheckoutTypes
export type CheckoutSchema = Core.CheckoutSchema<CheckoutTypes>

View File

@@ -0,0 +1,32 @@
import * as Core from '@vercel/commerce/types/customer/address'
export type CustomerAddressTypes = Core.CustomerAddressTypes
export type CustomerAddressSchema =
Core.CustomerAddressSchema<CustomerAddressTypes>
export interface OrdercloudAddress {
ID: string
FromCompanyID: string
ToCompanyID: string
FromUserID: string
BillingAddressID: null
BillingAddress: null
ShippingAddressID: null
Comments: null
LineItemCount: number
Status: string
DateCreated: string
DateSubmitted: null
DateApproved: null
DateDeclined: null
DateCanceled: null
DateCompleted: null
LastUpdated: string
Subtotal: number
ShippingCost: number
TaxCost: number
PromotionDiscount: number
Total: number
IsSubmitted: false
xp: null
}

View File

@@ -0,0 +1,16 @@
import * as Core from '@vercel/commerce/types/customer/card'
export type CustomerCardTypes = Core.CustomerCardTypes
export type CustomerCardSchema = Core.CustomerCardSchema<CustomerCardTypes>
export interface OredercloudCreditCard {
ID: string
Editable: boolean
Token: string
DateCreated: string
CardType: string
PartialAccountNumber: string
CardholderName: string
ExpirationDate: string
xp: null
}

View File

@@ -0,0 +1,3 @@
export interface CustomNodeJsGlobal extends NodeJS.Global {
token: string | null | undefined
}

View File

@@ -0,0 +1,55 @@
interface RawVariantSpec {
SpecID: string
Name: string
OptionID: string
Value: string
PriceMarkupType: string
PriceMarkup: string | null
}
export interface RawSpec {
ID: string
Name: string
Options: {
ID: string
Value: string
xp: {
hexColor?: string
}
}[]
}
export interface RawVariant {
ID: string
Specs: RawVariantSpec[]
}
export interface RawProduct {
OwnerID: string
DefaultPriceScheduleID: string | null
AutoForward: boolean
ID: string
Name: string
Description: string
QuantityMultiplier: number
ShipWeight: null
ShipHeight: null
ShipWidth: null
ShipLength: null
Active: boolean
SpecCount: number
VariantCount: number
ShipFromAddressID: null
Inventory: null
DefaultSupplierID: null
AllSuppliersCanSell: boolean
xp: {
Price: number
PriceCurrency: string
Images: {
url: string
}[]
Variants?: RawVariant[]
Specs?: RawSpec[]
}
}

View File

@@ -0,0 +1,47 @@
import type { Product } from '@vercel/commerce/types/product'
import type { RawProduct } from '../types/product'
export function normalize(product: RawProduct): Product {
return {
id: product.ID,
name: product.Name,
description: product.Description,
slug: product.ID,
images: product.xp.Images,
price: {
value: product.xp.Price,
currencyCode: product.xp.PriceCurrency,
},
variants: product.xp.Variants?.length
? product.xp.Variants.map((variant) => ({
id: variant.ID,
options: variant.Specs.map((spec) => ({
id: spec.SpecID,
__typename: 'MultipleChoiceOption',
displayName: spec.Name,
values: [
{
label: spec.Value,
},
],
})),
}))
: [
{
id: '',
options: [],
},
],
options: product.xp.Specs?.length
? product.xp.Specs.map((spec) => ({
id: spec.ID,
displayName: spec.Name,
values: spec.Options.map((option) => ({
label: option.Value,
...(option.xp?.hexColor && { hexColors: [option.xp.hexColor] }),
})),
}))
: [],
}
}

View File

@@ -0,0 +1,13 @@
import { useCallback } from 'react'
export function emptyHook() {
const useEmptyHook = async (options = {}) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@@ -0,0 +1,17 @@
import { useCallback } from 'react'
type Options = {
includeProducts?: boolean
}
export function emptyHook(options?: Options) {
const useEmptyHook = async ({ id }: { id: string | number }) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@@ -0,0 +1,43 @@
import { HookFetcher } from '@vercel/commerce/utils/types'
import type { Product } from '@vercel/commerce/types/product'
const defaultOpts = {}
export type Wishlist = {
items: [
{
product_id: number
variant_id: number
id: number
product: Product
}
]
}
export interface UseWishlistOptions {
includeProducts?: boolean
}
export interface UseWishlistInput extends UseWishlistOptions {
customerId?: number
}
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = () => {
return null
}
export function extendHook(
customFetcher: typeof fetcher,
// swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
swrOptions?: any
) {
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
return { data: null }
}
useWishlist.extend = extendHook
return useWishlist
}
export default extendHook(fetcher)

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"outDir": "dist",
"baseUrl": "src",
"lib": ["dom", "dom.iterable", "esnext"],
"declaration": true,
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"incremental": true,
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}