diff --git a/.env.template b/.env.template index 1f0fe0536..4a3bfbff9 100644 --- a/.env.template +++ b/.env.template @@ -1,4 +1,4 @@ -# Available providers: bigcommerce, shopify, swell +# Available providers: bigcommerce, shopify, swell, local, saleor, vendure, ordercloud COMMERCE_PROVIDER= BIGCOMMERCE_STOREFRONT_API_URL= @@ -23,3 +23,6 @@ NEXT_PUBLIC_SALEOR_CHANNEL= NEXT_PUBLIC_VENDURE_SHOP_API_URL= NEXT_PUBLIC_VENDURE_LOCAL_URL= + +NEXT_PUBLIC_ORDERCLOUD_CLIENT_ID= +NEXT_PUBLIC_ORDERCLOUD_CLIENT_SECRET= diff --git a/framework/ordercloud/api/index.ts b/framework/ordercloud/api/index.ts index 55007eada..180984ad6 100644 --- a/framework/ordercloud/api/index.ts +++ b/framework/ordercloud/api/index.ts @@ -20,7 +20,7 @@ export interface OrdercloudConfig extends Omit { } const config: OrdercloudConfig = { - commerceUrl: 'https://sandboxapi.ordercloud.io/v1', + commerceUrl: 'https://sandboxapi.ordercloud.io', apiToken: '', cartCookie: '', customerCookie: '', diff --git a/framework/ordercloud/api/operations/get-all-products.ts b/framework/ordercloud/api/operations/get-all-products.ts index 71a3e4f2f..7dc6f71ff 100644 --- a/framework/ordercloud/api/operations/get-all-products.ts +++ b/framework/ordercloud/api/operations/get-all-products.ts @@ -1,12 +1,9 @@ -import { Product } from '@commerce/types/product' -import { GetAllProductsOperation } from '@commerce/types/product' +import type { Product } from '@commerce/types/product' +import type { GetAllProductsOperation } from '@commerce/types/product' import type { OperationContext } from '@commerce/api/operations' +import type { RawProduct } from '@framework/types/product' import type { OrdercloudConfig, Provider } from '../index' -import { - PriceSchedule, - RawProduct, - RawProductWithPrice, -} from '@framework/types/product' + import { normalize as normalizeProduct } from '@framework/utils/product' export default function getAllProductsOperation({ @@ -26,19 +23,11 @@ export default function getAllProductsOperation({ 'GET', '/products' ).then((response) => response.Items) - const rawProductsWithPrice: RawProductWithPrice[] = await Promise.all( - rawProducts.map(async (product) => ({ - ...product, - priceSchedule: await fetch( - 'GET', - `/priceschedules/${product.ID}` - ), - })) - ) return { - products: rawProductsWithPrice.map(normalizeProduct), + products: rawProducts.map(normalizeProduct), } } + return getAllProducts } diff --git a/framework/ordercloud/api/operations/get-product.ts b/framework/ordercloud/api/operations/get-product.ts index 3f1dfed21..0cfc57122 100644 --- a/framework/ordercloud/api/operations/get-product.ts +++ b/framework/ordercloud/api/operations/get-product.ts @@ -1,24 +1,32 @@ -import type { OrdercloudConfig } from '../index' -import { Product } from '@commerce/types/product' -import { GetProductOperation } from '@commerce/types/product' -import data from '../../data.json' import type { OperationContext } from '@commerce/api/operations' +import type { RawProduct } from '@framework/types/product' +import type { Product } from '@commerce/types/product' +import type { GetProductOperation } from '@commerce/types/product' +import type { OrdercloudConfig, Provider } from '../index' + +import { normalize as normalizeProduct } from '@framework/utils/product' export default function getProductOperation({ commerce, -}: OperationContext) { +}: OperationContext) { async function getProduct({ - query = '', - variables, config, + variables, }: { query?: string variables?: T['variables'] config?: Partial preview?: boolean - } = {}): Promise { + } = {}): Promise<{ product: Product }> { + const { fetch } = commerce.getConfig(config) + + const rawProduct: RawProduct = await fetch( + 'GET', + `/products/${variables?.slug}` + ) + return { - product: null // data.products.find(({ slug }) => slug === variables!.slug), + product: normalizeProduct(rawProduct), } } diff --git a/framework/ordercloud/api/utils/fetch-rest.ts b/framework/ordercloud/api/utils/fetch-rest.ts index e6d1403e6..b48805dee 100644 --- a/framework/ordercloud/api/utils/fetch-rest.ts +++ b/framework/ordercloud/api/utils/fetch-rest.ts @@ -18,26 +18,62 @@ const fetchRestApi: ( fetchOptions?: Record ) => { const { commerceUrl } = getConfig() - const res = await fetch(`${commerceUrl}${resource}`, { + // Check if we have a token stored + if (!global.token) { + // If not, get a new one and store it + const authResponse = await fetch(`${commerceUrl}/oauth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + body: `client_id=${process.env.NEXT_PUBLIC_ORDERCLOUD_CLIENT_ID}&grant_type=client_credentials&client_secret=${process.env.NEXT_PUBLIC_ORDERCLOUD_CLIENT_SECRET}`, + }) + + // If something failed getting the auth response + if (!authResponse.ok) { + // Get the body of it + const error = await authResponse.json() + + // And return an error + throw new FetcherError({ + errors: [{ message: error.error_description.Code }], + status: error.error_description.HttpStatus, + }) + } + + // If everything is fine, store the access token in global.token + global.token = await authResponse + .json() + .then((response) => response.access_token) + } + + // Do the request with the correct headers + const dataResponse = await fetch(`${commerceUrl}/v1${resource}`, { ...fetchOptions, method, headers: { ...fetchOptions?.headers, accept: 'application/json, text/plain, */*', - 'accept-language': 'es,en;q=0.9,es-ES;q=0.8,fr;q=0.7', - authorization: 'Bearer ', + authorization: `Bearer ${global.token}`, }, body: body ? JSON.stringify(body) : undefined, }) - if (!res.ok) { + // If something failed getting the data response + if (!dataResponse.ok) { + // Get the body of it + const error = await dataResponse.json() + + // And return an error throw new FetcherError({ - errors: [{ message: res.statusText }], - status: res.status, + errors: [{ message: error.error_description.Code }], + status: error.error_description.HttpStatus, }) } - return (await res.json()) as T + // Return the data and specify the expected type + return (await dataResponse.json()) as T } export default fetchRestApi diff --git a/framework/ordercloud/next.config.js b/framework/ordercloud/next.config.js index ce46b706f..6a9da4ef0 100644 --- a/framework/ordercloud/next.config.js +++ b/framework/ordercloud/next.config.js @@ -3,6 +3,15 @@ const commerce = require('./commerce.config.json') module.exports = { commerce, images: { - domains: ['localhost'], + domains: [ + 'localhost', + // TODO: Remove this + 'images.bloomingdalesassets.com', + 'pbs.twimg.com', + 'images.asos-media.com', + 'di2ponv0v5otw.cloudfront.net', + 'cdn.shopify.com', + 'encrypted-tbn0.gstatic.com', + ], }, } diff --git a/framework/ordercloud/types/node.d.ts b/framework/ordercloud/types/node.d.ts new file mode 100644 index 000000000..f4e4a21f4 --- /dev/null +++ b/framework/ordercloud/types/node.d.ts @@ -0,0 +1,5 @@ +declare module NodeJS { + interface Global { + token: string | null | undefined + } +} diff --git a/framework/ordercloud/types/product.ts b/framework/ordercloud/types/product.ts index 465ef5f53..21155f29b 100644 --- a/framework/ordercloud/types/product.ts +++ b/framework/ordercloud/types/product.ts @@ -17,28 +17,11 @@ export interface RawProduct { Inventory: null DefaultSupplierID: null AllSuppliersCanSell: boolean - xp: null -} - -export interface RawProductWithPrice extends RawProduct { - priceSchedule: PriceSchedule -} - -export interface PriceSchedule { - OwnerID: string - ID: string - Name: string - ApplyTax: boolean - ApplyShipping: boolean - MinQuantity: number - MaxQuantity: number - UseCumulativeQuantity: boolean - RestrictedQuantity: boolean - PriceBreaks: [ - { - Quantity: number - Price: number - } - ] - xp: null + xp: { + Price: number + PriceCurrency: string + Images: { + url: string + }[] + } } diff --git a/framework/ordercloud/utils/product.ts b/framework/ordercloud/utils/product.ts index 125e443d6..f316c31ff 100644 --- a/framework/ordercloud/utils/product.ts +++ b/framework/ordercloud/utils/product.ts @@ -1,17 +1,60 @@ -import type { RawProductWithPrice } from '@framework/types/product' +import type { RawProduct } from '@framework/types/product' import type { Product } from '@commerce/types/product' -export function normalize(product: RawProductWithPrice): Product { +export function normalize(product: RawProduct): Product { return { id: product.ID, name: product.Name, description: product.Description, - images: [], - variants: [], + slug: product.ID, + images: product.xp.Images, price: { - value: product.priceSchedule.PriceBreaks[0].Price, - currencyCode: 'USD', + value: product.xp.Price, + currencyCode: product.xp.PriceCurrency, }, - options: [], + // TODO: Implement this + variants: [ + { + id: 'unique', + options: [ + { + id: 'unique', + displayName: 'Model', + values: [ + { + label: 'Unique', + }, + ], + }, + ], + }, + ], + options: [ + { + id: 'option-color', + displayName: 'Color', + values: [ + { + label: 'color', + hexColors: ['#222'], + }, + ], + }, + { + id: 'option-size', + displayName: 'Size', + values: [ + { + label: 'S', + }, + { + label: 'M', + }, + { + label: 'L', + }, + ], + }, + ], } }