Add Spree as allowed Framework

This commit is contained in:
tniezg 2021-07-23 17:01:43 +02:00
parent a3ef27f5e7
commit c90fa7abf2
53 changed files with 912 additions and 166 deletions

View File

@ -14,6 +14,7 @@ const PROVIDERS = [
'swell',
'vendure',
'local',
'spree',
]
function getProviderName() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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)

View File

@ -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<SpreeApiProvider>) {
async function getAllPages<T extends GetAllPagesOperation>(options?: {
config?: Partial<SpreeApiConfig>
preview?: boolean
}): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>(
opts: {
config?: Partial<SpreeApiConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>({
config,
preview,
query,
}: {
url?: string
config?: Partial<SpreeApiConfig>
preview?: boolean
query?: string
} = {}): Promise<T['data']> {
return {
pages: [],
}
}
return getAllPages
}

View File

@ -0,0 +1,17 @@
// import data from '../../data.json'
export type GetAllProductPathsResult = {
products: Array<{ path: string }>
}
export default function getAllProductPathsOperation() {
function getAllProductPaths(): Promise<GetAllProductPathsResult> {
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
}

View File

@ -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<SpreeApiProvider>) {
async function getAllProducts<T extends GetAllProductsOperation>({
query = 'products.list',
variables = { first: 10 },
config: userConfig,
}: {
query?: string
variables?: T['variables']
config?: Partial<LocalConfig>
} = {}): 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<IProducts>(
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
}

View File

@ -0,0 +1,6 @@
export default function getCustomerWishlistOperation() {
function getCustomerWishlist(): any {
return { wishlist: {} }
}
return getCustomerWishlist
}

View File

@ -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<GetPageResult> {
return Promise.resolve({})
}
return getPage
}

View File

@ -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<any>) {
async function getProduct<T extends GetProductOperation>({
query = '',
variables,
config,
}: {
query?: string
variables?: T['variables']
config?: Partial<LocalConfig>
preview?: boolean
} = {}): Promise<Product | {} | any> {
return {
product: data.products.find(({ slug }) => slug === variables!.slug),
// TODO: Return Spree product.
// product: {},
}
}
return getProduct
}

View File

@ -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<any>) {
// TODO: Get Spree categories for display in React components.
function getSiteInfo({
query,
variables,
config: cfg,
}: {
query?: string
variables?: any
config?: Partial<LocalConfig>
preview?: boolean
} = {}): Promise<GetSiteInfoResult> {
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
}

View File

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

View File

@ -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<JsonApiDocument | JsonApiListResponse>
> = (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<JsonApiDocument | JsonApiListResponse> =
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<Data = any, Variables = any>(
// query: string,
// queryData?: CommerceAPIFetchOptions<Variables>,
// fetchOptions?: RequestInit
// ): Promise<GraphQLFetcherResult<Data>>
// 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

View File

@ -0,0 +1,3 @@
import zeitFetch from '@vercel/fetch'
export default zeitFetch()

View File

@ -0,0 +1,3 @@
export { default as useLogin } from './use-login'
export { default as useLogout } from './use-logout'
export { default as useSignup } from './use-signup'

View File

@ -0,0 +1,16 @@
import { MutationHook } from '@commerce/utils/types'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook: () => () => {
return async function () {}
},
}

View File

@ -0,0 +1,17 @@
import { MutationHook } from '@commerce/utils/types'
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook:
({ fetch }) =>
() =>
async () => {},
}

View File

@ -0,0 +1,19 @@
import { useCallback } from 'react'
import useCustomer from '../customer/use-customer'
import { MutationHook } from '@commerce/utils/types'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook:
({ fetch }) =>
() =>
() => {},
}

View File

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

View File

@ -0,0 +1,17 @@
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import { MutationHook } from '@commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() => {
return async function addItem() {
return {}
}
},
}

View File

@ -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<typeof handler>
export const handler: SWRHook<any> = {
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,
},
}
),
[]
)
},
}

View File

@ -0,0 +1,18 @@
import { MutationHook } from '@commerce/utils/types'
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() => {
return async function removeItem(input) {
return {}
}
},
}

View File

@ -0,0 +1,18 @@
import { MutationHook } from '@commerce/utils/types'
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() => {
return async function addItem() {
return {}
}
},
}

View File

@ -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<JsonApiDocument | JsonApiListResponse> =
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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
import { SWRHook } from '@commerce/utils/types'
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook: () => () => {
return async function addItem() {
return {}
}
},
}

View File

@ -0,0 +1 @@
export default class MisconfigurationError extends Error {}

View File

@ -0,0 +1 @@
export default class MissingConfigurationValueError extends Error {}

View File

@ -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<JsonApiDocument | JsonApiListResponse> =
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

View File

@ -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 (
<CoreCommerceProvider provider={provider} config={config}>
<CoreCommerceProvider
provider={provider}
config={{ ...spreeCommerceConfigDefaults, ...restProps }}
>
{children}
</CoreCommerceProvider>
)

View File

@ -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>(isomorphicConfig, key)
export { requireConfigValue }

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
export type UnknownObjectValues = Record<string, unknown>
export type NonUndefined<T> = T extends undefined ? never : T
export type ValueOf<T> = T[keyof T]

View File

@ -0,0 +1,43 @@
import type { NonUndefined, UnknownObjectValues } from '../types'
import MisconfigurationError from '../errors/MisconfigurationError'
import isServer from './isServer'
const generateMisconfigurationErrorMessage = (
keys: Array<string | number | symbol>
) => `${keys.join(', ')} must have values before running the Framework.`
const forceIsomorphicConfigValues = <
X extends keyof T,
T extends UnknownObjectValues,
H extends Record<X, NonUndefined<T[X]>>
>(
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

View File

@ -0,0 +1 @@
export default typeof window === 'undefined'

View File

@ -0,0 +1,16 @@
import MissingConfigurationValueError from '../errors/MissingConfigurationValueError'
import type { NonUndefined, ValueOf } from '../types'
const requireConfig = <T>(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<ValueOf<T>>
}
export default requireConfig

View File

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

View File

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

View File

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

View File

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

View File

@ -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"],