mirror of
https://github.com/vercel/commerce.git
synced 2025-05-15 05:56:59 +00:00
Refactor SFCC SDK implementation and configuration
This commit is contained in:
parent
52842e11cb
commit
8de3aa431e
3
.gitignore
vendored
3
.gitignore
vendored
@ -37,3 +37,6 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
|
# editors
|
||||||
|
.cursor
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import OpengraphImage from 'components/opengraph-image';
|
import OpengraphImage from "components/opengraph-image";
|
||||||
import { fetchCollection as getCollection } from 'lib/sfcc/scapi';
|
import { getCollection } from "lib/sfcc";
|
||||||
|
|
||||||
export default async function Image({
|
export default async function Image({
|
||||||
params
|
params
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
Checkout,
|
helpers,
|
||||||
Customer,
|
ShopperBaskets,
|
||||||
Product as SalesforceProduct,
|
ShopperBasketsTypes,
|
||||||
Search,
|
ShopperLogin,
|
||||||
} from "commerce-sdk";
|
ShopperProducts,
|
||||||
import { ShopperBaskets } from "commerce-sdk/dist/checkout/checkout";
|
ShopperProductsTypes,
|
||||||
|
ShopperSearch,
|
||||||
|
} from "commerce-sdk-isomorphic";
|
||||||
import { defaultSort, storeCatalog, TAGS } from "lib/constants";
|
import { defaultSort, storeCatalog, TAGS } from "lib/constants";
|
||||||
import { unstable_cache as cache, revalidateTag } from "next/cache";
|
import { unstable_cache as cache, revalidateTag } from "next/cache";
|
||||||
import { cookies, headers } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
@ -17,20 +19,21 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
Product,
|
Product,
|
||||||
ProductRecommendations,
|
ProductRecommendations,
|
||||||
|
SdkError,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const config = {
|
const apiConfig = {
|
||||||
headers: {},
|
throwOnBadResponse: true,
|
||||||
parameters: {
|
parameters: {
|
||||||
clientId: process.env.SFCC_CLIENT_ID,
|
clientId: process.env.SFCC_CLIENT_ID || "",
|
||||||
organizationId: process.env.SFCC_ORGANIZATIONID,
|
organizationId: process.env.SFCC_ORGANIZATIONID || "",
|
||||||
shortCode: process.env.SFCC_SHORTCODE,
|
shortCode: process.env.SFCC_SHORTCODE || "",
|
||||||
siteId: process.env.SFCC_SITEID,
|
siteId: process.env.SFCC_SITEID || "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type SortedProductResult = {
|
type SortedProductResult = {
|
||||||
productResult: SalesforceProduct.ShopperProducts.Product;
|
productResult: ShopperProductsTypes.Product;
|
||||||
index: number;
|
index: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -110,8 +113,8 @@ export async function createCart() {
|
|||||||
// get the guest config
|
// get the guest config
|
||||||
const config = await getGuestUserConfig(guestToken);
|
const config = await getGuestUserConfig(guestToken);
|
||||||
|
|
||||||
// initialize the basket config
|
// initialize the basket client
|
||||||
const basketClient = new Checkout.ShopperBaskets(config);
|
const basketClient = new ShopperBaskets(config);
|
||||||
|
|
||||||
// create an empty ShopperBaskets.Basket
|
// create an empty ShopperBaskets.Basket
|
||||||
const createdBasket = await basketClient.createBasket({
|
const createdBasket = await basketClient.createBasket({
|
||||||
@ -133,13 +136,11 @@ export async function getCart(): Promise<Cart | undefined> {
|
|||||||
if (!cartId) return;
|
if (!cartId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const basketClient = new Checkout.ShopperBaskets(config);
|
const basketClient = new ShopperBaskets(config);
|
||||||
|
|
||||||
const basket = await basketClient.getBasket({
|
const basket = await basketClient.getBasket({
|
||||||
parameters: {
|
parameters: {
|
||||||
basketId: cartId,
|
basketId: cartId,
|
||||||
organizationId: process.env.SFCC_ORGANIZATIONID,
|
|
||||||
siteId: process.env.SFCC_SITEID,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -162,13 +163,11 @@ export async function addToCart(
|
|||||||
const config = await getGuestUserConfig(guestToken);
|
const config = await getGuestUserConfig(guestToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const basketClient = new Checkout.ShopperBaskets(config);
|
const basketClient = new ShopperBaskets(config);
|
||||||
|
|
||||||
const basket = await basketClient.addItemToBasket({
|
const basket = await basketClient.addItemToBasket({
|
||||||
parameters: {
|
parameters: {
|
||||||
basketId: cartId,
|
basketId: cartId,
|
||||||
organizationId: process.env.SFCC_ORGANIZATIONID,
|
|
||||||
siteId: process.env.SFCC_SITEID,
|
|
||||||
},
|
},
|
||||||
body: lines.map((line) => {
|
body: lines.map((line) => {
|
||||||
return {
|
return {
|
||||||
@ -198,7 +197,7 @@ export async function removeFromCart(lineIds: string[]) {
|
|||||||
const guestToken = (await cookies()).get("guest_token")?.value;
|
const guestToken = (await cookies()).get("guest_token")?.value;
|
||||||
const config = await getGuestUserConfig(guestToken);
|
const config = await getGuestUserConfig(guestToken);
|
||||||
|
|
||||||
const basketClient = new Checkout.ShopperBaskets(config);
|
const basketClient = new ShopperBaskets(config);
|
||||||
|
|
||||||
const basket = await basketClient.removeItemFromBasket({
|
const basket = await basketClient.removeItemFromBasket({
|
||||||
parameters: {
|
parameters: {
|
||||||
@ -219,7 +218,7 @@ export async function updateCart(
|
|||||||
const guestToken = (await cookies()).get("guest_token")?.value;
|
const guestToken = (await cookies()).get("guest_token")?.value;
|
||||||
const config = await getGuestUserConfig(guestToken);
|
const config = await getGuestUserConfig(guestToken);
|
||||||
|
|
||||||
const basketClient = new Checkout.ShopperBaskets(config);
|
const basketClient = new ShopperBaskets(config);
|
||||||
|
|
||||||
// ProductItem quantity can not be updated through the API
|
// ProductItem quantity can not be updated through the API
|
||||||
// Quantity updates need to remove all items from the cart and add them back with updated quantities
|
// Quantity updates need to remove all items from the cart and add them back with updated quantities
|
||||||
@ -273,8 +272,8 @@ export async function getProductRecommendations(productId: string) {
|
|||||||
|
|
||||||
if (!ocProductRecommendations?.recommendations?.length) return [];
|
if (!ocProductRecommendations?.recommendations?.length) return [];
|
||||||
|
|
||||||
const clientConfig = await getGuestUserConfig();
|
const config = await getGuestUserConfig();
|
||||||
const productsClient = new SalesforceProduct.ShopperProducts(clientConfig);
|
const productsClient = new ShopperProducts(config);
|
||||||
|
|
||||||
const recommendedProducts: SortedProductResult[] = [];
|
const recommendedProducts: SortedProductResult[] = [];
|
||||||
|
|
||||||
@ -283,8 +282,6 @@ export async function getProductRecommendations(productId: string) {
|
|||||||
async (recommendation, index) => {
|
async (recommendation, index) => {
|
||||||
const productResult = await productsClient.getProduct({
|
const productResult = await productsClient.getProduct({
|
||||||
parameters: {
|
parameters: {
|
||||||
organizationId: clientConfig.parameters.organizationId,
|
|
||||||
siteId: clientConfig.parameters.siteId,
|
|
||||||
id: recommendation.recommended_item_id,
|
id: recommendation.recommended_item_id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -294,7 +291,7 @@ export async function getProductRecommendations(productId: string) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const sortedResults = recommendedProducts
|
const sortedResults = recommendedProducts
|
||||||
.sort((a: any, b: any) => a.index - b.index)
|
.sort((a, b) => a.index - b.index)
|
||||||
.map((item) => item.productResult);
|
.map((item) => item.productResult);
|
||||||
|
|
||||||
return reshapeProducts(sortedResults);
|
return reshapeProducts(sortedResults);
|
||||||
@ -338,30 +335,29 @@ export async function revalidate(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getGuestUserAuthToken() {
|
async function getGuestUserAuthToken() {
|
||||||
const base64data = Buffer.from(
|
const loginClient = new ShopperLogin(apiConfig);
|
||||||
`${process.env.SFCC_CLIENT_ID}:${process.env.SFCC_SECRET}`
|
try {
|
||||||
).toString("base64");
|
return await helpers.loginGuestUserPrivate(
|
||||||
const headers = { Authorization: `Basic ${base64data}` };
|
loginClient,
|
||||||
const client = new Customer.ShopperLogin(config);
|
{},
|
||||||
|
{ clientSecret: process.env.SFCC_SECRET || "" }
|
||||||
return await client.getAccessToken({
|
);
|
||||||
headers,
|
} catch (e) {
|
||||||
body: {
|
// The commerce sdk is configured to throw a custom error for any 400 or 500 response.
|
||||||
grant_type: "client_credentials",
|
// See https://github.com/SalesforceCommerceCloud/commerce-sdk-isomorphic/tree/main?tab=readme-ov-file#throwonbadresponse
|
||||||
channel_id: process.env.SFCC_SITEID,
|
const sdkError = e as SdkError;
|
||||||
},
|
if (sdkError.response) {
|
||||||
});
|
const error = await sdkError.response.json();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error("Failed to retrieve access token");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getGuestUserConfig(token?: string) {
|
async function getGuestUserConfig(token?: string) {
|
||||||
const guestToken = token || (await getGuestUserAuthToken()).access_token;
|
const guestToken = token || (await getGuestUserAuthToken()).access_token;
|
||||||
|
|
||||||
if (!guestToken) {
|
|
||||||
throw new Error("Failed to retrieve access token");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...config,
|
...apiConfig,
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${guestToken}`,
|
authorization: `Bearer ${guestToken}`,
|
||||||
},
|
},
|
||||||
@ -370,7 +366,7 @@ async function getGuestUserConfig(token?: string) {
|
|||||||
|
|
||||||
async function getSFCCCollections() {
|
async function getSFCCCollections() {
|
||||||
const config = await getGuestUserConfig();
|
const config = await getGuestUserConfig();
|
||||||
const productsClient = new SalesforceProduct.ShopperProducts(config);
|
const productsClient = new ShopperProducts(config);
|
||||||
|
|
||||||
const result = await productsClient.getCategories({
|
const result = await productsClient.getCategories({
|
||||||
parameters: {
|
parameters: {
|
||||||
@ -378,17 +374,15 @@ async function getSFCCCollections() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return reshapeCategories(result.data || []);
|
return reshapeCategories(result?.data || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSFCCProduct(id: string) {
|
async function getSFCCProduct(id: string) {
|
||||||
const config = await getGuestUserConfig();
|
const config = await getGuestUserConfig();
|
||||||
const productsClient = new SalesforceProduct.ShopperProducts(config);
|
const productsClient = new ShopperProducts(config);
|
||||||
|
|
||||||
const product = await productsClient.getProduct({
|
const product = await productsClient.getProduct({
|
||||||
parameters: {
|
parameters: {
|
||||||
organizationId: config.parameters.organizationId,
|
|
||||||
siteId: config.parameters.siteId,
|
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -404,7 +398,7 @@ async function searchProducts(options: {
|
|||||||
const { query, categoryId, sortKey = defaultSort.sortKey } = options;
|
const { query, categoryId, sortKey = defaultSort.sortKey } = options;
|
||||||
const config = await getGuestUserConfig();
|
const config = await getGuestUserConfig();
|
||||||
|
|
||||||
const searchClient = new Search.ShopperSearch(config);
|
const searchClient = new ShopperSearch(config);
|
||||||
const searchResults = await searchClient.productSearch({
|
const searchResults = await searchClient.productSearch({
|
||||||
parameters: {
|
parameters: {
|
||||||
q: query || "",
|
q: query || "",
|
||||||
@ -416,30 +410,26 @@ async function searchProducts(options: {
|
|||||||
|
|
||||||
const results: SortedProductResult[] = [];
|
const results: SortedProductResult[] = [];
|
||||||
|
|
||||||
const productsClient = new SalesforceProduct.ShopperProducts(config);
|
const productsClient = new ShopperProducts(config);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
searchResults.hits.map(
|
searchResults.hits.map(async (product, index) => {
|
||||||
async (product: { productId: string }, index: number) => {
|
|
||||||
const productResult = await productsClient.getProduct({
|
const productResult = await productsClient.getProduct({
|
||||||
parameters: {
|
parameters: {
|
||||||
organizationId: config.parameters.organizationId,
|
|
||||||
siteId: config.parameters.siteId,
|
|
||||||
id: product.productId,
|
id: product.productId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
results.push({ productResult, index });
|
results.push({ productResult, index });
|
||||||
}
|
})
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedResults = results
|
const sortedResults = results
|
||||||
.sort((a: any, b: any) => a.index - b.index)
|
.sort((a, b) => a.index - b.index)
|
||||||
.map((item) => item.productResult);
|
.map((item) => item.productResult);
|
||||||
|
|
||||||
return reshapeProducts(sortedResults);
|
return reshapeProducts(sortedResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCartItems(createdBasket: ShopperBaskets.Basket) {
|
async function getCartItems(createdBasket: ShopperBasketsTypes.Basket) {
|
||||||
const cartItems: CartItem[] = [];
|
const cartItems: CartItem[] = [];
|
||||||
|
|
||||||
if (createdBasket.productItems) {
|
if (createdBasket.productItems) {
|
||||||
@ -448,16 +438,15 @@ async function getCartItems(createdBasket: ShopperBaskets.Basket) {
|
|||||||
// Fetch all matching products for items in the cart
|
// Fetch all matching products for items in the cart
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
createdBasket.productItems
|
createdBasket.productItems
|
||||||
.filter((l: ShopperBaskets.ProductItem) => l.productId)
|
.filter((l) => l.productId)
|
||||||
.map(async (l: ShopperBaskets.ProductItem) => {
|
.map(async (l) => {
|
||||||
const product = await getProduct(l.productId!);
|
const product = await getProduct(l.productId!);
|
||||||
productsInCart.push(product);
|
productsInCart.push(product);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reshape the sfcc items and push them onto the cartItems
|
// Reshape the sfcc items and push them onto the cartItems
|
||||||
createdBasket.productItems.map(
|
createdBasket.productItems.map((productItem) => {
|
||||||
(productItem: ShopperBaskets.ProductItem) => {
|
|
||||||
cartItems.push(
|
cartItems.push(
|
||||||
reshapeProductItem(
|
reshapeProductItem(
|
||||||
productItem,
|
productItem,
|
||||||
@ -465,15 +454,14 @@ async function getCartItems(createdBasket: ShopperBaskets.Basket) {
|
|||||||
productsInCart.find((p) => p.id === productItem.productId)!
|
productsInCart.find((p) => p.id === productItem.productId)!
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cartItems;
|
return cartItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reshapeCategory(
|
function reshapeCategory(
|
||||||
category: SalesforceProduct.ShopperProducts.Category
|
category: ShopperProductsTypes.Category
|
||||||
): Collection | undefined {
|
): Collection | undefined {
|
||||||
if (!category) {
|
if (!category) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -492,9 +480,7 @@ function reshapeCategory(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function reshapeCategories(
|
function reshapeCategories(categories: ShopperProductsTypes.Category[]) {
|
||||||
categories: SalesforceProduct.ShopperProducts.Category[]
|
|
||||||
) {
|
|
||||||
const reshapedCategories = [];
|
const reshapedCategories = [];
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
if (category) {
|
if (category) {
|
||||||
@ -507,7 +493,7 @@ function reshapeCategories(
|
|||||||
return reshapedCategories;
|
return reshapedCategories;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reshapeProduct(product: SalesforceProduct.ShopperProducts.Product) {
|
function reshapeProduct(product: ShopperProductsTypes.Product) {
|
||||||
if (!product.name) {
|
if (!product.name) {
|
||||||
throw new Error("Product name is not set");
|
throw new Error("Product name is not set");
|
||||||
}
|
}
|
||||||
@ -547,7 +533,8 @@ function reshapeProduct(product: SalesforceProduct.ShopperProducts.Product) {
|
|||||||
},
|
},
|
||||||
images: images,
|
images: images,
|
||||||
options:
|
options:
|
||||||
product.variationAttributes?.map((attribute) => {
|
product.variationAttributes?.map(
|
||||||
|
(attribute: ShopperProductsTypes.VariationAttribute) => {
|
||||||
return {
|
return {
|
||||||
id: attribute.id,
|
id: attribute.id,
|
||||||
name: attribute.name!,
|
name: attribute.name!,
|
||||||
@ -557,7 +544,8 @@ function reshapeProduct(product: SalesforceProduct.ShopperProducts.Product) {
|
|||||||
?.filter((v) => v.value !== undefined)
|
?.filter((v) => v.value !== undefined)
|
||||||
?.map((v) => v.name!) || [],
|
?.map((v) => v.name!) || [],
|
||||||
};
|
};
|
||||||
}) || [],
|
}
|
||||||
|
) || [],
|
||||||
seo: {
|
seo: {
|
||||||
title: product.pageTitle || "",
|
title: product.pageTitle || "",
|
||||||
description: product.pageDescription || "",
|
description: product.pageDescription || "",
|
||||||
@ -567,9 +555,7 @@ function reshapeProduct(product: SalesforceProduct.ShopperProducts.Product) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function reshapeProducts(
|
function reshapeProducts(products: ShopperProductsTypes.Product[]) {
|
||||||
products: SalesforceProduct.ShopperProducts.Product[]
|
|
||||||
) {
|
|
||||||
const reshapedProducts = [];
|
const reshapedProducts = [];
|
||||||
for (const product of products) {
|
for (const product of products) {
|
||||||
if (product) {
|
if (product) {
|
||||||
@ -583,7 +569,7 @@ function reshapeProducts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reshapeImages(
|
function reshapeImages(
|
||||||
imageGroups: SalesforceProduct.ShopperProducts.ImageGroup[] | undefined
|
imageGroups: ShopperProductsTypes.ImageGroup[] | undefined
|
||||||
): Image[] {
|
): Image[] {
|
||||||
if (!imageGroups) return [];
|
if (!imageGroups) return [];
|
||||||
|
|
||||||
@ -602,15 +588,15 @@ function reshapeImages(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reshapeVariants(
|
function reshapeVariants(
|
||||||
variants: SalesforceProduct.ShopperProducts.Variant[],
|
variants: ShopperProductsTypes.Variant[],
|
||||||
product: SalesforceProduct.ShopperProducts.Product
|
product: ShopperProductsTypes.Product
|
||||||
) {
|
) {
|
||||||
return variants.map((variant) => reshapeVariant(variant, product));
|
return variants.map((variant) => reshapeVariant(variant, product));
|
||||||
}
|
}
|
||||||
|
|
||||||
function reshapeVariant(
|
function reshapeVariant(
|
||||||
variant: SalesforceProduct.ShopperProducts.Variant,
|
variant: ShopperProductsTypes.Variant,
|
||||||
product: SalesforceProduct.ShopperProducts.Product
|
product: ShopperProductsTypes.Product
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
id: variant.productId,
|
id: variant.productId,
|
||||||
@ -636,7 +622,7 @@ function reshapeVariant(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reshapeProductItem(
|
function reshapeProductItem(
|
||||||
item: Checkout.ShopperBaskets.ProductItem,
|
item: ShopperBasketsTypes.ProductItem,
|
||||||
currency: string,
|
currency: string,
|
||||||
matchingProduct: Product
|
matchingProduct: Product
|
||||||
): CartItem {
|
): CartItem {
|
||||||
@ -665,7 +651,7 @@ function reshapeProductItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reshapeBasket(
|
function reshapeBasket(
|
||||||
basket: ShopperBaskets.Basket,
|
basket: ShopperBasketsTypes.Basket,
|
||||||
cartItems: CartItem[]
|
cartItems: CartItem[]
|
||||||
): Cart {
|
): Cart {
|
||||||
return {
|
return {
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
import { Collection } from './types';
|
|
||||||
import { ExtractVariables, salesforceFetch } from './utils';
|
|
||||||
|
|
||||||
export async function scapiFetch<T>(options: {
|
|
||||||
method: 'POST' | 'GET';
|
|
||||||
apiEndpoint: string;
|
|
||||||
cache?: RequestCache;
|
|
||||||
headers?: HeadersInit;
|
|
||||||
tags?: string[];
|
|
||||||
variables?: ExtractVariables<T>;
|
|
||||||
}): Promise<{ status: number; body: T } | never> {
|
|
||||||
const scapiDomain = `https://${process.env.SFCC_SHORTCODE}.api.commercecloud.salesforce.com`;
|
|
||||||
const apiEndpoint = `${scapiDomain}${options.apiEndpoint}?siteId=${process.env.SFCC_SITEID}`;
|
|
||||||
return salesforceFetch<T>({
|
|
||||||
...options,
|
|
||||||
apiEndpoint
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchAccessToken() {
|
|
||||||
const response = await scapiFetch<{ access_token: string }>({
|
|
||||||
method: 'POST',
|
|
||||||
apiEndpoint: `/shopper/auth/v1/organizations/${process.env.SFCC_ORGANIZATIONID}/oauth2/token?grant_type=client_credentials&channel_id=${process.env.SFCC_SITEID}`,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${Buffer.from(
|
|
||||||
`${process.env.SFCC_CLIENT_ID}:${process.env.SFCC_SECRET}`
|
|
||||||
).toString('base64')}`,
|
|
||||||
'content-type': 'application/x-www-form-urlencoded'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200 || !response.body.access_token) {
|
|
||||||
throw new Error('Failed to fetch access token');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body.access_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchCollection(handle: string): Promise<Collection | undefined> {
|
|
||||||
const accessToken = await fetchAccessToken();
|
|
||||||
|
|
||||||
const response = await scapiFetch<Collection>({
|
|
||||||
method: 'GET',
|
|
||||||
apiEndpoint: `/product/shopper-products/v1/organizations/${process.env.SFCC_ORGANIZATIONID}/products/${handle}`,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error('Failed to fetch collection');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body;
|
|
||||||
}
|
|
@ -35,7 +35,7 @@ export type SalesforceProduct = {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Product = Omit<SalesforceProduct, 'variants' | 'images'> & {
|
export type Product = Omit<SalesforceProduct, "variants" | "images"> & {
|
||||||
variants: ProductVariant[];
|
variants: ProductVariant[];
|
||||||
images: Image[];
|
images: Image[];
|
||||||
};
|
};
|
||||||
@ -86,7 +86,7 @@ export type SalesforceCart = {
|
|||||||
totalQuantity: number;
|
totalQuantity: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Cart = Omit<SalesforceCart, 'lines'> & {
|
export type Cart = Omit<SalesforceCart, "lines"> & {
|
||||||
lines: CartItem[];
|
lines: CartItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -145,3 +145,7 @@ export type Page = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SdkError = {
|
||||||
|
response?: Response;
|
||||||
|
};
|
||||||
|
14
package.json
14
package.json
@ -11,19 +11,19 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"commerce-sdk": "^4.0.0",
|
|
||||||
"geist": "^1.3.1",
|
|
||||||
"next": "15.2.0-canary.67",
|
|
||||||
"react": "19.0.0",
|
|
||||||
"react-dom": "19.0.0",
|
|
||||||
"sonner": "^2.0.1",
|
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"commerce-sdk-isomorphic": "^3.2.0",
|
||||||
|
"geist": "^1.3.1",
|
||||||
"lucide-react": "^0.438.0",
|
"lucide-react": "^0.438.0",
|
||||||
|
"next": "15.2.0-canary.67",
|
||||||
|
"react": "19.0.0",
|
||||||
|
"react-dom": "19.0.0",
|
||||||
|
"sonner": "^2.0.1",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
|
2807
pnpm-lock.yaml
generated
2807
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user