diff --git a/framework/spree/.env.template b/framework/spree/.env.template index 82a00d7be..f6e557e57 100644 --- a/framework/spree/.env.template +++ b/framework/spree/.env.template @@ -10,3 +10,4 @@ NEXT_PUBLIC_SPREE_IMAGE_HOST=http://localhost:3000 NEXT_PUBLIC_SPREE_ALLOWED_IMAGE_DOMAIN=localhost NEXT_PUBLIC_SPREE_CATEGORIES_TAXONOMY_ID=1 NEXT_PUBLIC_SPREE_BRANDS_TAXONOMY_ID=27 +NEXT_PUBLIC_SHOW_SINGLE_VARIANT_OPTIONS=false diff --git a/framework/spree/api/operations/get-all-products.ts b/framework/spree/api/operations/get-all-products.ts index 3572ee2d2..6ffb704b0 100644 --- a/framework/spree/api/operations/get-all-products.ts +++ b/framework/spree/api/operations/get-all-products.ts @@ -55,10 +55,9 @@ export default function getAllProductsOperation({ const config = commerce.getConfig(userConfig) const { fetch: apiFetch } = config // TODO: Send config.locale to Spree. - const { data: spreeSuccessResponse } = await apiFetch( - '__UNUSED__', - { variables } - ) + const { + data: { data: spreeSuccessResponse }, + } = await apiFetch<{ data: IProducts }>('__UNUSED__', { variables }) const normalizedProducts: Product[] = spreeSuccessResponse.data.map( (spreeProduct) => normalizeProduct(spreeSuccessResponse, spreeProduct) diff --git a/framework/spree/api/operations/get-product.ts b/framework/spree/api/operations/get-product.ts index 87c0ff284..42bfbb7fb 100644 --- a/framework/spree/api/operations/get-product.ts +++ b/framework/spree/api/operations/get-product.ts @@ -61,10 +61,9 @@ export default function getProductOperation({ const config = commerce.getConfig(userConfig) const { fetch: apiFetch } = config // TODO: Send config.locale to Spree. - const { data: spreeSuccessResponse } = await apiFetch( - '__UNUSED__', - { variables } - ) + const { + data: { data: spreeSuccessResponse }, + } = await apiFetch<{ data: IProduct }>('__UNUSED__', { variables }) return { product: normalizeProduct( diff --git a/framework/spree/api/operations/get-site-info.ts b/framework/spree/api/operations/get-site-info.ts index 7538827fe..ab3d395cf 100644 --- a/framework/spree/api/operations/get-site-info.ts +++ b/framework/spree/api/operations/get-site-info.ts @@ -84,21 +84,25 @@ export default function getSiteInfoOperation({ const config = commerce.getConfig(userConfig) const { fetch: apiFetch } = config // TODO: Send config.locale to Spree. - const { data: spreeCategoriesSuccessResponse } = await apiFetch( - '__UNUSED__', - { - variables: createVariables( - requireConfigValue('spreeCategoriesTaxonomyId') - ), - } - ) + const { + data: { data: spreeCategoriesSuccessResponse }, + } = await apiFetch<{ + data: ITaxons + }>('__UNUSED__', { + variables: createVariables( + requireConfigValue('spreeCategoriesTaxonomyId') as string + ), + }) - const { data: spreeBrandsSuccessResponse } = await apiFetch( - '__UNUSED__', - { - variables: createVariables(requireConfigValue('spreeBrandsTaxonomyId')), - } - ) + const { + data: { data: spreeBrandsSuccessResponse }, + } = await apiFetch<{ + data: ITaxons + }>('__UNUSED__', { + variables: createVariables( + requireConfigValue('spreeBrandsTaxonomyId') as string + ), + }) const normalizedCategories: GetSiteInfoOperation['data']['categories'] = spreeCategoriesSuccessResponse.data.sort(taxonsSort).map((spreeTaxon) => { diff --git a/framework/spree/api/utils/create-api-fetch.ts b/framework/spree/api/utils/create-api-fetch.ts index e6c1f30e8..be7e2a6d6 100644 --- a/framework/spree/api/utils/create-api-fetch.ts +++ b/framework/spree/api/utils/create-api-fetch.ts @@ -1,4 +1,3 @@ -import { GraphQLFetcher, GraphQLFetcherResult } from '@commerce/api' import { SpreeApiConfig } from '..' import { errors, makeClient } from '@spree/storefront-api-v2-sdk' import { requireConfigValue } from 'framework/spree/isomorphicConfig' @@ -6,18 +5,25 @@ import convertSpreeErrorToGraphQlError from 'framework/spree/utils/convertSpreeE import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse' import type { JsonApiListResponse, - JsonApiResponse, + JsonApiSingleResponse, } from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' import getSpreeSdkMethodFromEndpointPath from 'framework/spree/utils/getSpreeSdkMethodFromEndpointPath' import { SpreeSdkVariables } from 'framework/spree/types' import SpreeSdkMethodFromEndpointPathError from 'framework/spree/errors/SpreeSdkMethodFromEndpointPathError' +import { GraphQLFetcher, GraphQLFetcherResult } from '@commerce/api' +import createCreateFetchFetcher from '../../utils/createCreateFetchFetcher' +import createVercelFetch from '@vercel/fetch' const createApiFetch: ( getConfig: () => SpreeApiConfig ) => GraphQLFetcher, SpreeSdkVariables> = ( - getConfig + _getConfig ) => { - const client = makeClient({ host: requireConfigValue('spreeApiHost') }) + const client = makeClient({ + host: requireConfigValue('spreeApiHost') as string, + fetcherType: 'custom', + createFetcher: createCreateFetchFetcher({ fetch: createVercelFetch() }), + }) return async (url, queryData = {}, fetchOptions = {}) => { console.log( @@ -38,22 +44,23 @@ const createApiFetch: ( ) } - const storeResponse: ResultResponse = - await getSpreeSdkMethodFromEndpointPath( - client, - variables.methodPath - )(...variables.arguments) + const storeResponse: ResultResponse< + JsonApiSingleResponse | JsonApiListResponse + > = await getSpreeSdkMethodFromEndpointPath( + client, + variables.methodPath + )(...variables.arguments) + + if (storeResponse.isSuccess()) { + const data = storeResponse.success() + const rawFetchRespone = Object.getPrototypeOf(data).response - if (storeResponse.success()) { return { - data: storeResponse.success(), - res: storeResponse as any, //FIXME: MUST BE fetch() RESPONSE instead of axios. + data, + res: rawFetchRespone, } } - // FIXME: Allow Spree SDK to use fetch instead of axios - // (https://github.com/spree/spree-storefront-api-v2-js-sdk/issues/189) - const storeResponseError = storeResponse.fail() if (storeResponseError instanceof errors.SpreeError) { diff --git a/framework/spree/fetcher.ts b/framework/spree/fetcher.ts index 882a6c2bf..2916d4050 100644 --- a/framework/spree/fetcher.ts +++ b/framework/spree/fetcher.ts @@ -4,20 +4,26 @@ import { makeClient } from '@spree/storefront-api-v2-sdk' import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse' import type { JsonApiListResponse, - JsonApiResponse, + JsonApiSingleResponse, } from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' import { errors } from '@spree/storefront-api-v2-sdk' import { requireConfigValue } from './isomorphicConfig' import getSpreeSdkMethodFromEndpointPath from './utils/getSpreeSdkMethodFromEndpointPath' import SpreeSdkMethodFromEndpointPathError from './errors/SpreeSdkMethodFromEndpointPathError' import type { SpreeSdkVariables } from './types' -import { GraphQLFetcherResult } from '@commerce/api' +import type { GraphQLFetcherResult } from '@commerce/api' +import createCreateFetchFetcher from './utils/createCreateFetchFetcher' -const client = makeClient({ host: requireConfigValue('spreeApiHost') }) +const client = makeClient({ + host: requireConfigValue('spreeApiHost') as string, + fetcherType: 'custom', + createFetcher: createCreateFetchFetcher({ fetch: globalThis.fetch }), +}) -const fetcher: Fetcher, SpreeSdkVariables> = async ( - requestOptions -) => { +const fetcher: Fetcher< + GraphQLFetcherResult, + SpreeSdkVariables +> = async (requestOptions) => { // url?: string // query?: string // method?: string @@ -40,22 +46,23 @@ const fetcher: Fetcher, SpreeSdkVariables> = async ( ) } - const storeResponse: ResultResponse = - await getSpreeSdkMethodFromEndpointPath( - client, - variables.methodPath - )(...variables.arguments) + const storeResponse: ResultResponse< + JsonApiSingleResponse | JsonApiListResponse + > = await getSpreeSdkMethodFromEndpointPath( + client, + variables.methodPath + )(...variables.arguments) + + if (storeResponse.isSuccess()) { + const data = storeResponse.success() + const rawFetchRespone = Object.getPrototypeOf(data).response - if (storeResponse.success()) { return { - data: storeResponse.success(), - res: storeResponse as any, //FIXME: MUST BE fetch() RESPONSE instead of axios. + data, + res: rawFetchRespone, } } - // FIXME: Allow Spree SDK to use fetch instead of axios - // (https://github.com/spree/spree-storefront-api-v2-js-sdk/issues/189) - const storeResponseError = storeResponse.fail() if (storeResponseError instanceof errors.SpreeError) { @@ -65,14 +72,4 @@ const fetcher: Fetcher, SpreeSdkVariables> = async ( throw storeResponseError } -// 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 afc1f91eb..03f93cc1c 100644 --- a/framework/spree/index.tsx +++ b/framework/spree/index.tsx @@ -18,8 +18,8 @@ export type SpreeProps = { } & SpreeConfig export const spreeCommerceConfigDefaults: CommerceConfig = { - locale: requireConfigValue('defaultLocale'), - cartCookie: requireConfigValue('cartCookieName'), + locale: requireConfigValue('defaultLocale') as string, + cartCookie: requireConfigValue('cartCookieName') as string, } export type SpreeConfig = CommerceConfig diff --git a/framework/spree/isomorphicConfig.ts b/framework/spree/isomorphicConfig.ts index e472fdefe..7c5245f99 100644 --- a/framework/spree/isomorphicConfig.ts +++ b/framework/spree/isomorphicConfig.ts @@ -9,6 +9,8 @@ const isomorphicConfig = { spreeCategoriesTaxonomyId: process.env.NEXT_PUBLIC_SPREE_CATEGORIES_TAXONOMY_ID, spreeBrandsTaxonomyId: process.env.NEXT_PUBLIC_SPREE_BRANDS_TAXONOMY_ID, + showSingleVariantOptions: + process.env.NEXT_PUBLIC_SHOW_SINGLE_VARIANT_OPTIONS === 'true', } export default forceIsomorphicConfigValues( @@ -21,6 +23,7 @@ export default forceIsomorphicConfigValues( 'spreeImageHost', 'spreeCategoriesTaxonomyId', 'spreeBrandsTaxonomyId', + 'showSingleVariantOptions', ] ) diff --git a/framework/spree/product/use-search.tsx b/framework/spree/product/use-search.tsx index f9beac38c..efeedc2ab 100644 --- a/framework/spree/product/use-search.tsx +++ b/framework/spree/product/use-search.tsx @@ -40,9 +40,9 @@ export const handler: SWRHook = { const sort = input.sort ? { sort: nextToSpreeSortMap[input.sort] } : {} - const { data: spreeSuccessResponse } = await fetch< - GraphQLFetcherResult - >({ + const { + data: { data: spreeSuccessResponse }, + } = await fetch>({ variables: { methodPath: 'products.list', arguments: [ diff --git a/framework/spree/utils/createCreateFetchFetcher.ts b/framework/spree/utils/createCreateFetchFetcher.ts new file mode 100644 index 000000000..8c6e9e0cb --- /dev/null +++ b/framework/spree/utils/createCreateFetchFetcher.ts @@ -0,0 +1,72 @@ +import * as qs from 'qs' +import { errors } from '@spree/storefront-api-v2-sdk' +import type { CreateFetcher } from '@spree/storefront-api-v2-sdk/types/interfaces/ClientConfig' +import { Request } from 'node-fetch' + +// TODO: Fix rawFetch any type. +const createCreateFetchFetcher = + ({ fetch: rawFetch }): CreateFetcher => + (fetcherOptions) => { + const { FetchError } = errors + const sharedHeaders = { + 'Content-Type': 'application/json', + } + + return { + fetch: async (fetchOptions) => { + // This fetcher always returns request equal null, + // because @vercel/fetch doesn't accept a Request object as argument + // and it's not used by NJC anyway. + try { + const { url, params, method, headers } = fetchOptions + const absoluteUrl = new URL(url, fetcherOptions.host) + let payload + + switch (method.toUpperCase()) { + case 'PUT': + case 'POST': + case 'DELETE': + case 'PATCH': + payload = { body: JSON.stringify(params) } + break + default: + payload = null + absoluteUrl.search = qs.stringify(params, { + arrayFormat: 'brackets', + }) + } + + try { + const response = await rawFetch(absoluteUrl.toString(), { + method, + headers: { ...sharedHeaders, ...headers }, + ...payload, + }) + + const data = await response.json() + + if (!response.ok) { + // Use the "traditional" approach and reject non 2xx responses. + throw new FetchError(response, null, data) + } + + return { + // Add response key to the prototype so it can be passed inside the GraphQLFetcherResult type. + // TODO: Search for a better solution than adding response to the prototype. + data: Object.setPrototypeOf({ data }, { response }), + } + } catch (error) { + if (error instanceof TypeError) { + throw new FetchError(null, null, null) + } + + throw error + } + } catch (error) { + throw new FetchError(null, null, null, error.message) + } + }, + } + } + +export default createCreateFetchFetcher diff --git a/framework/spree/utils/normalizeProduct.ts b/framework/spree/utils/normalizeProduct.ts index a34ee25b3..f3ace745b 100644 --- a/framework/spree/utils/normalizeProduct.ts +++ b/framework/spree/utils/normalizeProduct.ts @@ -5,7 +5,7 @@ import type { } from '@commerce/types/product' import type { JsonApiListResponse, - JsonApiResponse, + JsonApiSingleResponse, } from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' import type { ProductAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Product' import type { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships' @@ -16,7 +16,7 @@ import getMediaGallery from './getMediaGallery' import { findIncludedOfType } from './jsonApi' const normalizeProduct = ( - spreeSuccessResponse: JsonApiResponse | JsonApiListResponse, + spreeSuccessResponse: JsonApiSingleResponse | JsonApiListResponse, spreeProduct: ProductAttr ) => { const spreeImageRecords = findIncludedOfType( @@ -27,7 +27,7 @@ const normalizeProduct = ( const images = getMediaGallery( spreeImageRecords, - createGetAbsoluteImageUrl(requireConfigValue('spreeImageHost')) + createGetAbsoluteImageUrl(requireConfigValue('spreeImageHost') as string) ) const price: ProductPrice = { @@ -41,6 +41,10 @@ const normalizeProduct = ( const hasNonMasterVariants = (spreeProduct.relationships.variants.data as RelationType[]).length > 1 + const showOptions = + (requireConfigValue('showSingleVariantOptions') as boolean) || + hasNonMasterVariants + let variants: ProductVariant[] let options: ProductOption[] = [] @@ -53,7 +57,7 @@ const normalizeProduct = ( variants = spreeVariantRecords.map((spreeVariantRecord) => { let variantOptions: ProductOption[] = [] - if (hasNonMasterVariants) { + if (showOptions) { const spreeOptionValues = findIncludedOfType( spreeSuccessResponse, spreeVariantRecord, diff --git a/package.json b/package.json index 832b9c8fd..14ae7a79b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@react-spring/web": "^9.2.1", "@spree/storefront-api-v2-sdk": "^4.5.4", + "@types/qs": "^6.9.7", "@vercel/fetch": "^6.1.0", "autoprefixer": "^10.2.6", "body-scroll-lock": "^3.1.5", @@ -38,6 +39,7 @@ "next-themes": "^0.0.14", "postcss": "^8.3.5", "postcss-nesting": "^8.0.1", + "qs": "^6.7.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-fast-marquee": "^1.1.4", diff --git a/yarn.lock b/yarn.lock index eb580b565..386645893 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1135,6 +1135,11 @@ resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/qs@^6.9.7": + version "6.9.7" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + "@types/react@^17.0.8": version "17.0.11" resolved "https://registry.npmjs.org/@types/react/-/react-17.0.11.tgz" @@ -4960,7 +4965,7 @@ purgecss@^4.0.3: postcss "^8.2.1" postcss-selector-parser "^6.0.2" -qs@6.7.0: +qs@6.7.0, qs@^6.7.0: version "6.7.0" resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==