diff --git a/framework/commerce/config.js b/framework/commerce/config.js index 28502a04e..d629b45e3 100644 --- a/framework/commerce/config.js +++ b/framework/commerce/config.js @@ -14,6 +14,7 @@ const PROVIDERS = [ 'swell', 'vendure', 'local', + 'spree', ] function getProviderName() { diff --git a/framework/spree/.env.template b/framework/spree/.env.template index d6644591a..8831ec06c 100644 --- a/framework/spree/.env.template +++ b/framework/spree/.env.template @@ -2,7 +2,12 @@ COMMERCE_PROVIDER=spree -SPREE_API_HOST = 'http://localhost:3000' +{# public (available in the web browser) #} +NEXT_PUBLIC_SPREE_API_HOST=http://localhost:3000 -# TODO: -# COMMERCE_IMAGE_HOST +{# private #} +NEXT_PUBLIC_SPREE_DEFAULT_LOCALE=en-us +NEXT_PUBLIC_SPREE_CART_COOKIE_NAME=spree_cart + +{# # TODO: #} +{# # COMMERCE_IMAGE_HOST #} diff --git a/framework/spree/api/endpoints/cart/index.ts b/framework/spree/api/endpoints/cart/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/framework/spree/api/endpoints/cart/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/framework/spree/api/endpoints/catalog/index.ts b/framework/spree/api/endpoints/catalog/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/framework/spree/api/endpoints/catalog/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/framework/spree/api/endpoints/catalog/products.ts b/framework/spree/api/endpoints/catalog/products.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/framework/spree/api/endpoints/catalog/products.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/framework/spree/api/endpoints/checkout/index.ts b/framework/spree/api/endpoints/checkout/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/framework/spree/api/endpoints/checkout/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/framework/spree/api/endpoints/customer/index.ts b/framework/spree/api/endpoints/customer/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/framework/spree/api/endpoints/customer/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/framework/spree/api/endpoints/login/index.ts b/framework/spree/api/endpoints/login/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/framework/spree/api/endpoints/login/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/framework/spree/api/endpoints/logout/index.ts b/framework/spree/api/endpoints/logout/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/framework/spree/api/endpoints/logout/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/framework/spree/api/endpoints/signup/index.ts b/framework/spree/api/endpoints/signup/index.ts new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/framework/spree/api/endpoints/signup/index.ts @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/framework/spree/api/endpoints/wishlist/index.tsx b/framework/spree/api/endpoints/wishlist/index.tsx new file mode 100644 index 000000000..491bf0ac9 --- /dev/null +++ b/framework/spree/api/endpoints/wishlist/index.tsx @@ -0,0 +1 @@ +export default function noopApi(...args: any[]): void {} diff --git a/framework/spree/api/index.ts b/framework/spree/api/index.ts new file mode 100644 index 000000000..13461dcb7 --- /dev/null +++ b/framework/spree/api/index.ts @@ -0,0 +1,39 @@ +import type { APIProvider, CommerceAPI, CommerceAPIConfig } from '@commerce/api' +import { getCommerceApi as commerceApi } from '@commerce/api' +import createApiFetch from './utils/create-api-fetch' + +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 SpreeApiConfig extends CommerceAPIConfig {} + +const config: SpreeApiConfig = { + commerceUrl: '', + apiToken: '', + cartCookie: '', + customerCookie: '', + cartCookieMaxAge: 2592000, + fetch: createApiFetch(() => getCommerceApi().getConfig()), +} + +const operations = { + getAllPages, + getPage, + getSiteInfo, + getCustomerWishlist, + getAllProductPaths, + getAllProducts, + getProduct, +} + +export const provider: APIProvider = { config, operations } + +export type SpreeApiProvider = APIProvider + +export const getCommerceApi = (customProvider: APIProvider = provider) => + commerceApi(customProvider) diff --git a/framework/spree/api/operations/get-all-pages.ts b/framework/spree/api/operations/get-all-pages.ts new file mode 100644 index 000000000..4ff56b8e8 --- /dev/null +++ b/framework/spree/api/operations/get-all-pages.ts @@ -0,0 +1,37 @@ +export type Page = { url: string } +import { OperationContext, OperationOptions } from '@commerce/api/operations' +import { GetAllPagesOperation } from '@commerce/types/page' +import type { SpreeApiConfig, SpreeApiProvider } from '../index' + +export default function getAllPagesOperation({ + commerce, +}: OperationContext) { + async function getAllPages(options?: { + config?: Partial + preview?: boolean + }): Promise + + async function getAllPages( + opts: { + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getAllPages({ + config, + preview, + query, + }: { + url?: string + config?: Partial + preview?: boolean + query?: string + } = {}): Promise { + return { + pages: [], + } + } + + return getAllPages +} diff --git a/framework/spree/api/operations/get-all-product-paths.ts b/framework/spree/api/operations/get-all-product-paths.ts new file mode 100644 index 000000000..e2368a833 --- /dev/null +++ b/framework/spree/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 { + return Promise.resolve({ + // products: data.products.map(({ path }) => ({ path })), + // TODO: Return Storefront [{ path: '/long-sleeve-shirt' }, ...] from Spree products. Paths using product IDs are fine too. + products: [], + }) + } + + return getAllProductPaths +} diff --git a/framework/spree/api/operations/get-all-products.ts b/framework/spree/api/operations/get-all-products.ts new file mode 100644 index 000000000..d19280f4c --- /dev/null +++ b/framework/spree/api/operations/get-all-products.ts @@ -0,0 +1,79 @@ +import { Product } from '@commerce/types/product' +import { GetAllProductsOperation } from '@commerce/types/product' +import type { OperationContext } from '@commerce/api/operations' +import type { LocalConfig, Provider, SpreeApiProvider } from '../index' +import type { IProducts } from '@spree/storefront-api-v2-sdk/types/interfaces/Product' +// import data from '../../../local/data.json' + +export default function getAllProductsOperation({ + commerce, +}: OperationContext) { + async function getAllProducts({ + query = 'products.list', + variables = { first: 10 }, + config: userConfig, + }: { + query?: string + variables?: T['variables'] + config?: Partial + } = {}): Promise<{ products: Product[] | any[] }> { + const config = commerce.getConfig(userConfig) + const { fetch: apiFetch /*, locale*/ } = config + const first = variables.first // How many products to fetch. + + console.log( + 'sdfuasdufahsdf variables = ', + variables, + 'query = ', + query, + 'config = ', + config + ) + + console.log('sdfasdg') + + const { data } = await apiFetch( + query, + { variables } + // { + // ...(locale && {}), + // } + ) + + console.log('asuidfhasdf', data) + + // return { + // products: data.products.edges.map(({ node }) => + // normalizeProduct(node as ShopifyProduct) + // ), + // } + + const normalizedProducts: Product[] = data.data.map((spreeProduct) => { + return { + id: spreeProduct.id, + name: spreeProduct.attributes.name, + description: spreeProduct.attributes.description, + images: [], + variants: [], + options: [], + price: { + value: 10, + currencyCode: 'USD', + retailPrice: 8, + salePrice: 7, + listPrice: 6, + extendedSalePrice: 2, + extendedListPrice: 1, + }, + } + }) + + return { + // products: data.products, + // TODO: Return Spree products. + products: normalizedProducts, + } + } + + return getAllProducts +} diff --git a/framework/spree/api/operations/get-customer-wishlist.ts b/framework/spree/api/operations/get-customer-wishlist.ts new file mode 100644 index 000000000..8c34b9e87 --- /dev/null +++ b/framework/spree/api/operations/get-customer-wishlist.ts @@ -0,0 +1,6 @@ +export default function getCustomerWishlistOperation() { + function getCustomerWishlist(): any { + return { wishlist: {} } + } + return getCustomerWishlist +} diff --git a/framework/spree/api/operations/get-page.ts b/framework/spree/api/operations/get-page.ts new file mode 100644 index 000000000..b0cfdf58f --- /dev/null +++ b/framework/spree/api/operations/get-page.ts @@ -0,0 +1,13 @@ +export type Page = any +export type GetPageResult = { page?: Page } + +export type PageVariables = { + id: number +} + +export default function getPageOperation() { + function getPage(): Promise { + return Promise.resolve({}) + } + return getPage +} diff --git a/framework/spree/api/operations/get-product.ts b/framework/spree/api/operations/get-product.ts new file mode 100644 index 000000000..de0804592 --- /dev/null +++ b/framework/spree/api/operations/get-product.ts @@ -0,0 +1,28 @@ +import type { LocalConfig } from '../index' +import { Product } from '@commerce/types/product' +import { GetProductOperation } from '@commerce/types/product' +import data from '../../../local/data.json' +import type { OperationContext } from '@commerce/api/operations' + +export default function getProductOperation({ + commerce, +}: OperationContext) { + async function getProduct({ + query = '', + variables, + config, + }: { + query?: string + variables?: T['variables'] + config?: Partial + preview?: boolean + } = {}): Promise { + return { + product: data.products.find(({ slug }) => slug === variables!.slug), + // TODO: Return Spree product. + // product: {}, + } + } + + return getProduct +} diff --git a/framework/spree/api/operations/get-site-info.ts b/framework/spree/api/operations/get-site-info.ts new file mode 100644 index 000000000..159fb6004 --- /dev/null +++ b/framework/spree/api/operations/get-site-info.ts @@ -0,0 +1,44 @@ +import { OperationContext } from '@commerce/api/operations' +import { Category } from '@commerce/types/site' +import { LocalConfig } from '../index' + +export type GetSiteInfoResult< + T extends { categories: any[]; brands: any[] } = { + categories: Category[] + brands: any[] + } +> = T + +export default function getSiteInfoOperation({}: OperationContext) { + // TODO: Get Spree categories for display in React components. + function getSiteInfo({ + query, + variables, + config: cfg, + }: { + query?: string + variables?: any + config?: Partial + preview?: boolean + } = {}): Promise { + return Promise.resolve({ + categories: [ + { + id: 'new-arrivals', + name: 'New Arrivals', + slug: 'new-arrivals', + path: '/new-arrivals', + }, + { + id: 'featured', + name: 'Featured', + slug: 'featured', + path: '/featured', + }, + ], + brands: [], + }) + } + + return getSiteInfo +} diff --git a/framework/spree/api/operations/index.ts b/framework/spree/api/operations/index.ts new file mode 100644 index 000000000..086fdf83a --- /dev/null +++ b/framework/spree/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/framework/spree/api/utils/create-api-fetch.ts b/framework/spree/api/utils/create-api-fetch.ts new file mode 100644 index 000000000..162e5c1d5 --- /dev/null +++ b/framework/spree/api/utils/create-api-fetch.ts @@ -0,0 +1,154 @@ +// import { FetcherError } from '@commerce/utils/errors' +// import type { GraphQLFetcher } from '@commerce/api' +// import type { BigcommerceConfig } from '../index' + +import { GraphQLFetcher, GraphQLFetcherResult } from '@commerce/api' +import { SpreeApiConfig } from '..' +import { errors, makeClient } from '@spree/storefront-api-v2-sdk' +import { requireConfigValue } from 'framework/spree/isomorphicConfig' +import convertSpreeErrorToGraphQlError from 'framework/spree/utils/convertSpreeErrorToGraphQlError' +import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse' +import type { + JsonApiDocument, + JsonApiListResponse, +} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' +// import fetch from './fetch' + +// const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher = +// (getConfig) => +// async (query: string, { variables, preview } = {}, fetchOptions) => { +// // log.warn(query) +// const config = getConfig() +// const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { +// ...fetchOptions, +// method: 'POST', +// headers: { +// Authorization: `Bearer ${config.apiToken}`, +// ...fetchOptions?.headers, +// 'Content-Type': 'application/json', +// }, +// body: JSON.stringify({ +// query, +// variables, +// }), +// }) + +// const json = await res.json() +// if (json.errors) { +// throw new FetcherError({ +// errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }], +// status: res.status, +// }) +// } + +// return { data: json.data, res } +// } + +// export default fetchGraphqlApi + +const createApiFetch: ( + getConfig: () => SpreeApiConfig +) => GraphQLFetcher< + GraphQLFetcherResult +> = (getConfig) => { + const client = makeClient({ host: requireConfigValue('spreeApiHost') }) + + // FIXME: Allow Spree SDK to use fetch instead of axios. + return async (query, queryData = {}, fetchOptions = {}) => { + const url = query + console.log('ydsfgasgdfagsdf', url) + const { variables } = queryData + let prev = null // FIXME: + const clientEndpointMethod = url + .split('.') + .reduce((clientNode: any, pathPart) => { + prev = clientNode + //FIXME: use actual type instead of any. + // TODO: Fix clientNode type + return clientNode[pathPart] + }, client) + .bind(prev) + + console.log('aisdfuiuashdf', clientEndpointMethod) + + const storeResponse: ResultResponse = + await clientEndpointMethod() // FIXME: Not the best to use variables here as it's type is any. + // await clientEndpointMethod(...variables.args) // FIXME: Not the best to use variables here as it's type is any. + + console.log('87868767868', storeResponse) + + if (storeResponse.success()) { + return { + data: storeResponse.success(), + res: storeResponse as any, //FIXME: MUST BE FETCH RESPONSE + } + } + + const storeResponseError = storeResponse.fail() + + if (storeResponseError instanceof errors.SpreeError) { + throw convertSpreeErrorToGraphQlError(storeResponseError) + } + + throw storeResponseError + // throw getError( + // [ + // { + // message: `${err} \n Most likely related to an unexpected output. e.g the store might be protected with password or not available.`, + // }, + // ], + // 500 + // ) + // console.log('jsdkfhjasdf', getConfig()) + // // await + // return { + // data: [], + // res: , + // } + } +} + +export default createApiFetch + +// LOCAL + +// fetch( +// query: string, +// queryData?: CommerceAPIFetchOptions, +// fetchOptions?: RequestInit +// ): Promise> + +// import { FetcherError } from '@commerce/utils/errors' +// import type { GraphQLFetcher } from '@commerce/api' +// import type { LocalConfig } from '../index' +// import fetch from './fetch' + +// const fetchGraphqlApi: (getConfig: () => LocalConfig) => GraphQLFetcher = +// (getConfig) => +// async (query: string, { variables, preview } = {}, fetchOptions) => { +// const config = getConfig() +// const res = await fetch(config.commerceUrl, { +// ...fetchOptions, +// method: 'POST', +// headers: { +// ...fetchOptions?.headers, +// 'Content-Type': 'application/json', +// }, +// body: JSON.stringify({ +// query, +// variables, +// }), +// }) + +// const json = await res.json() +// if (json.errors) { +// throw new FetcherError({ +// errors: json.errors ?? [{ message: 'Failed to fetch for API' }], +// status: res.status, +// }) +// } + +// return { data: json.data, res } +// } + +// export default fetchGraphqlApi diff --git a/framework/spree/api/utils/fetch.ts b/framework/spree/api/utils/fetch.ts new file mode 100644 index 000000000..9d9fff3ed --- /dev/null +++ b/framework/spree/api/utils/fetch.ts @@ -0,0 +1,3 @@ +import zeitFetch from '@vercel/fetch' + +export default zeitFetch() diff --git a/framework/spree/auth/index.ts b/framework/spree/auth/index.ts new file mode 100644 index 000000000..36e757a89 --- /dev/null +++ b/framework/spree/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/spree/auth/use-login.tsx b/framework/spree/auth/use-login.tsx new file mode 100644 index 000000000..28351dc7f --- /dev/null +++ b/framework/spree/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 + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher() { + return null + }, + useHook: () => () => { + return async function () {} + }, +} diff --git a/framework/spree/auth/use-logout.tsx b/framework/spree/auth/use-logout.tsx new file mode 100644 index 000000000..9b3fc3e44 --- /dev/null +++ b/framework/spree/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 + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher() { + return null + }, + useHook: + ({ fetch }) => + () => + async () => {}, +} diff --git a/framework/spree/auth/use-signup.tsx b/framework/spree/auth/use-signup.tsx new file mode 100644 index 000000000..e9ad13458 --- /dev/null +++ b/framework/spree/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 + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher() { + return null + }, + useHook: + ({ fetch }) => + () => + () => {}, +} diff --git a/framework/spree/cart/index.ts b/framework/spree/cart/index.ts new file mode 100644 index 000000000..3b8ba990e --- /dev/null +++ b/framework/spree/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/spree/cart/use-add-item.tsx b/framework/spree/cart/use-add-item.tsx new file mode 100644 index 000000000..7f3d1061f --- /dev/null +++ b/framework/spree/cart/use-add-item.tsx @@ -0,0 +1,17 @@ +import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item' +import { MutationHook } from '@commerce/utils/types' + +export default useAddItem as UseAddItem +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: + ({ fetch }) => + () => { + return async function addItem() { + return {} + } + }, +} diff --git a/framework/spree/cart/use-cart.tsx b/framework/spree/cart/use-cart.tsx new file mode 100644 index 000000000..b3e509a21 --- /dev/null +++ b/framework/spree/cart/use-cart.tsx @@ -0,0 +1,42 @@ +import { useMemo } from 'react' +import { SWRHook } from '@commerce/utils/types' +import useCart, { UseCart } from '@commerce/cart/use-cart' + +export default useCart as UseCart + +export const handler: SWRHook = { + fetchOptions: { + query: '', + }, + async fetcher() { + return { + id: '', + createdAt: '', + currency: { code: '' }, + taxesIncluded: '', + lineItems: [], + lineItemsSubtotalPrice: '', + subtotalPrice: 0, + totalPrice: 0, + } + }, + useHook: + ({ useData }) => + (input) => { + return useMemo( + () => + Object.create( + {}, + { + isEmpty: { + get() { + return true + }, + enumerable: true, + }, + } + ), + [] + ) + }, +} diff --git a/framework/spree/cart/use-remove-item.tsx b/framework/spree/cart/use-remove-item.tsx new file mode 100644 index 000000000..b4ed583b8 --- /dev/null +++ b/framework/spree/cart/use-remove-item.tsx @@ -0,0 +1,18 @@ +import { MutationHook } from '@commerce/utils/types' +import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item' + +export default useRemoveItem as UseRemoveItem + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: + ({ fetch }) => + () => { + return async function removeItem(input) { + return {} + } + }, +} diff --git a/framework/spree/cart/use-update-item.tsx b/framework/spree/cart/use-update-item.tsx new file mode 100644 index 000000000..06d703f70 --- /dev/null +++ b/framework/spree/cart/use-update-item.tsx @@ -0,0 +1,18 @@ +import { MutationHook } from '@commerce/utils/types' +import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item' + +export default useUpdateItem as UseUpdateItem + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: + ({ fetch }) => + () => { + return async function addItem() { + return {} + } + }, +} diff --git a/framework/spree/createFetcher.ts b/framework/spree/createFetcher.ts deleted file mode 100644 index fafce34ee..000000000 --- a/framework/spree/createFetcher.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { Fetcher } from '@commerce/utils/types' -import convertSpreeErrorToGraphQlError from './utils/convertSpreeErrorToGraphQlError' -import { makeClient } from '@spree/storefront-api-v2-sdk' -import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse' -import type { - JsonApiDocument, - JsonApiListResponse, -} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' -import { errors } from '@spree/storefront-api-v2-sdk' -// import { API_TOKEN, API_URL } from './const' -// import { handleFetchResponse } from './utils' - -const createFetcher = (fetcherOptions: any): Fetcher => { - const { host } = fetcherOptions - const client = makeClient({ host }) - - //TODO: Add types to fetcherOptions - return async (requestOptions) => { - console.log('FETCHER') - // url?: string - // query?: string - // method?: string - // variables?: any - // body?: Body - const { url, method, variables, query } = requestOptions - const { locale, ...vars } = variables ?? {} - - if (!url) { - // TODO: Create a custom type for this error. - throw new Error('Url not provider for fetcher.') - } - - console.log( - `Fetching products using options: ${JSON.stringify(requestOptions)}.` - ) - - // const storeResponse = await fetch(url, { - // method, - // body: JSON.stringify({ query, variables: vars }), - // headers: { - // 'X-Shopify-Storefront-Access-Token': API_TOKEN, - // 'Content-Type': 'application/json', TODO: Probably not needed. Check! - // }, - // }) - - // const storeResponse.json() - - // if (storeResponse.ok) { - // return - // } - - // TODO: Not best to use url for finding the method, but should be good enough for now. - - const clientEndpointMethod = url - .split('.') - .reduce((clientNode: any, pathPart) => { - // TODO: Fix clientNode type - return clientNode[pathPart] - }, client) - - const storeResponse: ResultResponse = - await clientEndpointMethod(...variables.args) // TODO: Not the best to use variables here as it's type is any. - - if (storeResponse.success()) { - return storeResponse.success() - } - - if (storeResponse.fail() instanceof errors.SpreeError) { - throw convertSpreeErrorToGraphQlError(storeResponse.fail()) - } - - throw storeResponse.fail() - } -} - -// import { Fetcher } from '@commerce/utils/types' - -// export const fetcher: Fetcher = async () => { -// console.log('FETCHER') -// const res = await fetch('./data.json') -// if (res.ok) { -// const { data } = await res.json() -// return data -// } -// throw res -// } - -export default createFetcher diff --git a/framework/spree/createProvider.ts b/framework/spree/createProvider.ts deleted file mode 100644 index d8756bd73..000000000 --- a/framework/spree/createProvider.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Provider } from '@commerce' -import { Config } from '.' -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 saleorProvider = { -// locale: 'en-us', -// cartCookie: '', -// cartCookieToken: '', -// fetcher, -// cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, -// customer: { useCustomer }, -// products: { useSearch }, -// auth: { useLogin, useLogout, useSignup }, -// } - -export const createProvider = (options: { config: Config }): Provider => { - const { config } = options - - return { - locale: '', // Not an optional key in TypeScript, but already set in config. So, just make it an empty string. - cartCookie: '', // Not an optional key in TypeScript, but already set in config. So, just make it an empty string. - fetcher: createFetcher({ host: config.store.host }), - // FIXME: Add dummy hooks for below based on framework/local EXCEPT use-product - cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, - customer: { useCustomer }, - products: { useSearch }, - auth: { useLogin, useLogout, useSignup }, - } -} - -export type { Provider } diff --git a/framework/spree/customer/index.ts b/framework/spree/customer/index.ts new file mode 100644 index 000000000..6c903ecc5 --- /dev/null +++ b/framework/spree/customer/index.ts @@ -0,0 +1 @@ +export { default as useCustomer } from './use-customer' diff --git a/framework/spree/customer/use-customer.tsx b/framework/spree/customer/use-customer.tsx new file mode 100644 index 000000000..41757cd0d --- /dev/null +++ b/framework/spree/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 +export const handler: SWRHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: () => () => { + return async function addItem() { + return {} + } + }, +} diff --git a/framework/spree/errors/MisconfigurationError.ts b/framework/spree/errors/MisconfigurationError.ts new file mode 100644 index 000000000..0717ae404 --- /dev/null +++ b/framework/spree/errors/MisconfigurationError.ts @@ -0,0 +1 @@ +export default class MisconfigurationError extends Error {} diff --git a/framework/spree/errors/MissingConfigurationValueError.ts b/framework/spree/errors/MissingConfigurationValueError.ts new file mode 100644 index 000000000..02b497bf1 --- /dev/null +++ b/framework/spree/errors/MissingConfigurationValueError.ts @@ -0,0 +1 @@ +export default class MissingConfigurationValueError extends Error {} diff --git a/framework/spree/fetcher.ts b/framework/spree/fetcher.ts new file mode 100644 index 000000000..36e79c7a2 --- /dev/null +++ b/framework/spree/fetcher.ts @@ -0,0 +1,86 @@ +import type { Fetcher } from '@commerce/utils/types' +import convertSpreeErrorToGraphQlError from './utils/convertSpreeErrorToGraphQlError' +import { makeClient } from '@spree/storefront-api-v2-sdk' +import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse' +import type { + JsonApiDocument, + JsonApiListResponse, +} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' +import { errors } from '@spree/storefront-api-v2-sdk' +import { requireConfigValue } from './isomorphicConfig' +// import { handleFetchResponse } from './utils' + +const client = makeClient({ host: requireConfigValue('spreeApiHost') }) + +const fetcher: Fetcher = async (requestOptions) => { + console.log('Fetcher called') + // url?: string + // query?: string + // method?: string + // variables?: any + // body?: Body + const { url, method, variables, query } = requestOptions + const { locale, ...vars } = variables ?? {} + + if (!url) { + // TODO: Create a custom type for this error. + throw new Error('Url not provider for fetcher.') + } + + console.log( + `Fetching products using options: ${JSON.stringify(requestOptions)}.` + ) + + // const storeResponse = await fetch(url, { + // method, + // body: JSON.stringify({ query, variables: vars }), + // headers: { + // 'X-Shopify-Storefront-Access-Token': API_TOKEN, + // 'Content-Type': 'application/json', TODO: Probably not needed. Check! + // }, + // }) + + // const storeResponse.json() + + // if (storeResponse.ok) { + // return + // } + + // TODO: Not best to use url for finding the method, but should be good enough for now. + + const clientEndpointMethod = url + .split('.') + .reduce((clientNode: any, pathPart) => { + // TODO: Fix clientNode type + return clientNode[pathPart] + }, client) + + const storeResponse: ResultResponse = + await clientEndpointMethod(...variables.args) // TODO: Not the best to use variables here as it's type is any. + + if (storeResponse.success()) { + return storeResponse.success() + } + + const storeResponseError = storeResponse.fail() + + if (storeResponseError instanceof errors.SpreeError) { + throw convertSpreeErrorToGraphQlError(storeResponseError) + } + + throw storeResponseError +} + +// import { Fetcher } from '@commerce/utils/types' + +// export const fetcher: Fetcher = async () => { +// console.log('FETCHER') +// const res = await fetch('./data.json') +// if (res.ok) { +// const { data } = await res.json() +// return data +// } +// throw res +// } + +export default fetcher diff --git a/framework/spree/index.tsx b/framework/spree/index.tsx index db8865117..afc1f91eb 100644 --- a/framework/spree/index.tsx +++ b/framework/spree/index.tsx @@ -7,37 +7,29 @@ import { useCommerce as useCoreCommerce, } from '@commerce' -// import { provider, Provider } from './provider' -import { createProvider, Provider } from './createProvider' - -// export { provider } - -// TODO: Below is probably not needed. Expect default values to be set by NextJS Commerce and be ok for now. -// export const saleorConfig: CommerceConfig = { -// locale: 'en-us', -// cartCookie: Const.CHECKOUT_ID_COOKIE, -// } - -export type Config = { - store: { - host: string - } -} & CommerceConfig // This is the type that holds any custom values specifically for the Spree Framework. +import { provider } from './provider' +import type { Provider } from './provider' +import { requireConfigValue } from './isomorphicConfig' export type SpreeProps = { children: ReactNode provider: Provider - config: Config -} & Config + config: SpreeConfig +} & SpreeConfig -export function CommerceProvider({ children, ...config }: SpreeProps) { - console.log('CommerceProvider called') +export const spreeCommerceConfigDefaults: CommerceConfig = { + locale: requireConfigValue('defaultLocale'), + cartCookie: requireConfigValue('cartCookieName'), +} - // TODO: Make sure this doesn't get called all the time. If it does, useMemo. - const provider = createProvider({ config }) +export type SpreeConfig = CommerceConfig +export function CommerceProvider({ children, ...restProps }: SpreeProps) { return ( - + {children} ) diff --git a/framework/spree/isomorphicConfig.ts b/framework/spree/isomorphicConfig.ts new file mode 100644 index 000000000..4d5938b78 --- /dev/null +++ b/framework/spree/isomorphicConfig.ts @@ -0,0 +1,21 @@ +import forceIsomorphicConfigValues from './utils/forceIsomorphicConfigValues' +import requireConfig from './utils/requireConfig' + +const isomorphicConfig = { + spreeApiHost: process.env.NEXT_PUBLIC_SPREE_API_HOST, + defaultLocale: process.env.NEXT_PUBLIC_SPREE_DEFAULT_LOCALE, + cartCookieName: process.env.NEXT_PUBLIC_SPREE_CART_COOKIE_NAME, +} + +export default forceIsomorphicConfigValues( + isomorphicConfig, + ['defaultLocale', 'cartCookieName'], + ['spreeApiHost'] +) + +type IsomorphicConfig = typeof isomorphicConfig + +const requireConfigValue = (key: keyof IsomorphicConfig) => + requireConfig(isomorphicConfig, key) + +export { requireConfigValue } diff --git a/framework/spree/next.config.js b/framework/spree/next.config.js index 0bef26aba..11b9ef289 100644 --- a/framework/spree/next.config.js +++ b/framework/spree/next.config.js @@ -2,12 +2,7 @@ const commerce = require('./commerce.config.json') module.exports = { commerce, - store: { - host: process.env.SPREE_API_HOST, - }, // images: { // domains: [process.env.COMMERCE_IMAGE_HOST], // }, - // locale: 'en-us', - // cartCookie: Const.CHECKOUT_ID_COOKIE, } diff --git a/framework/spree/product/index.ts b/framework/spree/product/index.ts new file mode 100644 index 000000000..426a3edcd --- /dev/null +++ b/framework/spree/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/spree/product/use-price.tsx b/framework/spree/product/use-price.tsx new file mode 100644 index 000000000..0174faf5e --- /dev/null +++ b/framework/spree/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/spree/provider.ts b/framework/spree/provider.ts new file mode 100644 index 000000000..3a96f456d --- /dev/null +++ b/framework/spree/provider.ts @@ -0,0 +1,28 @@ +import type { Provider } from '@commerce' +import fetcher from './fetcher' + +// TODO: Using dummy hooks to fetch static content. Based on the local framework. +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' + +const provider = { + locale: '', // Not an optional key in TypeScript, but already set in config. So, just make it an empty string. + cartCookie: '', // Not an optional key in TypeScript, but already set in config. So, just make it an empty string. + fetcher, + // FIXME: Add dummy hooks for below based on framework/local EXCEPT use-product + cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, + customer: { useCustomer }, + products: { useSearch }, + auth: { useLogin, useLogout, useSignup }, +} + +export { provider } + +export type { Provider } diff --git a/framework/spree/types/index.ts b/framework/spree/types/index.ts new file mode 100644 index 000000000..5ddeee8e5 --- /dev/null +++ b/framework/spree/types/index.ts @@ -0,0 +1,5 @@ +export type UnknownObjectValues = Record + +export type NonUndefined = T extends undefined ? never : T + +export type ValueOf = T[keyof T] diff --git a/framework/spree/utils/forceIsomorphicConfigValues.ts b/framework/spree/utils/forceIsomorphicConfigValues.ts new file mode 100644 index 000000000..94c38c2af --- /dev/null +++ b/framework/spree/utils/forceIsomorphicConfigValues.ts @@ -0,0 +1,43 @@ +import type { NonUndefined, UnknownObjectValues } from '../types' +import MisconfigurationError from '../errors/MisconfigurationError' +import isServer from './isServer' + +const generateMisconfigurationErrorMessage = ( + keys: Array +) => `${keys.join(', ')} must have values before running the Framework.` + +const forceIsomorphicConfigValues = < + X extends keyof T, + T extends UnknownObjectValues, + H extends Record> +>( + config: T, + requiredServerKeys: string[], + requiredPublicKeys: X[] +) => { + if (isServer) { + const missingServerConfigValues = requiredServerKeys.filter( + (requiredServerKey) => typeof config[requiredServerKey] === 'undefined' + ) + + if (missingServerConfigValues.length > 0) { + throw new MisconfigurationError( + generateMisconfigurationErrorMessage(missingServerConfigValues) + ) + } + } + + const missingPublicConfigValues = requiredPublicKeys.filter( + (requiredPublicKey) => typeof config[requiredPublicKey] === 'undefined' + ) + + if (missingPublicConfigValues.length > 0) { + throw new MisconfigurationError( + generateMisconfigurationErrorMessage(missingPublicConfigValues) + ) + } + + return config as T & H +} + +export default forceIsomorphicConfigValues diff --git a/framework/spree/utils/isServer.ts b/framework/spree/utils/isServer.ts new file mode 100644 index 000000000..4544a4884 --- /dev/null +++ b/framework/spree/utils/isServer.ts @@ -0,0 +1 @@ +export default typeof window === 'undefined' diff --git a/framework/spree/utils/requireConfig.ts b/framework/spree/utils/requireConfig.ts new file mode 100644 index 000000000..92b7916ca --- /dev/null +++ b/framework/spree/utils/requireConfig.ts @@ -0,0 +1,16 @@ +import MissingConfigurationValueError from '../errors/MissingConfigurationValueError' +import type { NonUndefined, ValueOf } from '../types' + +const requireConfig = (isomorphicConfig: T, key: keyof T) => { + const valueUnderKey = isomorphicConfig[key] + + if (typeof valueUnderKey === 'undefined') { + throw new MissingConfigurationValueError( + `Value for configuration key ${key} was undefined.` + ) + } + + return valueUnderKey as NonUndefined> +} + +export default requireConfig diff --git a/framework/spree/wishlist/use-add-item.tsx b/framework/spree/wishlist/use-add-item.tsx new file mode 100644 index 000000000..75f067c3a --- /dev/null +++ b/framework/spree/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/spree/wishlist/use-remove-item.tsx b/framework/spree/wishlist/use-remove-item.tsx new file mode 100644 index 000000000..a2d3a8a05 --- /dev/null +++ b/framework/spree/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/spree/wishlist/use-wishlist.tsx b/framework/spree/wishlist/use-wishlist.tsx new file mode 100644 index 000000000..9fe0e758f --- /dev/null +++ b/framework/spree/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 = () => { + 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/package.json b/package.json index 6451806ec..832b9c8fd 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "nextjs-commerce", "version": "1.0.0", "scripts": { - "dev": "NODE_OPTIONS='--inspect' next dev", + "dev": "NODE_OPTIONS='--inspect' next dev -p 4000", "build": "next build", - "start": "next start", + "start": "next start -p 4000", "analyze": "BUNDLE_ANALYZE=both yarn build", "prettier-fix": "prettier --write .", "find:unused": "npx next-unused", diff --git a/tsconfig.json b/tsconfig.json index 340929669..b06e78b37 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,8 +23,8 @@ "@components/*": ["components/*"], "@commerce": ["framework/commerce"], "@commerce/*": ["framework/commerce/*"], - "@framework": ["framework/local"], - "@framework/*": ["framework/local/*"] + "@framework": ["framework/spree"], + "@framework/*": ["framework/spree/*"] } }, "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],