diff --git a/packages/appibase/.env.template b/packages/appibase/.env.template
new file mode 100644
index 000000000..0ec47188b
--- /dev/null
+++ b/packages/appibase/.env.template
@@ -0,0 +1 @@
+COMMERCE_PROVIDER=appibase
\ No newline at end of file
diff --git a/packages/appibase/.prettierignore b/packages/appibase/.prettierignore
new file mode 100644
index 000000000..f06235c46
--- /dev/null
+++ b/packages/appibase/.prettierignore
@@ -0,0 +1,2 @@
+node_modules
+dist
diff --git a/packages/appibase/.prettierrc b/packages/appibase/.prettierrc
new file mode 100644
index 000000000..e1076edfa
--- /dev/null
+++ b/packages/appibase/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "useTabs": false
+}
diff --git a/packages/appibase/README.md b/packages/appibase/README.md
new file mode 100644
index 000000000..a3bc1db32
--- /dev/null
+++ b/packages/appibase/README.md
@@ -0,0 +1 @@
+# Next.js Local Provider
diff --git a/packages/appibase/package-lock.json b/packages/appibase/package-lock.json
new file mode 100644
index 000000000..9bbc15602
--- /dev/null
+++ b/packages/appibase/package-lock.json
@@ -0,0 +1,18 @@
+{
+ "name": "appibase",
+ "version": "0.0.1",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "js-cookies": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/js-cookies/-/js-cookies-1.0.4.tgz",
+ "integrity": "sha1-1G5XbEIP9tVULA9SttTvfWN+dU4="
+ },
+ "urijs": {
+ "version": "1.19.10",
+ "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.10.tgz",
+ "integrity": "sha512-EzauQlgKuJgsXOqoMrCiePBf4At5jVqRhXykF3Wfb8ZsOBMxPcfiVBcsHXug4Aepb/ICm2PIgqAUGMelgdrWEg=="
+ }
+ }
+}
diff --git a/packages/appibase/package.json b/packages/appibase/package.json
new file mode 100644
index 000000000..b070d793c
--- /dev/null
+++ b/packages/appibase/package.json
@@ -0,0 +1,84 @@
+{
+ "name": "appibase",
+ "version": "0.0.1",
+ "license": "MIT",
+ "scripts": {
+ "release": "taskr release",
+ "build": "taskr build",
+ "dev": "taskr",
+ "types": "tsc --emitDeclarationOnly",
+ "prettier-fix": "prettier --write ."
+ },
+ "sideEffects": false,
+ "type": "module",
+ "exports": {
+ ".": "./dist/index.js",
+ "./*": [
+ "./dist/*.js",
+ "./dist/*/index.js"
+ ],
+ "./next.config": "./dist/next.config.cjs"
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "src/*",
+ "src/*/index"
+ ],
+ "next.config": [
+ "dist/next.config.d.cts"
+ ]
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "publishConfig": {
+ "typesVersions": {
+ "*": {
+ "*": [
+ "dist/*.d.ts",
+ "dist/*/index.d.ts"
+ ],
+ "next.config": [
+ "dist/next.config.d.cts"
+ ]
+ }
+ }
+ },
+ "dependencies": {
+ "@vercel/commerce": "^0.0.1",
+ "@vercel/fetch": "^6.1.1",
+ "deserialize-json-api": "^1.4.0",
+ "js-cookies": "^1.0.4",
+ "json-api-response-converter": "^1.6.0",
+ "kitsu": "^10.0.0-alpha.22",
+ "urijs": "^1.19.10"
+ },
+ "peerDependencies": {
+ "next": "^12",
+ "react": "^17",
+ "react-dom": "^17"
+ },
+ "devDependencies": {
+ "@taskr/clear": "^1.1.0",
+ "@taskr/esnext": "^1.1.0",
+ "@taskr/watch": "^1.1.0",
+ "@types/node": "^17.0.8",
+ "@types/react": "^17.0.38",
+ "lint-staged": "^12.1.7",
+ "next": "^12.0.8",
+ "prettier": "^2.5.1",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2",
+ "taskr": "^1.1.0",
+ "taskr-swc": "^0.0.1",
+ "typescript": "^4.5.4"
+ },
+ "lint-staged": {
+ "**/*.{js,jsx,ts,tsx,json}": [
+ "prettier --write",
+ "git add"
+ ]
+ }
+}
diff --git a/packages/appibase/src/api/endpoints/cart/index.ts b/packages/appibase/src/api/endpoints/cart/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/cart/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/endpoints/catalog/index.ts b/packages/appibase/src/api/endpoints/catalog/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/catalog/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/endpoints/catalog/products.ts b/packages/appibase/src/api/endpoints/catalog/products.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/catalog/products.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/endpoints/checkout/index.ts b/packages/appibase/src/api/endpoints/checkout/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/checkout/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/endpoints/customer/address.ts b/packages/appibase/src/api/endpoints/customer/address.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/customer/address.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/endpoints/customer/card.ts b/packages/appibase/src/api/endpoints/customer/card.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/customer/card.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/endpoints/customer/index.ts b/packages/appibase/src/api/endpoints/customer/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/customer/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/endpoints/login/index.ts b/packages/appibase/src/api/endpoints/login/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/login/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/endpoints/logout/index.ts b/packages/appibase/src/api/endpoints/logout/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/logout/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/endpoints/signup/index.ts b/packages/appibase/src/api/endpoints/signup/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/signup/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/endpoints/wishlist/index.tsx b/packages/appibase/src/api/endpoints/wishlist/index.tsx
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/packages/appibase/src/api/endpoints/wishlist/index.tsx
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/packages/appibase/src/api/index.ts b/packages/appibase/src/api/index.ts
new file mode 100644
index 000000000..84862128e
--- /dev/null
+++ b/packages/appibase/src/api/index.ts
@@ -0,0 +1,43 @@
+import type { CommerceAPI, CommerceAPIConfig } from '@vercel/commerce/api'
+import { getCommerceApi as commerceApi } from '@vercel/commerce/api'
+import createFetcher from './utils/fetch-appibase'
+import { API_URL } from '../const'
+
+import getAllPages from './operations/get-all-pages'
+import getPage from './operations/get-page'
+import getSiteInfo from './operations/get-site-info'
+import getCustomerWishlist from './operations/get-customer-wishlist'
+import getAllProductPaths from './operations/get-all-product-paths'
+import getAllProducts from './operations/get-all-products'
+import getProduct from './operations/get-product'
+
+export interface LocalConfig extends CommerceAPIConfig {}
+const config: LocalConfig = {
+ commerceUrl: API_URL,
+ apiToken: '',
+ cartCookie: '',
+ customerCookie: '',
+ cartCookieMaxAge: 2592000,
+ fetch: createFetcher(() => getCommerceApi().getConfig()),
+}
+
+const operations = {
+ getAllPages,
+ getPage,
+ getSiteInfo,
+ getCustomerWishlist,
+ getAllProductPaths,
+ getAllProducts,
+ getProduct,
+}
+
+export const provider = { config, operations }
+
+export type Provider = typeof provider
+export type LocalAPI
= CommerceAPI
+
+export function getCommerceApi
(
+ customProvider: P = provider as any
+): LocalAPI
{
+ return commerceApi(customProvider as any)
+}
diff --git a/packages/appibase/src/api/operations/get-all-pages.ts b/packages/appibase/src/api/operations/get-all-pages.ts
new file mode 100644
index 000000000..098746040
--- /dev/null
+++ b/packages/appibase/src/api/operations/get-all-pages.ts
@@ -0,0 +1,21 @@
+export type Page = { url: string }
+export type GetAllPagesResult = { pages: Page[] }
+import type { LocalConfig } from '../index'
+
+export default function getAllPagesOperation() {
+ function getAllPages({
+ config,
+ preview,
+ }: {
+ url?: string
+ config?: Partial
+ preview?: boolean
+ }): Promise {
+ console.log('GETTING ALL PAGES');
+
+ return Promise.resolve({
+ pages: [],
+ })
+ }
+ return getAllPages
+}
diff --git a/packages/appibase/src/api/operations/get-all-product-paths.ts b/packages/appibase/src/api/operations/get-all-product-paths.ts
new file mode 100644
index 000000000..705a77cda
--- /dev/null
+++ b/packages/appibase/src/api/operations/get-all-product-paths.ts
@@ -0,0 +1,17 @@
+import data from '../../data.json'
+
+export type GetAllProductPathsResult = {
+ products: Array<{ path: string }>
+}
+
+export default function getAllProductPathsOperation() {
+ function getAllProductPaths(): Promise {
+ console.log('GETTING ALL PATHS');
+
+ return Promise.resolve({
+ products: [] //data.products.map(({ path }) => ({ path })),
+ })
+ }
+
+ return getAllProductPaths
+}
diff --git a/packages/appibase/src/api/operations/get-all-products.ts b/packages/appibase/src/api/operations/get-all-products.ts
new file mode 100644
index 000000000..ce15df8eb
--- /dev/null
+++ b/packages/appibase/src/api/operations/get-all-products.ts
@@ -0,0 +1,33 @@
+import { Product } from '@vercel/commerce/types/product'
+import { GetAllProductsOperation } from '@vercel/commerce/types/product'
+import type { OperationContext } from '@vercel/commerce/api/operations'
+import type { LocalConfig, Provider } from '../index'
+import { NormalizeProduct } from '../utils/normalize'
+import type { AppibaseProduct } from '../../types'
+
+export default function getAllProductsOperation({
+ commerce,
+}: OperationContext) {
+ async function getAllProducts({
+ query = '',
+ variables,
+ config,
+ }: {
+ query?: string
+ variables?: T['variables']
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise<{ products: Product[] | any[] }> {
+
+ const { fetch } = commerce.getConfig(config)
+
+ const { data: fetchedProducts } = await fetch('/products?filter[is_parent_true]=true&include=prices,options');
+
+ const products = fetchedProducts.map((p : AppibaseProduct) => NormalizeProduct(p))
+
+ return {
+ products
+ }
+ }
+ return getAllProducts
+}
diff --git a/packages/appibase/src/api/operations/get-customer-wishlist.ts b/packages/appibase/src/api/operations/get-customer-wishlist.ts
new file mode 100644
index 000000000..8c34b9e87
--- /dev/null
+++ b/packages/appibase/src/api/operations/get-customer-wishlist.ts
@@ -0,0 +1,6 @@
+export default function getCustomerWishlistOperation() {
+ function getCustomerWishlist(): any {
+ return { wishlist: {} }
+ }
+ return getCustomerWishlist
+}
diff --git a/packages/appibase/src/api/operations/get-page.ts b/packages/appibase/src/api/operations/get-page.ts
new file mode 100644
index 000000000..bb4f6ecdc
--- /dev/null
+++ b/packages/appibase/src/api/operations/get-page.ts
@@ -0,0 +1,15 @@
+export type Page = any
+export type GetPageResult = { page?: Page }
+
+export type PageVariables = {
+ id: number
+}
+
+export default function getPageOperation() {
+ function getPage(): Promise {
+ console.log('GETTING PAGE');
+
+ return Promise.resolve({})
+ }
+ return getPage
+}
diff --git a/packages/appibase/src/api/operations/get-product.ts b/packages/appibase/src/api/operations/get-product.ts
new file mode 100644
index 000000000..bb8a27637
--- /dev/null
+++ b/packages/appibase/src/api/operations/get-product.ts
@@ -0,0 +1,29 @@
+import type { LocalConfig } from '../index'
+import { Product } from '@vercel/commerce/types/product'
+import { GetProductOperation } from '@vercel/commerce/types/product'
+import type { OperationContext } from '@vercel/commerce/api/operations'
+import { NormalizeProduct } from '../utils/normalize'
+
+export default function getProductOperation({
+ commerce,
+}: OperationContext) {
+ async function getProduct({
+ query = '',
+ variables,
+ config,
+ }: {
+ query?: string
+ variables?: T['variables']
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ const { fetch } = commerce.getConfig(config)
+ const { data: fetchedProduct } = await fetch(`/products/${variables?.slug}?include=products,variations,options,prices,stock_items,variations.variation_options`);
+
+ return {
+ product: NormalizeProduct(fetchedProduct),
+ }
+ }
+
+ return getProduct
+}
diff --git a/packages/appibase/src/api/operations/get-site-info.ts b/packages/appibase/src/api/operations/get-site-info.ts
new file mode 100644
index 000000000..02066a990
--- /dev/null
+++ b/packages/appibase/src/api/operations/get-site-info.ts
@@ -0,0 +1,38 @@
+import { OperationContext } from '@vercel/commerce/api/operations'
+import { Category } from '@vercel/commerce/types/site'
+import { NormalizeCategory } from '../utils/normalize'
+import { LocalConfig } from '../index'
+import { AppibaseCollection } from '../../types'
+
+export type GetSiteInfoResult<
+ T extends { categories: any[]; brands: any[] } = {
+ categories: Category[]
+ brands: any[]
+ }
+> = T
+
+export default function getSiteInfoOperation({ commerce }: OperationContext) {
+ async function getSiteInfo({
+ query,
+ variables,
+ config,
+ }: {
+ query?: string
+ variables?: any
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+
+ const { fetch } = commerce.getConfig(config)
+
+ const { res : { data } } = await fetch('/collections');
+ const categories = data.map((p : AppibaseCollection) => NormalizeCategory(p))
+
+ return Promise.resolve({
+ categories,
+ brands: [],
+ })
+ }
+
+ return getSiteInfo
+}
diff --git a/packages/appibase/src/api/operations/index.ts b/packages/appibase/src/api/operations/index.ts
new file mode 100644
index 000000000..086fdf83a
--- /dev/null
+++ b/packages/appibase/src/api/operations/index.ts
@@ -0,0 +1,6 @@
+export { default as getPage } from './get-page'
+export { default as getSiteInfo } from './get-site-info'
+export { default as getAllPages } from './get-all-pages'
+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/packages/appibase/src/api/utils/access-token.ts b/packages/appibase/src/api/utils/access-token.ts
new file mode 100644
index 000000000..e07194b28
--- /dev/null
+++ b/packages/appibase/src/api/utils/access-token.ts
@@ -0,0 +1,35 @@
+import Cookies from 'js-cookie'
+import Kitsu from "kitsu";
+import { API_URL, SCOPE, CLIENT_ID_STOREFRONT } from '../../const'
+
+const GetAccessToken = async () => {
+
+ const fromCookies = Cookies.get('access_token');
+
+ if(!fromCookies) {
+ const myHeaders = new Headers();
+ myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
+ myHeaders.append("Authorization", `Basic ${Buffer.from(CLIENT_ID_STOREFRONT).toString('base64')}`);
+ myHeaders.append("Accept", "application/json");
+
+ const urlencoded = new URLSearchParams();
+ urlencoded.append("grant_type", "client_credentials");
+ urlencoded.append("scope", SCOPE);
+
+ const requestOptions = {
+ method: 'POST',
+ headers: myHeaders,
+ body: urlencoded,
+ };
+
+ const response = await fetch(API_URL + "/oauth/token", requestOptions)
+ const { access_token, expires_in } = await response.json()
+
+ Cookies.set('access_token', access_token, { expires: expires_in / 60 / 60 / 24 })
+ return access_token
+ }
+ else return fromCookies
+}
+
+
+export { GetAccessToken }
\ No newline at end of file
diff --git a/packages/appibase/src/api/utils/fetch-appibase.ts b/packages/appibase/src/api/utils/fetch-appibase.ts
new file mode 100644
index 000000000..9af60fe1a
--- /dev/null
+++ b/packages/appibase/src/api/utils/fetch-appibase.ts
@@ -0,0 +1,28 @@
+import { FetcherError } from '@vercel/commerce/utils/errors'
+import type { GraphQLFetcher } from '@vercel/commerce/api'
+import Kitsu from "kitsu";
+import type { LocalConfig } from '../index'
+import { API_URL } from '../../const'
+import { GetAccessToken } from './access-token'
+
+const fetchApi: (getConfig: () => LocalConfig) => GraphQLFetcher =
+ (getConfig) =>
+ async (query: string, { variables, preview } = {}, fetchOptions) => {
+ const config = getConfig()
+
+ const api = new Kitsu({ baseURL: API_URL + '/api/v1' })
+ api.headers.Authorization = `Bearer ${await GetAccessToken()}`
+
+ const res = await api.get(query)
+
+ if (res.errors) {
+ throw new FetcherError({
+ errors: res.errors ?? [{ message: 'Failed to fetch for API' }],
+ status: res.status,
+ })
+ }
+
+ return { data: res.data, res }
+ }
+
+export default fetchApi
diff --git a/packages/appibase/src/api/utils/fetch.ts b/packages/appibase/src/api/utils/fetch.ts
new file mode 100644
index 000000000..9d9fff3ed
--- /dev/null
+++ b/packages/appibase/src/api/utils/fetch.ts
@@ -0,0 +1,3 @@
+import zeitFetch from '@vercel/fetch'
+
+export default zeitFetch()
diff --git a/packages/appibase/src/api/utils/normalize.ts b/packages/appibase/src/api/utils/normalize.ts
new file mode 100644
index 000000000..a5bc725d0
--- /dev/null
+++ b/packages/appibase/src/api/utils/normalize.ts
@@ -0,0 +1,82 @@
+import { Product, ProductOption, ProductImage } from '@vercel/commerce/types/product'
+import { Category } from '@vercel/commerce/types/site'
+import { Cart, LineItem } from '@vercel/commerce/types/cart'
+import type { AppibaseProduct, AppibaseCollection, AppibaseCart } from '../../types'
+
+
+const NormalizeProduct = (product: AppibaseProduct): Product => {
+ const options: ProductOption[] = [];
+
+ for(const variation of (product.variations?.data || [])) {
+ const option : ProductOption | undefined = options.find(o => o.displayName === variation.name);
+ if(!option) {
+ options.push({
+ id: `option-${variation.name.toLowerCase()}`,
+ displayName : variation.name, values: variation.options?.map(o => ({ label: o.name })) || []
+ });
+ }
+ }
+
+ return {
+ id: product.id,
+ name: product.name,
+ description: product.description,
+ images: product.image_urls.map(i => { url: i }),
+ sku: product.sku,
+ slug: product.sku,
+ variants: product.children?.data?.map(p => ({
+ id: p.id,
+ options: p.variation_options?.data.map(o => ({
+ __typename: "MultipleChoiceOption",
+ id: o.id,
+ displayName: o.variation_name || "",
+ values: [{ label: o.name || "" }]
+ })) || []
+ })) || [],
+ price: { value: product.prices.data[0].amount.float, currencyCode: product.prices.data[0].currency },
+ options
+ }
+}
+
+const NormalizeCategory = (collection: AppibaseCollection): Category => {
+ return {
+ id: String(collection.id),
+ name: collection.name,
+ slug: collection.slug,
+ path: '/' + collection.slug,
+ }
+}
+
+const NormalizeCart = (cart: AppibaseCart): Cart => {
+ return {
+ id: String(cart.id),
+ createdAt: (new Date()).toDateString(),
+ currency: {
+ code: cart.currency
+ },
+ taxesIncluded: cart.tax_incl,
+ lineItemsSubtotalPrice: cart.subtotal_amount.float,
+ subtotalPrice: cart.subtotal_amount.float,
+ totalPrice: cart.total_amount.float,
+ lineItems: cart.cart_items?.data.map(i => ({
+ id: i.id,
+ variantId: i.id,
+ productId: i.id,
+ name: i.name,
+ discounts: [],
+ path: i.sku,
+ variant: {
+ sku: i.sku,
+ id: i.id,
+ name: i.name,
+ requiresShipping: true,
+ price: i.price.float,
+ listPrice: i.price.float,
+ image: { url: i.image_url }
+ },
+ quantity: parseInt(i.quantity)
+ })) || []
+ }
+}
+
+export { NormalizeProduct, NormalizeCategory, NormalizeCart }
\ No newline at end of file
diff --git a/packages/appibase/src/auth/index.ts b/packages/appibase/src/auth/index.ts
new file mode 100644
index 000000000..36e757a89
--- /dev/null
+++ b/packages/appibase/src/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/packages/appibase/src/auth/use-login.tsx b/packages/appibase/src/auth/use-login.tsx
new file mode 100644
index 000000000..20e3ed229
--- /dev/null
+++ b/packages/appibase/src/auth/use-login.tsx
@@ -0,0 +1,16 @@
+import { MutationHook } from '@vercel/commerce/utils/types'
+import useLogin, { UseLogin } from '@vercel/commerce/auth/use-login'
+
+export default useLogin as UseLogin
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher() {
+ return null
+ },
+ useHook: () => () => {
+ return async function () {}
+ },
+}
diff --git a/packages/appibase/src/auth/use-logout.tsx b/packages/appibase/src/auth/use-logout.tsx
new file mode 100644
index 000000000..4e74908f3
--- /dev/null
+++ b/packages/appibase/src/auth/use-logout.tsx
@@ -0,0 +1,17 @@
+import { MutationHook } from '@vercel/commerce/utils/types'
+import useLogout, { UseLogout } from '@vercel/commerce/auth/use-logout'
+
+export default useLogout as UseLogout
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher() {
+ return null
+ },
+ useHook:
+ ({ fetch }) =>
+ () =>
+ async () => {},
+}
diff --git a/packages/appibase/src/auth/use-signup.tsx b/packages/appibase/src/auth/use-signup.tsx
new file mode 100644
index 000000000..e48811403
--- /dev/null
+++ b/packages/appibase/src/auth/use-signup.tsx
@@ -0,0 +1,19 @@
+import { useCallback } from 'react'
+import useCustomer from '../customer/use-customer'
+import { MutationHook } from '@vercel/commerce/utils/types'
+import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup'
+
+export default useSignup as UseSignup
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher() {
+ return null
+ },
+ useHook:
+ ({ fetch }) =>
+ () =>
+ () => {},
+}
diff --git a/packages/appibase/src/cart/index.ts b/packages/appibase/src/cart/index.ts
new file mode 100644
index 000000000..3b8ba990e
--- /dev/null
+++ b/packages/appibase/src/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/packages/appibase/src/cart/use-add-item.tsx b/packages/appibase/src/cart/use-add-item.tsx
new file mode 100644
index 000000000..0eb469050
--- /dev/null
+++ b/packages/appibase/src/cart/use-add-item.tsx
@@ -0,0 +1,39 @@
+import { useCallback } from 'react';
+import useAddItem, { UseAddItem } from '@vercel/commerce/cart/use-add-item'
+import { MutationHook } from '@vercel/commerce/utils/types'
+import Cookies from 'js-cookie'
+import { NormalizeCart } from '../api/utils/normalize'
+
+export default useAddItem as UseAddItem
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input: item, options, fetch }) {
+
+ const fromCookies = Cookies.get('cart_id');
+
+ await fetch({
+ query : `/carts/${fromCookies}/cart_items`,
+ method: 'POST',
+ body: {
+ quantity: 1,
+ item: {
+ data : {
+ type: 'product',
+ id: item.variantId
+ }
+ }
+ }
+ })
+ return;
+ },
+ useHook:
+ ({ fetch }) =>
+ () => {
+ return useCallback(async function addItem(input) {
+ const data = await fetch({ input });
+ return data;
+ }, [fetch]);
+ },
+}
diff --git a/packages/appibase/src/cart/use-cart.tsx b/packages/appibase/src/cart/use-cart.tsx
new file mode 100644
index 000000000..965a8eed3
--- /dev/null
+++ b/packages/appibase/src/cart/use-cart.tsx
@@ -0,0 +1,51 @@
+import { useMemo } from 'react'
+import { SWRHook } from '@vercel/commerce/utils/types'
+import useCart, { UseCart } from '@vercel/commerce/cart/use-cart'
+import Cookies from 'js-cookie'
+import { NormalizeCart } from '../api/utils/normalize'
+import { STORE_ID } from '../const'
+
+export default useCart as UseCart
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: ''
+ },
+ async fetcher({ fetch }) {
+ const fromCookies = Cookies.get('cart_id');
+
+ if(!fromCookies) {
+
+ const { data } = await fetch({ query : `/stores/${STORE_ID}/carts`, method: "POST", body: { id: '' } })
+
+ Cookies.set('cart_id', data.id)
+
+ return NormalizeCart(data)
+ }
+
+ else {
+ const { data } = await fetch({ query : `/carts/${fromCookies}?include=cart_items` })
+ return NormalizeCart(data)
+ }
+ },
+ useHook:
+ ({ useData }) =>
+ (input) => {
+ const response = useData({
+ swrOptions: { revalidateOnFocus: true, ...input?.swrOptions },
+ })
+
+ return useMemo(
+ () =>
+ Object.create(response, {
+ isEmpty: {
+ get() {
+ return (response.data?.lineItems.length ?? 0) <= 0
+ },
+ enumerable: true,
+ },
+ }),
+ [response]
+ )
+ },
+}
diff --git a/packages/appibase/src/cart/use-remove-item.tsx b/packages/appibase/src/cart/use-remove-item.tsx
new file mode 100644
index 000000000..0da9169b2
--- /dev/null
+++ b/packages/appibase/src/cart/use-remove-item.tsx
@@ -0,0 +1,40 @@
+import { MutationHook } from '@vercel/commerce/utils/types'
+import { useCallback } from 'react'
+import useRemoveItem, {
+ UseRemoveItem,
+} from '@vercel/commerce/cart/use-remove-item'
+import Cookies from 'js-cookie'
+import { NormalizeCart } from '../api/utils/normalize'
+import useCart from './use-cart'
+
+export default useRemoveItem as UseRemoveItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input: item, options, fetch }) {
+ await fetch({
+ query : `cart_items`,
+ method: 'DELETE',
+ body: item.id
+ })
+
+ const fromCookies = Cookies.get('cart_id');
+ const { data } = await fetch({ query : `/carts/${fromCookies}?include=cart_items` })
+ return NormalizeCart(data)
+ },
+ useHook:
+ ({ fetch }) =>
+ () => {
+ const { mutate } = useCart()
+
+ return useCallback(async function removeItem(input) {
+ const data = await fetch({ input });
+
+ await mutate(data, true)
+
+ return data;
+ }, [fetch, mutate]);
+ },
+}
diff --git a/packages/appibase/src/cart/use-update-item.tsx b/packages/appibase/src/cart/use-update-item.tsx
new file mode 100644
index 000000000..c45f8c601
--- /dev/null
+++ b/packages/appibase/src/cart/use-update-item.tsx
@@ -0,0 +1,45 @@
+import { MutationHook } from '@vercel/commerce/utils/types'
+import { useCallback } from 'react'
+import useUpdateItem, {
+ UseUpdateItem,
+} from '@vercel/commerce/cart/use-update-item'
+import { NormalizeCart } from '../api/utils/normalize'
+import Cookies from 'js-cookie'
+import useCart from './use-cart'
+
+export default useUpdateItem as UseUpdateItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {
+ await fetch({
+ query : `cart_items`,
+ method: 'PATCH',
+ body: {
+ id: input.item.id,
+ type: 'cart_item',
+ quantity: String(input.quantity)
+ }
+ })
+
+ const fromCookies = Cookies.get('cart_id');
+ const { data } = await fetch({ query : `/carts/${fromCookies}?include=cart_items` })
+ return NormalizeCart(data)
+
+ },
+ useHook:
+ ({ fetch }) =>
+ ({ item }) => {
+ const { mutate } = useCart()
+
+ return useCallback(async function updateItem(input) {
+ const data = await fetch({ input: { item, quantity: input.quantity } });
+
+ await mutate(data, true)
+
+ return data;
+ }, [fetch, mutate]);
+ },
+}
diff --git a/packages/appibase/src/checkout/use-checkout.tsx b/packages/appibase/src/checkout/use-checkout.tsx
new file mode 100644
index 000000000..76997be73
--- /dev/null
+++ b/packages/appibase/src/checkout/use-checkout.tsx
@@ -0,0 +1,16 @@
+import { SWRHook } from '@vercel/commerce/utils/types'
+import useCheckout, {
+ UseCheckout,
+} from '@vercel/commerce/checkout/use-checkout'
+
+export default useCheckout as UseCheckout
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook:
+ ({ useData }) =>
+ async (input) => ({}),
+}
diff --git a/packages/appibase/src/commerce.config.json b/packages/appibase/src/commerce.config.json
new file mode 100644
index 000000000..f46c1d6b3
--- /dev/null
+++ b/packages/appibase/src/commerce.config.json
@@ -0,0 +1,10 @@
+{
+ "provider": "appibase",
+ "features": {
+ "wishlist": false,
+ "cart": true,
+ "search": true,
+ "customerAuth": false,
+ "customCheckout": true
+ }
+}
diff --git a/packages/appibase/src/const.ts b/packages/appibase/src/const.ts
new file mode 100644
index 000000000..b71e7d61e
--- /dev/null
+++ b/packages/appibase/src/const.ts
@@ -0,0 +1,7 @@
+export const API_URL = `https://appibase.com`
+
+export const SCOPE = `storefront client`
+
+export const CLIENT_ID_STOREFRONT = process.env.APPIBASE_CLIENT_ID_STOREFRONT
+
+export const STORE_ID = process.env.APPIBASE_STORE_ID
diff --git a/packages/appibase/src/customer/address/use-add-item.tsx b/packages/appibase/src/customer/address/use-add-item.tsx
new file mode 100644
index 000000000..4f85c8472
--- /dev/null
+++ b/packages/appibase/src/customer/address/use-add-item.tsx
@@ -0,0 +1,17 @@
+import useAddItem, {
+ UseAddItem,
+} from '@vercel/commerce/customer/address/use-add-item'
+import { MutationHook } from '@vercel/commerce/utils/types'
+
+export default useAddItem as UseAddItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook:
+ ({ fetch }) =>
+ () =>
+ async () => ({}),
+}
diff --git a/packages/appibase/src/customer/card/use-add-item.tsx b/packages/appibase/src/customer/card/use-add-item.tsx
new file mode 100644
index 000000000..77d149eff
--- /dev/null
+++ b/packages/appibase/src/customer/card/use-add-item.tsx
@@ -0,0 +1,17 @@
+import useAddItem, {
+ UseAddItem,
+} from '@vercel/commerce/customer/card/use-add-item'
+import { MutationHook } from '@vercel/commerce/utils/types'
+
+export default useAddItem as UseAddItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook:
+ ({ fetch }) =>
+ () =>
+ async () => ({}),
+}
diff --git a/packages/appibase/src/customer/index.ts b/packages/appibase/src/customer/index.ts
new file mode 100644
index 000000000..6c903ecc5
--- /dev/null
+++ b/packages/appibase/src/customer/index.ts
@@ -0,0 +1 @@
+export { default as useCustomer } from './use-customer'
diff --git a/packages/appibase/src/customer/use-customer.tsx b/packages/appibase/src/customer/use-customer.tsx
new file mode 100644
index 000000000..04c48943d
--- /dev/null
+++ b/packages/appibase/src/customer/use-customer.tsx
@@ -0,0 +1,17 @@
+import { SWRHook } from '@vercel/commerce/utils/types'
+import useCustomer, {
+ UseCustomer,
+} from '@vercel/commerce/customer/use-customer'
+
+export default useCustomer as UseCustomer
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ input, options, fetch }) {},
+ useHook: () => () => {
+ return async function addItem() {
+ return {}
+ }
+ },
+}
diff --git a/packages/appibase/src/data.json b/packages/appibase/src/data.json
new file mode 100644
index 000000000..18c8ee718
--- /dev/null
+++ b/packages/appibase/src/data.json
@@ -0,0 +1,235 @@
+{
+ "products": [
+ {
+ "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjA=",
+ "name": "New Short Sleeve T-Shirt",
+ "vendor": "Next.js",
+ "path": "/new-short-sleeve-t-shirt",
+ "slug": "new-short-sleeve-t-shirt",
+ "price": { "value": 25, "currencyCode": "USD" },
+ "descriptionHtml": "Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.
",
+ "images": [
+ {
+ "url": "/assets/drop-shirt-0.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-1.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/drop-shirt-2.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ }
+ ],
+ "variants": [
+ {
+ "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjAss=",
+ "options": [
+ {
+ "__typename": "MultipleChoiceOption",
+ "id": "asd",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "XL"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "options": [
+ {
+ "id": "option-color",
+ "displayName": "Color",
+ "values": [
+ {
+ "label": "color",
+ "hexColors": ["#222"]
+ }
+ ]
+ },
+ {
+ "id": "option-size",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "S"
+ },
+ {
+ "label": "M"
+ },
+ {
+ "label": "L"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "Z2lkOi8vc2hvcGlmeS9Qcm9ksdWN0LzU0NDczMjUwMjQ0MjA=",
+ "name": "Lightweight Jacket",
+ "vendor": "Next.js",
+ "path": "/lightweight-jacket",
+ "slug": "lightweight-jacket",
+ "price": { "value": 249.99, "currencyCode": "USD" },
+ "descriptionHtml": "Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.
",
+ "images": [
+ {
+ "url": "/assets/lightweight-jacket-0.png",
+ "altText": "Lightweight Jacket",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/lightweight-jacket-1.png",
+ "altText": "Lightweight Jacket",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/lightweight-jacket-2.png",
+ "altText": "Lightweight Jacket",
+ "width": 1000,
+ "height": 1000
+ }
+ ],
+ "variants": [
+ {
+ "id": "Z2lkOid8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjAss=",
+ "options": [
+ {
+ "__typename": "MultipleChoiceOption",
+ "id": "asd",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "XL"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "options": [
+ {
+ "id": "option-color",
+ "displayName": "Color",
+ "values": [
+ {
+ "label": "color",
+ "hexColors": ["#222"]
+ }
+ ]
+ },
+ {
+ "id": "option-size",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "S"
+ },
+ {
+ "label": "M"
+ },
+ {
+ "label": "L"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "Z2lkOis8vc2hvcGlmsddeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjA=",
+ "name": "Shirt",
+ "vendor": "Next.js",
+ "path": "/shirt",
+ "slug": "shirt",
+ "price": { "value": 25, "currencyCode": "USD" },
+ "descriptionHtml": "Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.
",
+ "images": [
+ {
+ "url": "/assets/t-shirt-0.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/t-shirt-1.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/t-shirt-2.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/t-shirt-3.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ },
+ {
+ "url": "/assets/t-shirt-4.png",
+ "altText": "Shirt",
+ "width": 1000,
+ "height": 1000
+ }
+ ],
+ "variants": [
+ {
+ "id": "Z2lkOi8vc2hvcGlmeS9Qcms9kdWN0LzU0NDczMjUwMjQ0MjAss=",
+ "options": [
+ {
+ "__typename": "MultipleChoiceOption",
+ "id": "asd",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "XL"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "options": [
+ {
+ "id": "option-color",
+ "displayName": "Color",
+ "values": [
+ {
+ "label": "color",
+ "hexColors": ["#222"]
+ }
+ ]
+ },
+ {
+ "id": "option-size",
+ "displayName": "Size",
+ "values": [
+ {
+ "label": "S"
+ },
+ {
+ "label": "M"
+ },
+ {
+ "label": "L"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/appibase/src/fetcher.ts b/packages/appibase/src/fetcher.ts
new file mode 100644
index 000000000..f9d5eb1e5
--- /dev/null
+++ b/packages/appibase/src/fetcher.ts
@@ -0,0 +1,25 @@
+import { Fetcher } from '@vercel/commerce/utils/types'
+import { API_URL } from './const'
+import { handleFetchResponse } from './utils'
+import { GetAccessToken } from './api/utils/access-token'
+import Kitsu from "kitsu";
+
+const fetcher: Fetcher = async ({
+ method = 'GET',
+ variables,
+ query = '',
+ body
+}) => {
+ const { locale, ...vars } = variables ?? {}
+
+ const api = new Kitsu({ baseURL: API_URL + '/api/v1', camelCaseTypes: false, pluralize: false })
+ api.headers.Authorization = `Bearer ${await GetAccessToken()}`
+
+ if(method === 'GET') return await api.get(query)
+ else if(method === 'POST') return await api.create(query, body)
+ else if(method === 'DELETE') return await api.remove(query, body)
+ else if(method === 'PATCH') return await api.update(query, body)
+}
+
+export default fetcher
+
diff --git a/packages/appibase/src/index.tsx b/packages/appibase/src/index.tsx
new file mode 100644
index 000000000..6bdaba8ba
--- /dev/null
+++ b/packages/appibase/src/index.tsx
@@ -0,0 +1,12 @@
+import {
+ getCommerceProvider,
+ useCommerce as useCoreCommerce,
+} from '@vercel/commerce'
+import { appibaseProvider, AppibaseProvider } from './provider'
+
+export { appibaseProvider }
+export type { AppibaseProvider }
+
+export const CommerceProvider = getCommerceProvider(appibaseProvider)
+
+export const useCommerce = () => useCoreCommerce()
diff --git a/packages/appibase/src/next.config.cjs b/packages/appibase/src/next.config.cjs
new file mode 100644
index 000000000..9885a2bae
--- /dev/null
+++ b/packages/appibase/src/next.config.cjs
@@ -0,0 +1,8 @@
+const commerce = require('./commerce.config.json')
+
+module.exports = {
+ commerce,
+ images: {
+ domains: ['localhost', 'shopify.vercel.store', 'vercel.store', 'demo.vercel.store'],
+ },
+}
diff --git a/packages/appibase/src/product/index.ts b/packages/appibase/src/product/index.ts
new file mode 100644
index 000000000..426a3edcd
--- /dev/null
+++ b/packages/appibase/src/product/index.ts
@@ -0,0 +1,2 @@
+export { default as usePrice } from './use-price'
+export { default as useSearch } from './use-search'
diff --git a/packages/appibase/src/product/use-price.tsx b/packages/appibase/src/product/use-price.tsx
new file mode 100644
index 000000000..fd42d7033
--- /dev/null
+++ b/packages/appibase/src/product/use-price.tsx
@@ -0,0 +1,2 @@
+export * from '@vercel/commerce/product/use-price'
+export { default } from '@vercel/commerce/product/use-price'
diff --git a/packages/appibase/src/product/use-search.tsx b/packages/appibase/src/product/use-search.tsx
new file mode 100644
index 000000000..8dc929c7c
--- /dev/null
+++ b/packages/appibase/src/product/use-search.tsx
@@ -0,0 +1,54 @@
+import { SWRHook } from '@vercel/commerce/utils/types'
+import useSearch, { UseSearch } from '@vercel/commerce/product/use-search'
+export default useSearch as UseSearch
+import type { AppibaseProduct } from '../types'
+import { NormalizeProduct } from '../api/utils/normalize'
+
+export type SearchProductsInput = {
+ search?: string
+ categoryId?: number | string
+ brandId?: number
+ sort?: string
+ locale?: string
+}
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ url: '/products?filter[is_parent_true]=true&include=prices',
+ method: 'GET',
+ },
+ async fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
+ let url = options.url
+
+ if (search) url += `&filter[name_cont_all]=${search}`
+ if (Number.isInteger(Number(categoryId)))
+ url += `&filter[collections_id_eq]=${categoryId}`
+ // if (Number.isInteger(brandId))
+ // url.searchParams.set('brandId', String(brandId))
+ // if (sort) url.searchParams.set('sort', sort)
+
+
+ const response = await fetch({
+ query: url,
+ method: options.method,
+ })
+
+ return { found: response.data.length, products: response.data.map((p : AppibaseProduct) => NormalizeProduct(p) ) }
+ },
+ useHook:
+ ({ useData }) =>
+ (input = {}) => {
+ return useData({
+ input: [
+ ['search', input.search],
+ ['categoryId', input.categoryId],
+ ['brandId', input.brandId],
+ ['sort', input.sort],
+ ],
+ swrOptions: {
+ revalidateOnFocus: false,
+ ...input.swrOptions,
+ },
+ })
+ },
+}
diff --git a/packages/appibase/src/provider.ts b/packages/appibase/src/provider.ts
new file mode 100644
index 000000000..818967643
--- /dev/null
+++ b/packages/appibase/src/provider.ts
@@ -0,0 +1,22 @@
+import fetcher from './fetcher'
+import { handler as useCart } from './cart/use-cart'
+import { handler as useAddItem } from './cart/use-add-item'
+import { handler as useUpdateItem } from './cart/use-update-item'
+import { handler as useRemoveItem } from './cart/use-remove-item'
+import { handler as useCustomer } from './customer/use-customer'
+import { handler as useSearch } from './product/use-search'
+import { handler as useLogin } from './auth/use-login'
+import { handler as useLogout } from './auth/use-logout'
+import { handler as useSignup } from './auth/use-signup'
+
+export const appibaseProvider = {
+ locale: 'en-us',
+ cartCookie: 'session',
+ fetcher: fetcher,
+ cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
+ customer: { useCustomer },
+ products: { useSearch },
+ auth: { useLogin, useLogout, useSignup },
+}
+
+export type AppibaseProvider = typeof appibaseProvider
diff --git a/packages/appibase/src/types.ts b/packages/appibase/src/types.ts
new file mode 100644
index 000000000..e55fc90f6
--- /dev/null
+++ b/packages/appibase/src/types.ts
@@ -0,0 +1,132 @@
+export type AppibaseVariation = {
+ id: string
+ name: string
+ options?: AppibaseVariationOption[]
+}
+
+export type AppibaseVariationOption = {
+ id: string
+ name: string
+ variation_name?: string
+ variation?: AppibaseVariation
+}
+
+export type PriceList = {
+ id: number
+ name: string
+ description?: string
+ currency: string
+ tax_incl: boolean
+}
+
+export type Amount = {
+ cents: number
+ float: number
+ formatted: string
+}
+
+export type AppibasePrices = {
+ data: AppibasePrice[]
+}
+
+export type AppibasePrice = {
+ id: number
+ currency: string
+ amount: Amount
+ original_amount: Amount
+ price_list?: PriceList
+ product?: AppibaseProduct
+}
+
+export type StockLocation = {
+ id: number
+ name: string
+ description?: string
+}
+
+export type StockItem = {
+ id: number
+ quantity: number
+ reserved: number
+ available: number
+ stock_location?: StockLocation
+ product?: AppibaseProduct
+}
+
+export type AppibaseVariations = {
+ data: AppibaseVariation[]
+}
+
+export type AppibaseProductChildren = {
+ data: AppibaseProduct[]
+}
+
+export type AppibaseVariationOptions = {
+ data: AppibaseVariationOption[]
+}
+
+export type AppibaseProduct = {
+ id: string
+ name: string
+ description: string
+ slug: string
+ sku: string
+ category: string
+ vendor: string
+ tags: string[]
+ image_urls: string[]
+ is_parent: boolean
+ active: boolean
+ livemode: boolean
+ parent?: AppibaseProduct
+ children?: AppibaseProductChildren
+ variations?: AppibaseVariations
+ variation_options?: AppibaseVariationOptions
+ prices: AppibasePrices
+ stock_items?: StockItem[]
+}
+
+export type AppibaseCollection = {
+ id: number
+ name: string
+ description: string
+ slug: string
+ image_url: string
+ is_parent: boolean
+ active: boolean
+ livemode: boolean
+ parent?: AppibaseCollection
+ children?: AppibaseCollection[]
+ products?: AppibaseProduct[]
+}
+
+export type AppibaseCartItems = {
+ data: AppibaseCartItem[]
+}
+
+export type AppibaseCart = {
+ id: string
+ guest: boolean
+ email: string
+ currency: string
+ tax_incl: boolean
+ tax_rate: number
+ subtotal_amount: Amount
+ shipping_amount: Amount
+ tax_amount: Amount
+ total_amount: Amount
+ cart_items: AppibaseCartItems
+}
+
+export type AppibaseCartItem = {
+ id: string
+ name: string
+ description: string
+ sku: string
+ image_url: string
+ quantity: string
+ currency: string
+ price: Amount
+ original_price: Amount
+ subtotal_amount: Amount
+}
\ No newline at end of file
diff --git a/packages/appibase/src/utils/handle-fetch-response.ts b/packages/appibase/src/utils/handle-fetch-response.ts
new file mode 100644
index 000000000..927ab54f1
--- /dev/null
+++ b/packages/appibase/src/utils/handle-fetch-response.ts
@@ -0,0 +1,27 @@
+import { FetcherError } from '@vercel/commerce/utils/errors'
+
+export function getError(errors: any[] | null, status: number) {
+ errors = errors ?? [{ message: 'Failed to fetch Shopify API' }]
+ return new FetcherError({ errors, status })
+}
+
+export async function getAsyncError(res: Response) {
+ const data = await res.json()
+ return getError(data.errors, res.status)
+}
+
+const handleFetchResponse = async (res: Response) => {
+ if (res.ok) {
+ const { data, errors } = await res.json()
+
+ if (errors && errors.length) {
+ throw getError(errors, res.status)
+ }
+
+ return data
+ }
+
+ throw await getAsyncError(res)
+}
+
+export default handleFetchResponse
diff --git a/packages/appibase/src/utils/index.ts b/packages/appibase/src/utils/index.ts
new file mode 100644
index 000000000..5aa8f04ea
--- /dev/null
+++ b/packages/appibase/src/utils/index.ts
@@ -0,0 +1 @@
+export { default as handleFetchResponse } from './handle-fetch-response'
diff --git a/packages/appibase/src/wishlist/use-add-item.tsx b/packages/appibase/src/wishlist/use-add-item.tsx
new file mode 100644
index 000000000..75f067c3a
--- /dev/null
+++ b/packages/appibase/src/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/packages/appibase/src/wishlist/use-remove-item.tsx b/packages/appibase/src/wishlist/use-remove-item.tsx
new file mode 100644
index 000000000..a2d3a8a05
--- /dev/null
+++ b/packages/appibase/src/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/packages/appibase/src/wishlist/use-wishlist.tsx b/packages/appibase/src/wishlist/use-wishlist.tsx
new file mode 100644
index 000000000..b2785d46f
--- /dev/null
+++ b/packages/appibase/src/wishlist/use-wishlist.tsx
@@ -0,0 +1,43 @@
+import { HookFetcher } from '@vercel/commerce/utils/types'
+import type { Product } from '@vercel/commerce/types/product'
+
+const defaultOpts = {}
+
+export type Wishlist = {
+ items: [
+ {
+ product_id: number
+ variant_id: number
+ id: number
+ product: Product
+ }
+ ]
+}
+
+export interface UseWishlistOptions {
+ includeProducts?: boolean
+}
+
+export interface UseWishlistInput extends UseWishlistOptions {
+ customerId?: number
+}
+
+export const fetcher: HookFetcher = () => {
+ return null
+}
+
+export function extendHook(
+ customFetcher: typeof fetcher,
+ // swrOptions?: SwrOptions
+ swrOptions?: any
+) {
+ const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
+ return { data: null }
+ }
+
+ useWishlist.extend = extendHook
+
+ return useWishlist
+}
+
+export default extendHook(fetcher)
diff --git a/packages/appibase/taskfile.js b/packages/appibase/taskfile.js
new file mode 100644
index 000000000..39b1b2a86
--- /dev/null
+++ b/packages/appibase/taskfile.js
@@ -0,0 +1,20 @@
+export async function build(task, opts) {
+ await task
+ .source('src/**/*.+(ts|tsx|js)')
+ .swc({ dev: opts.dev, outDir: 'dist', baseUrl: 'src' })
+ .target('dist')
+ .source('src/**/*.+(cjs|json)')
+ .target('dist')
+ task.$.log('Compiled src files')
+}
+
+export async function release(task) {
+ await task.clear('dist').start('build')
+}
+
+export default async function dev(task) {
+ const opts = { dev: true }
+ await task.clear('dist')
+ await task.start('build', opts)
+ await task.watch('src/**/*.+(ts|tsx|js|cjs|json)', 'build', opts)
+}
diff --git a/packages/appibase/tsconfig.json b/packages/appibase/tsconfig.json
new file mode 100644
index 000000000..cd04ab2ff
--- /dev/null
+++ b/packages/appibase/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "outDir": "dist",
+ "baseUrl": "src",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "declaration": true,
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "esModuleInterop": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "incremental": true,
+ "jsx": "react-jsx"
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/site/commerce-config.js b/site/commerce-config.js
index d52797e51..3c2fad564 100644
--- a/site/commerce-config.js
+++ b/site/commerce-config.js
@@ -19,6 +19,7 @@ const PROVIDERS = [
'@vercel/commerce-kibocommerce',
'@vercel/commerce-spree',
'@vercel/commerce-commercejs',
+ 'appibase'
]
function getProviderName() {
diff --git a/site/commerce.config.json b/site/commerce.config.json
index ad72b58de..b9ebeb681 100644
--- a/site/commerce.config.json
+++ b/site/commerce.config.json
@@ -4,6 +4,6 @@
"search": true,
"wishlist": false,
"customerAuth": false,
- "customCheckout": false
+ "customCheckout": true
}
}
diff --git a/site/tsconfig.json b/site/tsconfig.json
index 7c91afd6f..d4eac60b5 100644
--- a/site/tsconfig.json
+++ b/site/tsconfig.json
@@ -23,8 +23,8 @@
"@components/*": ["components/*"],
"@commerce": ["../packages/commerce/src"],
"@commerce/*": ["../packages/commerce/src/*"],
- "@framework": ["../packages/local/src"],
- "@framework/*": ["../packages/local/src/*"]
+ "@framework": ["../packages/appibase/src"],
+ "@framework/*": ["../packages/appibase/src/*"]
}
},
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
diff --git a/yarn.lock b/yarn.lock
index fc850a7a8..2b2092a7b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1985,6 +1985,13 @@ axios@^0.21.1:
dependencies:
follow-redirects "^1.14.0"
+axios@^0.26.0:
+ version "0.26.1"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
+ integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
+ dependencies:
+ follow-redirects "^1.14.8"
+
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -2802,6 +2809,15 @@ dependency-graph@^0.11.0:
resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27"
integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==
+deserialize-json-api@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/deserialize-json-api/-/deserialize-json-api-1.4.0.tgz#776df0d56db1cab420d1d58e7b3d916f0140fda8"
+ integrity sha512-rfViAb34ATDUygZwuU8VSi/FC1T5m+eIAjPiTwcuwCvf3V6uWdnKSYZ1Y8myHW9RCKPhnBKMHp/3ZVF/uEZISQ==
+ dependencies:
+ lodash.camelcase "^4.3.0"
+ lodash.isarray "^4.0.0"
+ lodash.isplainobject "^4.0.6"
+
detect-indent@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
@@ -3469,6 +3485,11 @@ follow-redirects@^1.14.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
+follow-redirects@^1.14.8:
+ version "1.14.9"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
+ integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
+
for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -4383,6 +4404,11 @@ js-cookie@^3.0.1:
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414"
integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==
+js-cookies@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/js-cookies/-/js-cookies-1.0.4.tgz#d46e576c420ff6d5542c0f52b6d4ef7d637e754e"
+ integrity sha1-1G5XbEIP9tVULA9SttTvfWN+dU4=
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -4400,6 +4426,11 @@ jsesc@^2.5.1:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+json-api-response-converter@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/json-api-response-converter/-/json-api-response-converter-1.6.0.tgz#66fc07c92d39da8a0a0d2eebc10b8b1b9b38d962"
+ integrity sha512-PetQPJUmimf5OuDPkjWij85LdNKctG9SnGQ9EfdOQxt2nwEviraXGekEgJcEwsgeKxnrG2zD7D+nQJ6YAkzM7Q==
+
json-buffer@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
@@ -4531,6 +4562,20 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+kitsu-core@^10.0.0-alpha.22:
+ version "10.0.0-alpha.22"
+ resolved "https://registry.yarnpkg.com/kitsu-core/-/kitsu-core-10.0.0-alpha.22.tgz#783fa6352245f147b1ddde057aef424d4d0e4da9"
+ integrity sha512-+1O27hmKlTxYSTOuJw0D5leJMD5KMpcRczb4k3CGKylRMyBlNqsCCFLb6FNDRIDVu4szRYKAnypljVJMgSkovg==
+
+kitsu@^10.0.0-alpha.22:
+ version "10.0.0-alpha.22"
+ resolved "https://registry.yarnpkg.com/kitsu/-/kitsu-10.0.0-alpha.22.tgz#d30281bad42572bed5ea78adf6fe3e7b92e93b65"
+ integrity sha512-EgDxOfiBEBj7n0FgEqzJSReqiXcUDwcKNKiVJcgbaCjISdkN9PdiSNTN3KWRYS4EuetLkIjWpH40VMkGAW4fjg==
+ dependencies:
+ axios "^0.26.0"
+ kitsu-core "^10.0.0-alpha.22"
+ pluralize "^8.0.0"
+
language-subtag-registry@~0.3.2:
version "0.3.21"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
@@ -4694,6 +4739,11 @@ lodash.includes@^4.3.0:
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
+lodash.isarray@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-4.0.0.tgz#2aca496b28c4ca6d726715313590c02e6ea34403"
+ integrity sha1-KspJayjEym1yZxUxNZDALm6jRAM=
+
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
@@ -5520,6 +5570,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+pluralize@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
+ integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
+
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -7122,6 +7177,11 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
+urijs@^1.19.10:
+ version "1.19.10"
+ resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.10.tgz#8e2fe70a8192845c180f75074884278f1eea26cb"
+ integrity sha512-EzauQlgKuJgsXOqoMrCiePBf4At5jVqRhXykF3Wfb8ZsOBMxPcfiVBcsHXug4Aepb/ICm2PIgqAUGMelgdrWEg==
+
urix@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"