From 3f0c38461b478a48520bd91ba9240bfec478b7fb Mon Sep 17 00:00:00 2001
From: Gonzalo Pozzo <gonzalo.pozzo4@gmail.com>
Date: Tue, 5 Oct 2021 09:49:01 -0300
Subject: [PATCH] Add ordercloud provider (#500)

* Add ordercloud provider

* Fix provider errors

* Make submit checkout optional

* Make submit checkout optional

* Remove nullables when creating endpoint type

* Update readme

* Log checkout error

* Log error

* Save token to cookie

* Update fetch rest

* Use token at checkout

Co-authored-by: Luis Alvarez <luis@vercel.com>
---
 .env.template                                 |   4 +
 framework/commerce/api/index.ts               |   4 +-
 framework/commerce/config.js                  |   1 +
 framework/commerce/types/checkout.ts          |   2 +-
 framework/commerce/types/customer/address.ts  |  98 ++++++----
 framework/commerce/types/customer/card.ts     |  72 +++----
 framework/ordercloud/.env.template            |   5 +
 framework/ordercloud/README.md                |   3 +
 .../ordercloud/api/endpoints/cart/add-item.ts |  99 ++++++++++
 .../ordercloud/api/endpoints/cart/get-cart.ts |  65 +++++++
 .../ordercloud/api/endpoints/cart/index.ts    |  28 +++
 .../api/endpoints/cart/remove-item.ts         |  45 +++++
 .../api/endpoints/cart/update-item.ts         |  63 +++++++
 .../ordercloud/api/endpoints/catalog/index.ts |   1 +
 .../api/endpoints/catalog/products.ts         |   1 +
 .../api/endpoints/checkout/get-checkout.ts    |  47 +++++
 .../api/endpoints/checkout/index.ts           |  23 +++
 .../api/endpoints/checkout/submit-checkout.ts |  32 ++++
 .../endpoints/customer/address/add-item.ts    |  47 +++++
 .../customer/address/get-addresses.ts         |   9 +
 .../api/endpoints/customer/address/index.ts   |  27 +++
 .../endpoints/customer/address/remove-item.ts |   9 +
 .../endpoints/customer/address/update-item.ts |   9 +
 .../api/endpoints/customer/card/add-item.ts   |  74 ++++++++
 .../api/endpoints/customer/card/get-cards.ts  |   9 +
 .../api/endpoints/customer/card/index.ts      |  27 +++
 .../endpoints/customer/card/remove-item.ts    |   9 +
 .../endpoints/customer/card/update-item.ts    |   9 +
 .../api/endpoints/customer/index.ts           |   1 +
 .../ordercloud/api/endpoints/login/index.ts   |   1 +
 .../ordercloud/api/endpoints/logout/index.ts  |   1 +
 .../ordercloud/api/endpoints/signup/index.ts  |   1 +
 .../api/endpoints/wishlist/index.tsx          |   1 +
 framework/ordercloud/api/index.ts             |  71 +++++++
 .../api/operations/get-all-pages.ts           |  22 +++
 .../api/operations/get-all-product-paths.ts   |  34 ++++
 .../api/operations/get-all-products.ts        |  35 ++++
 .../ordercloud/api/operations/get-page.ts     |  15 ++
 .../ordercloud/api/operations/get-product.ts  |  60 ++++++
 .../api/operations/get-site-info.ts           |  46 +++++
 framework/ordercloud/api/operations/index.ts  |   6 +
 framework/ordercloud/api/utils/cart.ts        |  41 ++++
 .../ordercloud/api/utils/fetch-graphql.ts     |  14 ++
 framework/ordercloud/api/utils/fetch-rest.ts  | 176 ++++++++++++++++++
 framework/ordercloud/auth/index.ts            |   3 +
 framework/ordercloud/auth/use-login.tsx       |  16 ++
 framework/ordercloud/auth/use-logout.tsx      |  17 ++
 framework/ordercloud/auth/use-signup.tsx      |  19 ++
 framework/ordercloud/cart/index.ts            |   4 +
 framework/ordercloud/cart/use-add-item.tsx    |  48 +++++
 framework/ordercloud/cart/use-cart.tsx        |  33 ++++
 framework/ordercloud/cart/use-remove-item.tsx |  60 ++++++
 framework/ordercloud/cart/use-update-item.tsx |  93 +++++++++
 framework/ordercloud/checkout/index.ts        |   2 +
 .../ordercloud/checkout/use-checkout.tsx      |  41 ++++
 .../checkout/use-submit-checkout.tsx          |  36 ++++
 framework/ordercloud/commerce.config.json     |  10 +
 framework/ordercloud/constants.ts             |   6 +
 .../ordercloud/customer/address/index.ts      |   4 +
 .../customer/address/use-add-item.tsx         |  38 ++++
 .../customer/address/use-addresses.tsx        |  35 ++++
 .../customer/address/use-remove-item.tsx      |  62 ++++++
 .../customer/address/use-update-item.tsx      |  52 ++++++
 framework/ordercloud/customer/card/index.ts   |   4 +
 .../ordercloud/customer/card/use-add-item.tsx |  38 ++++
 .../ordercloud/customer/card/use-cards.tsx    |  33 ++++
 .../customer/card/use-remove-item.tsx         |  62 ++++++
 .../customer/card/use-update-item.tsx         |  52 ++++++
 framework/ordercloud/customer/index.ts        |   1 +
 .../ordercloud/customer/use-customer.tsx      |  15 ++
 framework/ordercloud/fetcher.ts               |  17 ++
 framework/ordercloud/index.tsx                |   9 +
 framework/ordercloud/next.config.js           |   8 +
 framework/ordercloud/product/index.ts         |   2 +
 framework/ordercloud/product/use-price.tsx    |   2 +
 framework/ordercloud/product/use-search.tsx   |  17 ++
 framework/ordercloud/provider.ts              |  62 ++++++
 framework/ordercloud/types/cart.ts            | 126 +++++++++++++
 framework/ordercloud/types/category.ts        |  10 +
 framework/ordercloud/types/checkout.ts        |   4 +
 .../ordercloud/types/customer/address.ts      |  31 +++
 framework/ordercloud/types/customer/card.ts   |  16 ++
 framework/ordercloud/types/node.d.ts          |   5 +
 framework/ordercloud/types/product.ts         |  55 ++++++
 framework/ordercloud/utils/product.ts         |  47 +++++
 .../ordercloud/wishlist/use-add-item.tsx      |  13 ++
 .../ordercloud/wishlist/use-remove-item.tsx   |  17 ++
 .../ordercloud/wishlist/use-wishlist.tsx      |  43 +++++
 package.json                                  |   1 +
 yarn.lock                                     |  20 ++
 90 files changed, 2560 insertions(+), 76 deletions(-)
 create mode 100644 framework/ordercloud/.env.template
 create mode 100644 framework/ordercloud/README.md
 create mode 100644 framework/ordercloud/api/endpoints/cart/add-item.ts
 create mode 100644 framework/ordercloud/api/endpoints/cart/get-cart.ts
 create mode 100644 framework/ordercloud/api/endpoints/cart/index.ts
 create mode 100644 framework/ordercloud/api/endpoints/cart/remove-item.ts
 create mode 100644 framework/ordercloud/api/endpoints/cart/update-item.ts
 create mode 100644 framework/ordercloud/api/endpoints/catalog/index.ts
 create mode 100644 framework/ordercloud/api/endpoints/catalog/products.ts
 create mode 100644 framework/ordercloud/api/endpoints/checkout/get-checkout.ts
 create mode 100644 framework/ordercloud/api/endpoints/checkout/index.ts
 create mode 100644 framework/ordercloud/api/endpoints/checkout/submit-checkout.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/address/add-item.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/address/get-addresses.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/address/index.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/address/remove-item.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/address/update-item.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/card/add-item.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/card/get-cards.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/card/index.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/card/remove-item.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/card/update-item.ts
 create mode 100644 framework/ordercloud/api/endpoints/customer/index.ts
 create mode 100644 framework/ordercloud/api/endpoints/login/index.ts
 create mode 100644 framework/ordercloud/api/endpoints/logout/index.ts
 create mode 100644 framework/ordercloud/api/endpoints/signup/index.ts
 create mode 100644 framework/ordercloud/api/endpoints/wishlist/index.tsx
 create mode 100644 framework/ordercloud/api/index.ts
 create mode 100644 framework/ordercloud/api/operations/get-all-pages.ts
 create mode 100644 framework/ordercloud/api/operations/get-all-product-paths.ts
 create mode 100644 framework/ordercloud/api/operations/get-all-products.ts
 create mode 100644 framework/ordercloud/api/operations/get-page.ts
 create mode 100644 framework/ordercloud/api/operations/get-product.ts
 create mode 100644 framework/ordercloud/api/operations/get-site-info.ts
 create mode 100644 framework/ordercloud/api/operations/index.ts
 create mode 100644 framework/ordercloud/api/utils/cart.ts
 create mode 100644 framework/ordercloud/api/utils/fetch-graphql.ts
 create mode 100644 framework/ordercloud/api/utils/fetch-rest.ts
 create mode 100644 framework/ordercloud/auth/index.ts
 create mode 100644 framework/ordercloud/auth/use-login.tsx
 create mode 100644 framework/ordercloud/auth/use-logout.tsx
 create mode 100644 framework/ordercloud/auth/use-signup.tsx
 create mode 100644 framework/ordercloud/cart/index.ts
 create mode 100644 framework/ordercloud/cart/use-add-item.tsx
 create mode 100644 framework/ordercloud/cart/use-cart.tsx
 create mode 100644 framework/ordercloud/cart/use-remove-item.tsx
 create mode 100644 framework/ordercloud/cart/use-update-item.tsx
 create mode 100644 framework/ordercloud/checkout/index.ts
 create mode 100644 framework/ordercloud/checkout/use-checkout.tsx
 create mode 100644 framework/ordercloud/checkout/use-submit-checkout.tsx
 create mode 100644 framework/ordercloud/commerce.config.json
 create mode 100644 framework/ordercloud/constants.ts
 create mode 100644 framework/ordercloud/customer/address/index.ts
 create mode 100644 framework/ordercloud/customer/address/use-add-item.tsx
 create mode 100644 framework/ordercloud/customer/address/use-addresses.tsx
 create mode 100644 framework/ordercloud/customer/address/use-remove-item.tsx
 create mode 100644 framework/ordercloud/customer/address/use-update-item.tsx
 create mode 100644 framework/ordercloud/customer/card/index.ts
 create mode 100644 framework/ordercloud/customer/card/use-add-item.tsx
 create mode 100644 framework/ordercloud/customer/card/use-cards.tsx
 create mode 100644 framework/ordercloud/customer/card/use-remove-item.tsx
 create mode 100644 framework/ordercloud/customer/card/use-update-item.tsx
 create mode 100644 framework/ordercloud/customer/index.ts
 create mode 100644 framework/ordercloud/customer/use-customer.tsx
 create mode 100644 framework/ordercloud/fetcher.ts
 create mode 100644 framework/ordercloud/index.tsx
 create mode 100644 framework/ordercloud/next.config.js
 create mode 100644 framework/ordercloud/product/index.ts
 create mode 100644 framework/ordercloud/product/use-price.tsx
 create mode 100644 framework/ordercloud/product/use-search.tsx
 create mode 100644 framework/ordercloud/provider.ts
 create mode 100644 framework/ordercloud/types/cart.ts
 create mode 100644 framework/ordercloud/types/category.ts
 create mode 100644 framework/ordercloud/types/checkout.ts
 create mode 100644 framework/ordercloud/types/customer/address.ts
 create mode 100644 framework/ordercloud/types/customer/card.ts
 create mode 100644 framework/ordercloud/types/node.d.ts
 create mode 100644 framework/ordercloud/types/product.ts
 create mode 100644 framework/ordercloud/utils/product.ts
 create mode 100644 framework/ordercloud/wishlist/use-add-item.tsx
 create mode 100644 framework/ordercloud/wishlist/use-remove-item.tsx
 create mode 100644 framework/ordercloud/wishlist/use-wishlist.tsx

diff --git a/.env.template b/.env.template
index b24458b80..a5885494e 100644
--- a/.env.template
+++ b/.env.template
@@ -23,3 +23,7 @@ NEXT_PUBLIC_SALEOR_CHANNEL=
 
 NEXT_PUBLIC_VENDURE_SHOP_API_URL=
 NEXT_PUBLIC_VENDURE_LOCAL_URL=
+
+ORDERCLOUD_CLIENT_ID=
+ORDERCLOUD_CLIENT_SECRET=
+STRIPE_SECRET=
diff --git a/framework/commerce/api/index.ts b/framework/commerce/api/index.ts
index 716c11ed5..d7ba37059 100644
--- a/framework/commerce/api/index.ts
+++ b/framework/commerce/api/index.ts
@@ -65,8 +65,8 @@ export type EndpointHandlers<
   [H in keyof E['handlers']]: APIHandler<
     C,
     EndpointHandlers<C, E>,
-    E['handlers'][H]['data'],
-    E['handlers'][H]['body'],
+    NonNullable<E['handlers'][H]>['data'],
+    NonNullable<E['handlers'][H]>['body'],
     E['options']
   >
 }
diff --git a/framework/commerce/config.js b/framework/commerce/config.js
index 019c59a51..7fd0536f8 100644
--- a/framework/commerce/config.js
+++ b/framework/commerce/config.js
@@ -14,6 +14,7 @@ const PROVIDERS = [
   'shopify',
   'swell',
   'vendure',
+  'ordercloud',
 ]
 
 function getProviderName() {
diff --git a/framework/commerce/types/checkout.ts b/framework/commerce/types/checkout.ts
index 58b895368..d75b63902 100644
--- a/framework/commerce/types/checkout.ts
+++ b/framework/commerce/types/checkout.ts
@@ -30,7 +30,7 @@ export type GetCheckoutHook<T extends CheckoutTypes = CheckoutTypes> = {
 }
 
 export type CheckoutHooks<T extends CheckoutTypes = CheckoutTypes> = {
-  submitCheckout: SubmitCheckoutHook<T>
+  submitCheckout?: SubmitCheckoutHook<T>
   getCheckout: GetCheckoutHook<T>
 }
 
diff --git a/framework/commerce/types/customer/address.ts b/framework/commerce/types/customer/address.ts
index 5b6ca4b49..8dc6ffc0d 100644
--- a/framework/commerce/types/customer/address.ts
+++ b/framework/commerce/types/customer/address.ts
@@ -1,41 +1,46 @@
 export interface Address {
-  id: string;
-  mask: string;
+  id: string
+  mask: string
 }
 
 export interface AddressFields {
-  type: string;
-  firstName: string;
-  lastName: string;
-  company: string;
-  streetNumber: string;
-  apartments: string;
-  zipCode: string;
-  city: string;
-  country: string;
+  type: string
+  firstName: string
+  lastName: string
+  company: string
+  streetNumber: string
+  apartments: string
+  zipCode: string
+  city: string
+  country: string
 }
 
 export type CustomerAddressTypes = {
-  address?: Address;
-  fields: AddressFields;
+  address?: Address
+  fields: AddressFields
 }
 
-export type GetAddressesHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
-  data: T['address'] | null
+export type GetAddressesHook<
+  T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
+  data: T['address'][] | null
   input: {}
   fetcherInput: { cartId?: string }
   swrState: { isEmpty: boolean }
 }
 
-export type AddItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
-  data: T['address']
-  input?: T['fields']
-  fetcherInput: T['fields']
-  body: { item: T['fields'] }
-  actionInput: T['fields']
-}
+export type AddItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> =
+  {
+    data: T['address']
+    input?: T['fields']
+    fetcherInput: T['fields']
+    body: { item: T['fields'] }
+    actionInput: T['fields']
+  }
 
-export type UpdateItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
+export type UpdateItemHook<
+  T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
   data: T['address'] | null
   input: { item?: T['fields']; wait?: number }
   fetcherInput: { itemId: string; item: T['fields'] }
@@ -43,49 +48,62 @@ export type UpdateItemHook<T extends CustomerAddressTypes = CustomerAddressTypes
   actionInput: T['fields'] & { id: string }
 }
 
-export type RemoveItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
+export type RemoveItemHook<
+  T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
   data: T['address'] | null
-  input: { item?: T['fields'] }
+  input: { item?: T['address'] }
   fetcherInput: { itemId: string }
   body: { itemId: string }
   actionInput: { id: string }
 }
 
-export type CustomerAddressHooks<T extends CustomerAddressTypes = CustomerAddressTypes> = {
+export type CustomerAddressHooks<
+  T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
   getAddresses: GetAddressesHook<T>
   addItem: AddItemHook<T>
   updateItem: UpdateItemHook<T>
   removeItem: RemoveItemHook<T>
 }
 
-export type AddresssHandler<T extends CustomerAddressTypes = CustomerAddressTypes> = GetAddressesHook<T> & {
+export type AddressHandler<
+  T extends CustomerAddressTypes = CustomerAddressTypes
+> = GetAddressesHook<T> & {
   body: { cartId?: string }
 }
 
-export type AddItemHandler<T extends CustomerAddressTypes = CustomerAddressTypes> = AddItemHook<T> & {
+export type AddItemHandler<
+  T extends CustomerAddressTypes = CustomerAddressTypes
+> = AddItemHook<T> & {
   body: { cartId: string }
 }
 
-export type UpdateItemHandler<T extends CustomerAddressTypes = CustomerAddressTypes> =
-  UpdateItemHook<T> & {
-    data: T['address']
-    body: { cartId: string }
-  }
+export type UpdateItemHandler<
+  T extends CustomerAddressTypes = CustomerAddressTypes
+> = UpdateItemHook<T> & {
+  data: T['address']
+  body: { cartId: string }
+}
 
-export type RemoveItemHandler<T extends CustomerAddressTypes = CustomerAddressTypes> =
-  RemoveItemHook<T> & {
-    body: { cartId: string }
-  }
+export type RemoveItemHandler<
+  T extends CustomerAddressTypes = CustomerAddressTypes
+> = RemoveItemHook<T> & {
+  body: { cartId: string }
+}
 
-
-export type CustomerAddressHandlers<T extends CustomerAddressTypes = CustomerAddressTypes> = {
+export type CustomerAddressHandlers<
+  T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
   getAddresses: GetAddressesHook<T>
   addItem: AddItemHandler<T>
   updateItem: UpdateItemHandler<T>
   removeItem: RemoveItemHandler<T>
 }
 
-export type CustomerAddressSchema<T extends CustomerAddressTypes = CustomerAddressTypes> = {
+export type CustomerAddressSchema<
+  T extends CustomerAddressTypes = CustomerAddressTypes
+> = {
   endpoint: {
     options: {}
     handlers: CustomerAddressHandlers<T>
diff --git a/framework/commerce/types/customer/card.ts b/framework/commerce/types/customer/card.ts
index a8731411f..e9b220dcc 100644
--- a/framework/commerce/types/customer/card.ts
+++ b/framework/commerce/types/customer/card.ts
@@ -1,30 +1,30 @@
 export interface Card {
-  id: string;
-  mask: string;
-  provider: string;
+  id: string
+  mask: string
+  provider: string
 }
 
 export interface CardFields {
-  cardHolder: string;
-  cardNumber: string;
-  cardExpireDate: string;
-  cardCvc: string;
-  firstName: string;
-  lastName: string;
-  company: string;
-  streetNumber: string;
-  zipCode: string;
-  city: string;
-  country: string;
+  cardHolder: string
+  cardNumber: string
+  cardExpireDate: string
+  cardCvc: string
+  firstName: string
+  lastName: string
+  company: string
+  streetNumber: string
+  zipCode: string
+  city: string
+  country: string
 }
 
 export type CustomerCardTypes = {
-  card?: Card;
-  fields: CardFields;
+  card?: Card
+  fields: CardFields
 }
 
 export type GetCardsHook<T extends CustomerCardTypes = CustomerCardTypes> = {
-  data: T['card'] | null
+  data: T['card'][] | null
   input: {}
   fetcherInput: { cartId?: string }
   swrState: { isEmpty: boolean }
@@ -48,26 +48,29 @@ export type UpdateItemHook<T extends CustomerCardTypes = CustomerCardTypes> = {
 
 export type RemoveItemHook<T extends CustomerCardTypes = CustomerCardTypes> = {
   data: T['card'] | null
-  input: { item?: T['fields'] }
+  input: { item?: T['card'] }
   fetcherInput: { itemId: string }
   body: { itemId: string }
   actionInput: { id: string }
 }
 
-export type CustomerCardHooks<T extends CustomerCardTypes = CustomerCardTypes> = {
-  getCards: GetCardsHook<T>
-  addItem: AddItemHook<T>
-  updateItem: UpdateItemHook<T>
-  removeItem: RemoveItemHook<T>
-}
+export type CustomerCardHooks<T extends CustomerCardTypes = CustomerCardTypes> =
+  {
+    getCards: GetCardsHook<T>
+    addItem: AddItemHook<T>
+    updateItem: UpdateItemHook<T>
+    removeItem: RemoveItemHook<T>
+  }
 
-export type CardsHandler<T extends CustomerCardTypes = CustomerCardTypes> = GetCardsHook<T> & {
-  body: { cartId?: string }
-}
+export type CardsHandler<T extends CustomerCardTypes = CustomerCardTypes> =
+  GetCardsHook<T> & {
+    body: { cartId?: string }
+  }
 
-export type AddItemHandler<T extends CustomerCardTypes = CustomerCardTypes> = AddItemHook<T> & {
-  body: { cartId: string }
-}
+export type AddItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
+  AddItemHook<T> & {
+    body: { cartId: string }
+  }
 
 export type UpdateItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
   UpdateItemHook<T> & {
@@ -80,15 +83,18 @@ export type RemoveItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
     body: { cartId: string }
   }
 
-
-export type CustomerCardHandlers<T extends CustomerCardTypes = CustomerCardTypes> = {
+export type CustomerCardHandlers<
+  T extends CustomerCardTypes = CustomerCardTypes
+> = {
   getCards: GetCardsHook<T>
   addItem: AddItemHandler<T>
   updateItem: UpdateItemHandler<T>
   removeItem: RemoveItemHandler<T>
 }
 
-export type CustomerCardSchema<T extends CustomerCardTypes = CustomerCardTypes> = {
+export type CustomerCardSchema<
+  T extends CustomerCardTypes = CustomerCardTypes
+> = {
   endpoint: {
     options: {}
     handlers: CustomerCardHandlers<T>
diff --git a/framework/ordercloud/.env.template b/framework/ordercloud/.env.template
new file mode 100644
index 000000000..9b33282ba
--- /dev/null
+++ b/framework/ordercloud/.env.template
@@ -0,0 +1,5 @@
+COMMERCE_PROVIDER=ordercloud
+
+ORDERCLOUD_CLIENT_ID=
+ORDERCLOUD_CLIENT_SECRET=
+STRIPE_SECRET=
diff --git a/framework/ordercloud/README.md b/framework/ordercloud/README.md
new file mode 100644
index 000000000..ca1438eae
--- /dev/null
+++ b/framework/ordercloud/README.md
@@ -0,0 +1,3 @@
+# Next.js Ordercloud Provider
+
+Create your own store from [here](https://nextjs.org/commerce)
diff --git a/framework/ordercloud/api/endpoints/cart/add-item.ts b/framework/ordercloud/api/endpoints/cart/add-item.ts
new file mode 100644
index 000000000..28d372bd0
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/cart/add-item.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/cart/get-cart.ts b/framework/ordercloud/api/endpoints/cart/get-cart.ts
new file mode 100644
index 000000000..7ea077b54
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/cart/get-cart.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/cart/index.ts b/framework/ordercloud/api/endpoints/cart/index.ts
new file mode 100644
index 000000000..756bce9fe
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/cart/index.ts
@@ -0,0 +1,28 @@
+import type { CartSchema } from '../../../types/cart'
+import type { OrdercloudAPI } from '../..'
+
+import { GetAPISchema, createEndpoint } from '@commerce/api'
+import cartEndpoint from '@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
diff --git a/framework/ordercloud/api/endpoints/cart/remove-item.ts b/framework/ordercloud/api/endpoints/cart/remove-item.ts
new file mode 100644
index 000000000..ea9c46e4c
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/cart/remove-item.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/cart/update-item.ts b/framework/ordercloud/api/endpoints/cart/update-item.ts
new file mode 100644
index 000000000..20113baee
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/cart/update-item.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/catalog/index.ts b/framework/ordercloud/api/endpoints/catalog/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/catalog/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/catalog/products.ts b/framework/ordercloud/api/endpoints/catalog/products.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/catalog/products.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/checkout/get-checkout.ts b/framework/ordercloud/api/endpoints/checkout/get-checkout.ts
new file mode 100644
index 000000000..c0ab1a40d
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/checkout/get-checkout.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/checkout/index.ts b/framework/ordercloud/api/endpoints/checkout/index.ts
new file mode 100644
index 000000000..e1b8a9f1c
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/checkout/index.ts
@@ -0,0 +1,23 @@
+import type { CheckoutSchema } from '../../../types/checkout'
+import type { OrdercloudAPI } from '../..'
+
+import { GetAPISchema, createEndpoint } from '@commerce/api'
+import checkoutEndpoint from '@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
diff --git a/framework/ordercloud/api/endpoints/checkout/submit-checkout.ts b/framework/ordercloud/api/endpoints/checkout/submit-checkout.ts
new file mode 100644
index 000000000..8cd9be5e4
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/checkout/submit-checkout.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/customer/address/add-item.ts b/framework/ordercloud/api/endpoints/customer/address/add-item.ts
new file mode 100644
index 000000000..434c2400d
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/address/add-item.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/customer/address/get-addresses.ts b/framework/ordercloud/api/endpoints/customer/address/get-addresses.ts
new file mode 100644
index 000000000..2e27591c0
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/address/get-addresses.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/customer/address/index.ts b/framework/ordercloud/api/endpoints/customer/address/index.ts
new file mode 100644
index 000000000..385bc57f1
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/address/index.ts
@@ -0,0 +1,27 @@
+import type { CustomerAddressSchema } from '../../../../types/customer/address'
+import type { OrdercloudAPI } from '../../..'
+
+import { GetAPISchema, createEndpoint } from '@commerce/api'
+import customerAddressEndpoint from '@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
diff --git a/framework/ordercloud/api/endpoints/customer/address/remove-item.ts b/framework/ordercloud/api/endpoints/customer/address/remove-item.ts
new file mode 100644
index 000000000..fba4e1154
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/address/remove-item.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/customer/address/update-item.ts b/framework/ordercloud/api/endpoints/customer/address/update-item.ts
new file mode 100644
index 000000000..4c4b4b9ae
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/address/update-item.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/customer/card/add-item.ts b/framework/ordercloud/api/endpoints/customer/card/add-item.ts
new file mode 100644
index 000000000..ad7dead7c
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/card/add-item.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/customer/card/get-cards.ts b/framework/ordercloud/api/endpoints/customer/card/get-cards.ts
new file mode 100644
index 000000000..e77520803
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/card/get-cards.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/customer/card/index.ts b/framework/ordercloud/api/endpoints/customer/card/index.ts
new file mode 100644
index 000000000..672939a8b
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/card/index.ts
@@ -0,0 +1,27 @@
+import type { CustomerCardSchema } from '../../../../types/customer/card'
+import type { OrdercloudAPI } from '../../..'
+
+import { GetAPISchema, createEndpoint } from '@commerce/api'
+import customerCardEndpoint from '@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
diff --git a/framework/ordercloud/api/endpoints/customer/card/remove-item.ts b/framework/ordercloud/api/endpoints/customer/card/remove-item.ts
new file mode 100644
index 000000000..1a81d1cf4
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/card/remove-item.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/customer/card/update-item.ts b/framework/ordercloud/api/endpoints/customer/card/update-item.ts
new file mode 100644
index 000000000..9770644aa
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/card/update-item.ts
@@ -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
diff --git a/framework/ordercloud/api/endpoints/customer/index.ts b/framework/ordercloud/api/endpoints/customer/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/customer/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/login/index.ts b/framework/ordercloud/api/endpoints/login/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/login/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/logout/index.ts b/framework/ordercloud/api/endpoints/logout/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/logout/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/signup/index.ts b/framework/ordercloud/api/endpoints/signup/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/signup/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/endpoints/wishlist/index.tsx b/framework/ordercloud/api/endpoints/wishlist/index.tsx
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/ordercloud/api/endpoints/wishlist/index.tsx
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/ordercloud/api/index.ts b/framework/ordercloud/api/index.ts
new file mode 100644
index 000000000..df62843ab
--- /dev/null
+++ b/framework/ordercloud/api/index.ts
@@ -0,0 +1,71 @@
+import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api'
+import { getCommerceApi as commerceApi } from '@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)
+}
diff --git a/framework/ordercloud/api/operations/get-all-pages.ts b/framework/ordercloud/api/operations/get-all-pages.ts
new file mode 100644
index 000000000..1727532e2
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-all-pages.ts
@@ -0,0 +1,22 @@
+import type { OrdercloudConfig } from '../'
+
+import { GetAllPagesOperation } from '@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
+}
diff --git a/framework/ordercloud/api/operations/get-all-product-paths.ts b/framework/ordercloud/api/operations/get-all-product-paths.ts
new file mode 100644
index 000000000..1ac23c033
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-all-product-paths.ts
@@ -0,0 +1,34 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type { GetAllProductPathsOperation } from '@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
+}
diff --git a/framework/ordercloud/api/operations/get-all-products.ts b/framework/ordercloud/api/operations/get-all-products.ts
new file mode 100644
index 000000000..6af24d945
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-all-products.ts
@@ -0,0 +1,35 @@
+import type { GetAllProductsOperation } from '@commerce/types/product'
+import type { OperationContext } from '@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
+}
diff --git a/framework/ordercloud/api/operations/get-page.ts b/framework/ordercloud/api/operations/get-page.ts
new file mode 100644
index 000000000..6b0a86a4d
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-page.ts
@@ -0,0 +1,15 @@
+import { GetPageOperation } from "@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
+}
diff --git a/framework/ordercloud/api/operations/get-product.ts b/framework/ordercloud/api/operations/get-product.ts
new file mode 100644
index 000000000..864f931d4
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-product.ts
@@ -0,0 +1,60 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type { GetProductOperation } from '@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
+}
diff --git a/framework/ordercloud/api/operations/get-site-info.ts b/framework/ordercloud/api/operations/get-site-info.ts
new file mode 100644
index 000000000..95188c58e
--- /dev/null
+++ b/framework/ordercloud/api/operations/get-site-info.ts
@@ -0,0 +1,46 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type { Category, GetSiteInfoOperation } from '@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
+}
diff --git a/framework/ordercloud/api/operations/index.ts b/framework/ordercloud/api/operations/index.ts
new file mode 100644
index 000000000..84b04a978
--- /dev/null
+++ b/framework/ordercloud/api/operations/index.ts
@@ -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'
diff --git a/framework/ordercloud/api/utils/cart.ts b/framework/ordercloud/api/utils/cart.ts
new file mode 100644
index 000000000..716f3521e
--- /dev/null
+++ b/framework/ordercloud/api/utils/cart.ts
@@ -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: [],
+  }
+}
diff --git a/framework/ordercloud/api/utils/fetch-graphql.ts b/framework/ordercloud/api/utils/fetch-graphql.ts
new file mode 100644
index 000000000..af72a337c
--- /dev/null
+++ b/framework/ordercloud/api/utils/fetch-graphql.ts
@@ -0,0 +1,14 @@
+import type { GraphQLFetcher } from '@commerce/api'
+import type { OrdercloudConfig } from '../'
+
+import { FetcherError } from '@commerce/utils/errors'
+
+const fetchGraphqlApi: (getConfig: () => OrdercloudConfig) => GraphQLFetcher =
+  () => async () => {
+    throw new FetcherError({
+      errors: [{ message: 'GraphQL fetch is not implemented' }],
+      status: 500,
+    })
+  }
+
+export default fetchGraphqlApi
diff --git a/framework/ordercloud/api/utils/fetch-rest.ts b/framework/ordercloud/api/utils/fetch-rest.ts
new file mode 100644
index 000000000..fd686b958
--- /dev/null
+++ b/framework/ordercloud/api/utils/fetch-rest.ts
@@ -0,0 +1,176 @@
+import vercelFetch from '@vercel/fetch'
+import { FetcherError } from '@commerce/utils/errors'
+
+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>
+  ) => {
+    // Get provider config
+    const config = getConfig()
+
+    // If a token was passed, set it on global
+    if (fetchOptions?.token) {
+      global.token = fetchOptions.token
+    }
+
+    // Get a token
+    if (!global.token) {
+      global.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: global.token as string,
+      fetchOptions,
+      config,
+      method,
+      path,
+      body,
+    })
+
+    return {
+      ...data,
+      meta: { token: global.token as string },
+    }
+  }
diff --git a/framework/ordercloud/auth/index.ts b/framework/ordercloud/auth/index.ts
new file mode 100644
index 000000000..36e757a89
--- /dev/null
+++ b/framework/ordercloud/auth/index.ts
@@ -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'
diff --git a/framework/ordercloud/auth/use-login.tsx b/framework/ordercloud/auth/use-login.tsx
new file mode 100644
index 000000000..28351dc7f
--- /dev/null
+++ b/framework/ordercloud/auth/use-login.tsx
@@ -0,0 +1,16 @@
+import { MutationHook } from '@commerce/utils/types'
+import useLogin, { UseLogin } from '@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 () {}
+  },
+}
diff --git a/framework/ordercloud/auth/use-logout.tsx b/framework/ordercloud/auth/use-logout.tsx
new file mode 100644
index 000000000..9b3fc3e44
--- /dev/null
+++ b/framework/ordercloud/auth/use-logout.tsx
@@ -0,0 +1,17 @@
+import { MutationHook } from '@commerce/utils/types'
+import useLogout, { UseLogout } from '@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 () => {},
+}
diff --git a/framework/ordercloud/auth/use-signup.tsx b/framework/ordercloud/auth/use-signup.tsx
new file mode 100644
index 000000000..e9ad13458
--- /dev/null
+++ b/framework/ordercloud/auth/use-signup.tsx
@@ -0,0 +1,19 @@
+import { useCallback } from 'react'
+import useCustomer from '../customer/use-customer'
+import { MutationHook } from '@commerce/utils/types'
+import useSignup, { UseSignup } from '@commerce/auth/use-signup'
+
+export default useSignup as UseSignup<typeof handler>
+
+export const handler: MutationHook<any> = {
+  fetchOptions: {
+    query: '',
+  },
+  async fetcher() {
+    return null
+  },
+  useHook:
+    ({ fetch }) =>
+    () =>
+    () => {},
+}
diff --git a/framework/ordercloud/cart/index.ts b/framework/ordercloud/cart/index.ts
new file mode 100644
index 000000000..3b8ba990e
--- /dev/null
+++ b/framework/ordercloud/cart/index.ts
@@ -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'
diff --git a/framework/ordercloud/cart/use-add-item.tsx b/framework/ordercloud/cart/use-add-item.tsx
new file mode 100644
index 000000000..4699202c3
--- /dev/null
+++ b/framework/ordercloud/cart/use-add-item.tsx
@@ -0,0 +1,48 @@
+import type { AddItemHook } from '@commerce/types/cart'
+import type { MutationHook } from '@commerce/utils/types'
+
+import { useCallback } from 'react'
+import { CommerceError } from '@commerce/utils/errors'
+import useAddItem, { UseAddItem } from '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/cart/use-cart.tsx b/framework/ordercloud/cart/use-cart.tsx
new file mode 100644
index 000000000..d194f4097
--- /dev/null
+++ b/framework/ordercloud/cart/use-cart.tsx
@@ -0,0 +1,33 @@
+import type { GetCartHook } from '@commerce/types/cart'
+
+import { useMemo } from 'react'
+import { SWRHook } from '@commerce/utils/types'
+import useCart, { UseCart } from '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/cart/use-remove-item.tsx b/framework/ordercloud/cart/use-remove-item.tsx
new file mode 100644
index 000000000..748ba963d
--- /dev/null
+++ b/framework/ordercloud/cart/use-remove-item.tsx
@@ -0,0 +1,60 @@
+import type {
+  MutationHookContext,
+  HookFetcherContext,
+} from '@commerce/utils/types'
+import type { Cart, LineItem, RemoveItemHook } from '@commerce/types/cart'
+
+import { useCallback } from 'react'
+
+import { ValidationError } from '@commerce/utils/errors'
+import useRemoveItem, { UseRemoveItem } from '@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])
+    },
+}
diff --git a/framework/ordercloud/cart/use-update-item.tsx b/framework/ordercloud/cart/use-update-item.tsx
new file mode 100644
index 000000000..cc9d93b03
--- /dev/null
+++ b/framework/ordercloud/cart/use-update-item.tsx
@@ -0,0 +1,93 @@
+import type {
+  HookFetcherContext,
+  MutationHookContext,
+} from '@commerce/utils/types'
+import type { UpdateItemHook, LineItem } from '@commerce/types/cart'
+
+import { useCallback } from 'react'
+import debounce from 'lodash.debounce'
+
+import { MutationHook } from '@commerce/utils/types'
+import { ValidationError } from '@commerce/utils/errors'
+import useUpdateItem, { UseUpdateItem } from '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/checkout/index.ts b/framework/ordercloud/checkout/index.ts
new file mode 100644
index 000000000..306621059
--- /dev/null
+++ b/framework/ordercloud/checkout/index.ts
@@ -0,0 +1,2 @@
+export { default as useSubmitCheckout } from './use-submit-checkout'
+export { default as useCheckout } from './use-checkout'
diff --git a/framework/ordercloud/checkout/use-checkout.tsx b/framework/ordercloud/checkout/use-checkout.tsx
new file mode 100644
index 000000000..6ce13dbb6
--- /dev/null
+++ b/framework/ordercloud/checkout/use-checkout.tsx
@@ -0,0 +1,41 @@
+import type { GetCheckoutHook } from '@commerce/types/checkout'
+
+import { useMemo } from 'react'
+import { SWRHook } from '@commerce/utils/types'
+import useCheckout, { UseCheckout } from '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/checkout/use-submit-checkout.tsx b/framework/ordercloud/checkout/use-submit-checkout.tsx
new file mode 100644
index 000000000..47644de8e
--- /dev/null
+++ b/framework/ordercloud/checkout/use-submit-checkout.tsx
@@ -0,0 +1,36 @@
+import type { SubmitCheckoutHook } from '@commerce/types/checkout'
+import type { MutationHook } from '@commerce/utils/types'
+
+import { useCallback } from 'react'
+import useSubmitCheckout, { UseSubmitCheckout } from '@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 '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/commerce.config.json b/framework/ordercloud/commerce.config.json
new file mode 100644
index 000000000..d93afa783
--- /dev/null
+++ b/framework/ordercloud/commerce.config.json
@@ -0,0 +1,10 @@
+{
+  "provider": "ordercloud",
+  "features": {
+    "wishlist": false,
+    "cart": true,
+    "search": false,
+    "customerAuth": false,
+    "customCheckout": true
+  }
+}
diff --git a/framework/ordercloud/constants.ts b/framework/ordercloud/constants.ts
new file mode 100644
index 000000000..d89b13f64
--- /dev/null
+++ b/framework/ordercloud/constants.ts
@@ -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'
diff --git a/framework/ordercloud/customer/address/index.ts b/framework/ordercloud/customer/address/index.ts
new file mode 100644
index 000000000..02c73e53b
--- /dev/null
+++ b/framework/ordercloud/customer/address/index.ts
@@ -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'
diff --git a/framework/ordercloud/customer/address/use-add-item.tsx b/framework/ordercloud/customer/address/use-add-item.tsx
new file mode 100644
index 000000000..cf3f22c46
--- /dev/null
+++ b/framework/ordercloud/customer/address/use-add-item.tsx
@@ -0,0 +1,38 @@
+import type { AddItemHook } from '@commerce/types/customer/address'
+import type { MutationHook } from '@commerce/utils/types'
+
+import { useCallback } from 'react'
+import useAddItem, { UseAddItem } from '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/customer/address/use-addresses.tsx b/framework/ordercloud/customer/address/use-addresses.tsx
new file mode 100644
index 000000000..e9ddc7001
--- /dev/null
+++ b/framework/ordercloud/customer/address/use-addresses.tsx
@@ -0,0 +1,35 @@
+import type { GetAddressesHook } from '@commerce/types/customer/address'
+
+import { useMemo } from 'react'
+import { SWRHook } from '@commerce/utils/types'
+import useAddresses, {
+  UseAddresses,
+} from '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/customer/address/use-remove-item.tsx b/framework/ordercloud/customer/address/use-remove-item.tsx
new file mode 100644
index 000000000..4e6282c99
--- /dev/null
+++ b/framework/ordercloud/customer/address/use-remove-item.tsx
@@ -0,0 +1,62 @@
+import type {
+  MutationHookContext,
+  HookFetcherContext,
+} from '@commerce/utils/types'
+import type { Address, RemoveItemHook } from '@commerce/types/customer/address'
+
+import { useCallback } from 'react'
+
+import { ValidationError } from '@commerce/utils/errors'
+import useRemoveItem, {
+  UseRemoveItem,
+} from '@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])
+    },
+}
diff --git a/framework/ordercloud/customer/address/use-update-item.tsx b/framework/ordercloud/customer/address/use-update-item.tsx
new file mode 100644
index 000000000..720a339d8
--- /dev/null
+++ b/framework/ordercloud/customer/address/use-update-item.tsx
@@ -0,0 +1,52 @@
+import type {
+  HookFetcherContext,
+  MutationHookContext,
+} from '@commerce/utils/types'
+import type { UpdateItemHook, Address } from '@commerce/types/customer/address'
+
+import { useCallback } from 'react'
+
+import { MutationHook } from '@commerce/utils/types'
+import useUpdateItem, {
+  UseUpdateItem,
+} from '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/customer/card/index.ts b/framework/ordercloud/customer/card/index.ts
new file mode 100644
index 000000000..357d30500
--- /dev/null
+++ b/framework/ordercloud/customer/card/index.ts
@@ -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'
diff --git a/framework/ordercloud/customer/card/use-add-item.tsx b/framework/ordercloud/customer/card/use-add-item.tsx
new file mode 100644
index 000000000..6c6a6d7fd
--- /dev/null
+++ b/framework/ordercloud/customer/card/use-add-item.tsx
@@ -0,0 +1,38 @@
+import type { AddItemHook } from '@commerce/types/customer/card'
+import type { MutationHook } from '@commerce/utils/types'
+
+import { useCallback } from 'react'
+import useAddItem, { UseAddItem } from '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/customer/card/use-cards.tsx b/framework/ordercloud/customer/card/use-cards.tsx
new file mode 100644
index 000000000..92236deb2
--- /dev/null
+++ b/framework/ordercloud/customer/card/use-cards.tsx
@@ -0,0 +1,33 @@
+import type { GetCardsHook } from '@commerce/types/customer/card'
+
+import { useMemo } from 'react'
+import { SWRHook } from '@commerce/utils/types'
+import useCard, { UseCards } from '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/customer/card/use-remove-item.tsx b/framework/ordercloud/customer/card/use-remove-item.tsx
new file mode 100644
index 000000000..dc0781b95
--- /dev/null
+++ b/framework/ordercloud/customer/card/use-remove-item.tsx
@@ -0,0 +1,62 @@
+import type {
+  MutationHookContext,
+  HookFetcherContext,
+} from '@commerce/utils/types'
+import type { Card, RemoveItemHook } from '@commerce/types/customer/card'
+
+import { useCallback } from 'react'
+
+import { ValidationError } from '@commerce/utils/errors'
+import useRemoveItem, {
+  UseRemoveItem,
+} from '@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])
+    },
+}
diff --git a/framework/ordercloud/customer/card/use-update-item.tsx b/framework/ordercloud/customer/card/use-update-item.tsx
new file mode 100644
index 000000000..0dfb218d5
--- /dev/null
+++ b/framework/ordercloud/customer/card/use-update-item.tsx
@@ -0,0 +1,52 @@
+import type {
+  HookFetcherContext,
+  MutationHookContext,
+} from '@commerce/utils/types'
+import type { UpdateItemHook, Card } from '@commerce/types/customer/card'
+
+import { useCallback } from 'react'
+
+import { MutationHook } from '@commerce/utils/types'
+import useUpdateItem, {
+  UseUpdateItem,
+} from '@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]
+      )
+    },
+}
diff --git a/framework/ordercloud/customer/index.ts b/framework/ordercloud/customer/index.ts
new file mode 100644
index 000000000..6c903ecc5
--- /dev/null
+++ b/framework/ordercloud/customer/index.ts
@@ -0,0 +1 @@
+export { default as useCustomer } from './use-customer'
diff --git a/framework/ordercloud/customer/use-customer.tsx b/framework/ordercloud/customer/use-customer.tsx
new file mode 100644
index 000000000..41757cd0d
--- /dev/null
+++ b/framework/ordercloud/customer/use-customer.tsx
@@ -0,0 +1,15 @@
+import { SWRHook } from '@commerce/utils/types'
+import useCustomer, { UseCustomer } from '@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 {}
+    }
+  },
+}
diff --git a/framework/ordercloud/fetcher.ts b/framework/ordercloud/fetcher.ts
new file mode 100644
index 000000000..6f314a71e
--- /dev/null
+++ b/framework/ordercloud/fetcher.ts
@@ -0,0 +1,17 @@
+import { Fetcher } from '@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
diff --git a/framework/ordercloud/index.tsx b/framework/ordercloud/index.tsx
new file mode 100644
index 000000000..6a01c2ee4
--- /dev/null
+++ b/framework/ordercloud/index.tsx
@@ -0,0 +1,9 @@
+import { ordercloudProvider, OrdercloudProvider } from './provider'
+import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
+
+export { ordercloudProvider }
+export type { OrdercloudProvider }
+
+export const CommerceProvider = getCommerceProvider(ordercloudProvider)
+
+export const useCommerce = () => useCoreCommerce()
diff --git a/framework/ordercloud/next.config.js b/framework/ordercloud/next.config.js
new file mode 100644
index 000000000..793a4589f
--- /dev/null
+++ b/framework/ordercloud/next.config.js
@@ -0,0 +1,8 @@
+const commerce = require('./commerce.config.json')
+
+module.exports = {
+  commerce,
+  images: {
+    domains: ['localhost', 'ocdevops.blob.core.windows.net'],
+  },
+}
diff --git a/framework/ordercloud/product/index.ts b/framework/ordercloud/product/index.ts
new file mode 100644
index 000000000..426a3edcd
--- /dev/null
+++ b/framework/ordercloud/product/index.ts
@@ -0,0 +1,2 @@
+export { default as usePrice } from './use-price'
+export { default as useSearch } from './use-search'
diff --git a/framework/ordercloud/product/use-price.tsx b/framework/ordercloud/product/use-price.tsx
new file mode 100644
index 000000000..0174faf5e
--- /dev/null
+++ b/framework/ordercloud/product/use-price.tsx
@@ -0,0 +1,2 @@
+export * from '@commerce/product/use-price'
+export { default } from '@commerce/product/use-price'
diff --git a/framework/ordercloud/product/use-search.tsx b/framework/ordercloud/product/use-search.tsx
new file mode 100644
index 000000000..30e699537
--- /dev/null
+++ b/framework/ordercloud/product/use-search.tsx
@@ -0,0 +1,17 @@
+import { SWRHook } from '@commerce/utils/types'
+import useSearch, { UseSearch } from '@commerce/product/use-search'
+export default useSearch as UseSearch<typeof handler>
+
+export const handler: SWRHook<any> = {
+  fetchOptions: {
+    query: '',
+  },
+  async fetcher({ input, options, fetch }) {},
+  useHook: () => () => {
+    return {
+      data: {
+        products: [],
+      },
+    }
+  },
+}
diff --git a/framework/ordercloud/provider.ts b/framework/ordercloud/provider.ts
new file mode 100644
index 000000000..337eed657
--- /dev/null
+++ b/framework/ordercloud/provider.ts
@@ -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
diff --git a/framework/ordercloud/types/cart.ts b/framework/ordercloud/types/cart.ts
new file mode 100644
index 000000000..4716c355d
--- /dev/null
+++ b/framework/ordercloud/types/cart.ts
@@ -0,0 +1,126 @@
+import * as Core from '@commerce/types/cart'
+
+export * from '@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']
diff --git a/framework/ordercloud/types/category.ts b/framework/ordercloud/types/category.ts
new file mode 100644
index 000000000..247844a56
--- /dev/null
+++ b/framework/ordercloud/types/category.ts
@@ -0,0 +1,10 @@
+export interface RawCategory {
+  ID: string
+  Name: string
+  Description: null | string
+  ListOrder: number
+  Active: boolean
+  ParentID: null
+  ChildCount: number
+  xp: null
+}
diff --git a/framework/ordercloud/types/checkout.ts b/framework/ordercloud/types/checkout.ts
new file mode 100644
index 000000000..17cbf43de
--- /dev/null
+++ b/framework/ordercloud/types/checkout.ts
@@ -0,0 +1,4 @@
+import * as Core from '@commerce/types/checkout'
+
+export type CheckoutTypes = Core.CheckoutTypes
+export type CheckoutSchema = Core.CheckoutSchema<CheckoutTypes>
diff --git a/framework/ordercloud/types/customer/address.ts b/framework/ordercloud/types/customer/address.ts
new file mode 100644
index 000000000..3aaddc9a2
--- /dev/null
+++ b/framework/ordercloud/types/customer/address.ts
@@ -0,0 +1,31 @@
+import * as Core from '@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
+}
diff --git a/framework/ordercloud/types/customer/card.ts b/framework/ordercloud/types/customer/card.ts
new file mode 100644
index 000000000..eb1abffbb
--- /dev/null
+++ b/framework/ordercloud/types/customer/card.ts
@@ -0,0 +1,16 @@
+import * as Core from '@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
+}
diff --git a/framework/ordercloud/types/node.d.ts b/framework/ordercloud/types/node.d.ts
new file mode 100644
index 000000000..f4e4a21f4
--- /dev/null
+++ b/framework/ordercloud/types/node.d.ts
@@ -0,0 +1,5 @@
+declare module NodeJS {
+  interface Global {
+    token: string | null | undefined
+  }
+}
diff --git a/framework/ordercloud/types/product.ts b/framework/ordercloud/types/product.ts
new file mode 100644
index 000000000..8ccb778d2
--- /dev/null
+++ b/framework/ordercloud/types/product.ts
@@ -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[]
+  }
+}
diff --git a/framework/ordercloud/utils/product.ts b/framework/ordercloud/utils/product.ts
new file mode 100644
index 000000000..ee334f175
--- /dev/null
+++ b/framework/ordercloud/utils/product.ts
@@ -0,0 +1,47 @@
+import type { Product } from '@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] }),
+          })),
+        }))
+      : [],
+  }
+}
diff --git a/framework/ordercloud/wishlist/use-add-item.tsx b/framework/ordercloud/wishlist/use-add-item.tsx
new file mode 100644
index 000000000..75f067c3a
--- /dev/null
+++ b/framework/ordercloud/wishlist/use-add-item.tsx
@@ -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
diff --git a/framework/ordercloud/wishlist/use-remove-item.tsx b/framework/ordercloud/wishlist/use-remove-item.tsx
new file mode 100644
index 000000000..a2d3a8a05
--- /dev/null
+++ b/framework/ordercloud/wishlist/use-remove-item.tsx
@@ -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
diff --git a/framework/ordercloud/wishlist/use-wishlist.tsx b/framework/ordercloud/wishlist/use-wishlist.tsx
new file mode 100644
index 000000000..9fe0e758f
--- /dev/null
+++ b/framework/ordercloud/wishlist/use-wishlist.tsx
@@ -0,0 +1,43 @@
+import { HookFetcher } from '@commerce/utils/types'
+import type { Product } from '@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)
diff --git a/package.json b/package.json
index 68bf0059d..f42b2619b 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
     "react-fast-marquee": "^1.1.4",
     "react-merge-refs": "^1.1.0",
     "react-use-measure": "^2.0.4",
+    "stripe": "^8.176.0",
     "swell-js": "^4.0.0-next.0",
     "swr": "^0.5.6",
     "tabbable": "^5.2.0",
diff --git a/yarn.lock b/yarn.lock
index 35e9ca835..28e045710 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1170,6 +1170,11 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
   integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
 
+"@types/node@>=8.1.0":
+  version "16.9.6"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.6.tgz#040a64d7faf9e5d9e940357125f0963012e66f04"
+  integrity sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==
+
 "@types/parse-json@^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -5686,6 +5691,13 @@ qs@6.7.0:
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
   integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
 
+qs@^6.6.0:
+  version "6.10.1"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
+  integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
+  dependencies:
+    side-channel "^1.0.4"
+
 querystring-es3@0.2.1, querystring-es3@^0.2.0:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -6487,6 +6499,14 @@ strip-json-comments@~2.0.1:
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
   integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
 
+stripe@^8.176.0:
+  version "8.176.0"
+  resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.176.0.tgz#2f4980ab49acbfe6d67ecaddd54c05e20de9532c"
+  integrity sha512-0KCDo8TWFgeNWU7cPaqdjO2u2OSth0cmWYZmA7xsuxRCk7/lgWbJ/UbeSphx74cCIjFCmGuzDoNuNxqon9lEbg==
+  dependencies:
+    "@types/node" ">=8.1.0"
+    qs "^6.6.0"
+
 styled-jsx@3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018"