mirror of
https://github.com/vercel/commerce.git
synced 2025-07-23 04:36:49 +00:00
SFCC provider (#727)
* new SFCC provider * add search * normalization + search * categories as search results * adress PR feedback * Update README.md * get all paths for SSG * product variants and options * Apply suggestions from code review Co-authored-by: Luis Alvarez D. <luis@vercel.com> * remove console log * prettier * clean console log * ran prettier * Updated readme * remove static data and revert config changes * set default site Co-authored-by: Luis Alvarez D. <luis@vercel.com>
This commit is contained in:
1
packages/sfcc/src/api/endpoints/cart/index.ts
Normal file
1
packages/sfcc/src/api/endpoints/cart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@@ -0,0 +1,32 @@
|
||||
import { normalizeSearchProducts } from '../../../utils/normalise-product'
|
||||
import { ProductsEndpoint } from '.'
|
||||
|
||||
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
||||
req,
|
||||
res,
|
||||
body: { search, categoryId, brandId, sort },
|
||||
config,
|
||||
}) => {
|
||||
const { sdk } = config
|
||||
|
||||
// 'clothing' is our main category default, and a manually set category has priority
|
||||
const searchTerm = categoryId ? (categoryId as string) : search || 'clothing'
|
||||
|
||||
const searchClient = await sdk.getSearchClient()
|
||||
// use SDK search API for initial products
|
||||
const searchResults = await searchClient.productSearch({
|
||||
parameters: {
|
||||
q: searchTerm,
|
||||
limit: 20,
|
||||
},
|
||||
})
|
||||
let products = []
|
||||
let found = false
|
||||
if (searchResults.total) {
|
||||
found = true
|
||||
products = normalizeSearchProducts(searchResults.hits) as any[]
|
||||
}
|
||||
res.status(200).json({ data: { products, found } })
|
||||
}
|
||||
|
||||
export default getProducts
|
19
packages/sfcc/src/api/endpoints/catalog/products/index.ts
Normal file
19
packages/sfcc/src/api/endpoints/catalog/products/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { SFCCProviderAPI } from '../../..'
|
||||
|
||||
import { createEndpoint, GetAPISchema } from '@vercel/commerce/api'
|
||||
import { ProductsSchema } from '@vercel/commerce/types/product'
|
||||
import getProducts from './get-products'
|
||||
import productsEndpoint from '@vercel/commerce/api/endpoints/catalog/products'
|
||||
|
||||
export type ProductsAPI = GetAPISchema<SFCCProviderAPI, ProductsSchema>
|
||||
|
||||
export type ProductsEndpoint = ProductsAPI['endpoint']
|
||||
|
||||
export const handlers: ProductsEndpoint['handlers'] = { getProducts }
|
||||
|
||||
const productsApi = createEndpoint<ProductsAPI>({
|
||||
handler: productsEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default productsApi
|
1
packages/sfcc/src/api/endpoints/checkout/index.ts
Normal file
1
packages/sfcc/src/api/endpoints/checkout/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/sfcc/src/api/endpoints/customer/address.ts
Normal file
1
packages/sfcc/src/api/endpoints/customer/address.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/sfcc/src/api/endpoints/customer/card.ts
Normal file
1
packages/sfcc/src/api/endpoints/customer/card.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/sfcc/src/api/endpoints/customer/index.ts
Normal file
1
packages/sfcc/src/api/endpoints/customer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/sfcc/src/api/endpoints/login/index.ts
Normal file
1
packages/sfcc/src/api/endpoints/login/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/sfcc/src/api/endpoints/logout/index.ts
Normal file
1
packages/sfcc/src/api/endpoints/logout/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/sfcc/src/api/endpoints/signup/index.ts
Normal file
1
packages/sfcc/src/api/endpoints/signup/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/sfcc/src/api/endpoints/wishlist/index.tsx
Normal file
1
packages/sfcc/src/api/endpoints/wishlist/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
48
packages/sfcc/src/api/index.ts
Normal file
48
packages/sfcc/src/api/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { CommerceAPI, CommerceAPIConfig } from '@vercel/commerce/api'
|
||||
import { getCommerceApi as commerceApi } from '@vercel/commerce/api'
|
||||
import createFetcher from './utils/fetch-local'
|
||||
import sdk, { Sdk } from './utils/sfcc-sdk'
|
||||
|
||||
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 SFCCConfig extends CommerceAPIConfig {
|
||||
sdk: Sdk
|
||||
}
|
||||
const config: SFCCConfig = {
|
||||
commerceUrl: '',
|
||||
apiToken: '',
|
||||
cartCookie: '',
|
||||
customerCookie: '',
|
||||
cartCookieMaxAge: 2592000,
|
||||
fetch: createFetcher(() => getCommerceApi().getConfig()),
|
||||
sdk, // SalesForce Cloud Commerce API SDK
|
||||
}
|
||||
|
||||
const operations = {
|
||||
getAllPages,
|
||||
getPage,
|
||||
getSiteInfo,
|
||||
getCustomerWishlist,
|
||||
getAllProductPaths,
|
||||
getAllProducts,
|
||||
getProduct,
|
||||
}
|
||||
|
||||
export const provider = { config, operations }
|
||||
|
||||
export type Provider = typeof provider
|
||||
export type SFCCProviderAPI<P extends Provider = Provider> = CommerceAPI<
|
||||
P | any
|
||||
>
|
||||
|
||||
export function getCommerceApi<P extends Provider>(
|
||||
customProvider: P = provider as any
|
||||
): SFCCProviderAPI<P> {
|
||||
return commerceApi(customProvider as any)
|
||||
}
|
19
packages/sfcc/src/api/operations/get-all-pages.ts
Normal file
19
packages/sfcc/src/api/operations/get-all-pages.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export type Page = { url: string }
|
||||
export type GetAllPagesResult = { pages: Page[] }
|
||||
import type { SFCCConfig } from '../index'
|
||||
|
||||
export default function getAllPagesOperation() {
|
||||
function getAllPages({
|
||||
config,
|
||||
preview,
|
||||
}: {
|
||||
url?: string
|
||||
config?: Partial<SFCCConfig>
|
||||
preview?: boolean
|
||||
}): Promise<GetAllPagesResult> {
|
||||
return Promise.resolve({
|
||||
pages: [],
|
||||
})
|
||||
}
|
||||
return getAllPages
|
||||
}
|
45
packages/sfcc/src/api/operations/get-all-product-paths.ts
Normal file
45
packages/sfcc/src/api/operations/get-all-product-paths.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Product } from '@vercel/commerce/types/product'
|
||||
import { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import { normalizeSearchProducts } from '../utils/normalise-product'
|
||||
import { SFCCConfig } from '..'
|
||||
|
||||
export type GetAllProductPathsResult = {
|
||||
products: Array<{ path: string }>
|
||||
}
|
||||
|
||||
export default function getAllProductPathsOperation({
|
||||
commerce,
|
||||
}: OperationContext<any>) {
|
||||
async function getAllProductPaths({
|
||||
query,
|
||||
config,
|
||||
variables,
|
||||
}: {
|
||||
query?: string
|
||||
config?: SFCCConfig
|
||||
variables?: any
|
||||
} = {}): Promise<GetAllProductPathsResult> {
|
||||
// TODO: support locale
|
||||
const { sdk, locale } = commerce.getConfig(config) as SFCCConfig
|
||||
const searchClient = await sdk.getSearchClient()
|
||||
|
||||
// use SDK search API for initial products same as getAllProductsOperation
|
||||
const searchResults = await searchClient.productSearch({
|
||||
parameters: { q: 'dress', limit: variables?.first },
|
||||
})
|
||||
|
||||
let products = [] as Product[]
|
||||
|
||||
if (searchResults.total) {
|
||||
products = normalizeSearchProducts(searchResults.hits)
|
||||
}
|
||||
|
||||
return {
|
||||
products: products?.map(({ slug }: Product) => ({
|
||||
path: `/${slug}`,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
return getAllProductPaths
|
||||
}
|
40
packages/sfcc/src/api/operations/get-all-products.ts
Normal file
40
packages/sfcc/src/api/operations/get-all-products.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Product } from '@vercel/commerce/types/product'
|
||||
import { GetAllProductsOperation } from '@vercel/commerce/types/product'
|
||||
import type { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import type { SFCCConfig } from '../index'
|
||||
import { normalizeSearchProducts } from '../utils/normalise-product'
|
||||
|
||||
export default function getAllProductsOperation({
|
||||
commerce,
|
||||
}: OperationContext<any>) {
|
||||
async function getAllProducts<T extends GetAllProductsOperation>({
|
||||
query = '',
|
||||
variables,
|
||||
config,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: T['variables']
|
||||
config?: Partial<SFCCConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<{ products: Product[] | any[] }> {
|
||||
// TODO: support locale
|
||||
const { sdk, locale } = commerce.getConfig(config) as SFCCConfig
|
||||
const searchClient = await sdk.getSearchClient()
|
||||
|
||||
// use SDK search API for initial products
|
||||
const searchResults = await searchClient.productSearch({
|
||||
parameters: { q: 'dress', limit: variables?.first },
|
||||
})
|
||||
|
||||
let products = [] as Product[]
|
||||
|
||||
if (searchResults.total) {
|
||||
products = normalizeSearchProducts(searchResults.hits)
|
||||
}
|
||||
|
||||
return {
|
||||
products: products,
|
||||
}
|
||||
}
|
||||
return getAllProducts
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
export default function getCustomerWishlistOperation() {
|
||||
function getCustomerWishlist(): any {
|
||||
return { wishlist: {} }
|
||||
}
|
||||
return getCustomerWishlist
|
||||
}
|
13
packages/sfcc/src/api/operations/get-page.ts
Normal file
13
packages/sfcc/src/api/operations/get-page.ts
Normal 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
|
||||
}
|
33
packages/sfcc/src/api/operations/get-product.ts
Normal file
33
packages/sfcc/src/api/operations/get-product.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { GetProductOperation, Product } from '@vercel/commerce/types/product'
|
||||
import type { SFCCConfig } from '../index'
|
||||
import type { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import { normalizeProduct } from '../utils/normalise-product'
|
||||
|
||||
export default function getProductOperation({
|
||||
commerce,
|
||||
}: OperationContext<any>) {
|
||||
async function getProduct<T extends GetProductOperation>({
|
||||
query = '',
|
||||
variables,
|
||||
config,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: T['variables']
|
||||
config?: Partial<SFCCConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<Product | {} | any> {
|
||||
// TODO: support locale
|
||||
const { sdk, locale } = commerce.getConfig(config) as SFCCConfig
|
||||
const shopperProductsClient = await sdk.getshopperProductsClient()
|
||||
const product = await shopperProductsClient.getProduct({
|
||||
parameters: { id: variables?.slug as string },
|
||||
})
|
||||
const normalizedProduct = normalizeProduct(product)
|
||||
|
||||
return {
|
||||
product: normalizedProduct,
|
||||
}
|
||||
}
|
||||
|
||||
return getProduct
|
||||
}
|
43
packages/sfcc/src/api/operations/get-site-info.ts
Normal file
43
packages/sfcc/src/api/operations/get-site-info.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import { Category } from '@vercel/commerce/types/site'
|
||||
import { SFCCConfig } from '../index'
|
||||
|
||||
export type GetSiteInfoResult<
|
||||
T extends { categories: any[]; brands: any[] } = {
|
||||
categories: Category[]
|
||||
brands: any[]
|
||||
}
|
||||
> = T
|
||||
|
||||
export default function getSiteInfoOperation({}: OperationContext<any>) {
|
||||
function getSiteInfo({
|
||||
query,
|
||||
variables,
|
||||
config: cfg,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: any
|
||||
config?: Partial<SFCCConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<GetSiteInfoResult> {
|
||||
return Promise.resolve({
|
||||
categories: [
|
||||
{
|
||||
id: 'new-arrivals',
|
||||
name: 'New Arrivals',
|
||||
slug: 'new-arrivals',
|
||||
path: '/new-arrivals',
|
||||
},
|
||||
{
|
||||
id: 'womens-clothing-dresses',
|
||||
name: 'Womens Clothing Dresses',
|
||||
slug: 'womens-clothing-dresses',
|
||||
path: '/womens-clothing-dresses',
|
||||
},
|
||||
],
|
||||
brands: [],
|
||||
})
|
||||
}
|
||||
|
||||
return getSiteInfo
|
||||
}
|
6
packages/sfcc/src/api/operations/index.ts
Normal file
6
packages/sfcc/src/api/operations/index.ts
Normal 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'
|
34
packages/sfcc/src/api/utils/fetch-local.ts
Normal file
34
packages/sfcc/src/api/utils/fetch-local.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
import type { GraphQLFetcher } from '@vercel/commerce/api'
|
||||
import type { SFCCConfig } from '../index'
|
||||
import fetch from './fetch'
|
||||
|
||||
const fetchGraphqlApi: (getConfig: () => SFCCConfig) => 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
|
3
packages/sfcc/src/api/utils/fetch.ts
Normal file
3
packages/sfcc/src/api/utils/fetch.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import zeitFetch from '@vercel/fetch'
|
||||
|
||||
export default zeitFetch()
|
42
packages/sfcc/src/api/utils/get-auth-token.ts
Normal file
42
packages/sfcc/src/api/utils/get-auth-token.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ClientConfig, Customer } from 'commerce-sdk'
|
||||
|
||||
// client configuration parameters
|
||||
export const clientConfig: ClientConfig = {
|
||||
headers: {
|
||||
authorization: ``,
|
||||
},
|
||||
parameters: {
|
||||
clientId: process.env.SFCC_CLIENT_ID || '',
|
||||
organizationId: process.env.SFCC_ORG_ID || '',
|
||||
shortCode: process.env.SFCC_SHORT_CODE || '',
|
||||
siteId: process.env.SFCC_SITE_ID || '',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shopper or guest JWT/access token, along with a refresh token, using client credentials
|
||||
*
|
||||
* @returns guest user authorization token
|
||||
*/
|
||||
export async function getGuestUserAuthToken(): Promise<Customer.ShopperLogin.TokenResponse> {
|
||||
const credentials = `${process.env.SFCC_CLIENT_ID}:${process.env.SFCC_CLIENT_SECRET}`
|
||||
const base64data = Buffer.from(credentials).toString('base64')
|
||||
const headers = { Authorization: `Basic ${base64data}` }
|
||||
const client = new Customer.ShopperLogin(clientConfig)
|
||||
|
||||
return await client.getAccessToken({
|
||||
headers,
|
||||
body: {
|
||||
grant_type: 'client_credentials',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const getConfigAuth = async () => {
|
||||
const shopperToken = await getGuestUserAuthToken()
|
||||
const configAuth = {
|
||||
...clientConfig,
|
||||
headers: { authorization: `Bearer ${shopperToken.access_token}` },
|
||||
}
|
||||
return configAuth
|
||||
}
|
96
packages/sfcc/src/api/utils/normalise-product.ts
Normal file
96
packages/sfcc/src/api/utils/normalise-product.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Product as SFCCProduct, Search } from 'commerce-sdk'
|
||||
import type {
|
||||
Product,
|
||||
ProductImage,
|
||||
ProductOption,
|
||||
ProductVariant,
|
||||
} from '@vercel/commerce/types/product'
|
||||
|
||||
const normaliseOptions = (
|
||||
options: SFCCProduct.ShopperProducts.Product['variationAttributes']
|
||||
): Product['options'] => {
|
||||
if (!Array.isArray(options)) return []
|
||||
|
||||
return options.map((option) => {
|
||||
return {
|
||||
id: option.id,
|
||||
displayName: option.name as string,
|
||||
values: option.values!.map((value) => ({ label: value.name })),
|
||||
} as ProductOption
|
||||
})
|
||||
}
|
||||
|
||||
const normaliseVariants = (
|
||||
variants: SFCCProduct.ShopperProducts.Product['variants']
|
||||
): Product['variants'] => {
|
||||
if (!Array.isArray(variants)) return []
|
||||
|
||||
return variants.map((variant) => {
|
||||
const options = [] as ProductOption[]
|
||||
|
||||
if (variant.variationValues) {
|
||||
for (const [key, value] of Object.entries(variant.variationValues)) {
|
||||
const variantOptionObject = {
|
||||
id: `${variant.productId}-${key}`,
|
||||
displayName: key,
|
||||
values: [
|
||||
{
|
||||
label: value,
|
||||
},
|
||||
],
|
||||
}
|
||||
options.push(variantOptionObject)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: variant.productId,
|
||||
options,
|
||||
} as ProductVariant
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeProduct(
|
||||
product: SFCCProduct.ShopperProducts.Product
|
||||
): Product {
|
||||
return {
|
||||
id: product.id,
|
||||
// TODO: use `name-ID` as a virtual slug (for search 1:1)
|
||||
slug: product.id, // use product ID as a slug
|
||||
name: product.name!,
|
||||
description: product.longDescription!,
|
||||
price: {
|
||||
value: product.price!,
|
||||
currencyCode: product.currency,
|
||||
},
|
||||
images: product.imageGroups![0].images.map((image) => ({
|
||||
url: image.disBaseLink,
|
||||
altText: image.title,
|
||||
})) as ProductImage[],
|
||||
variants: normaliseVariants(product.variants),
|
||||
options: normaliseOptions(product.variationAttributes),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSearchProducts(
|
||||
products: Search.ShopperSearch.ProductSearchHit[]
|
||||
): Product[] {
|
||||
return products.map((product) => ({
|
||||
id: product.productId,
|
||||
slug: product.productId, // use product ID as a slug
|
||||
name: product.productName!,
|
||||
description: '',
|
||||
price: {
|
||||
value: product.price!,
|
||||
currencyCode: product.currency,
|
||||
},
|
||||
images: [
|
||||
{
|
||||
url: product.image!.link,
|
||||
altText: product.productName,
|
||||
} as ProductImage,
|
||||
],
|
||||
variants: normaliseVariants(product.variants),
|
||||
options: normaliseOptions(product.variationAttributes),
|
||||
}))
|
||||
}
|
19
packages/sfcc/src/api/utils/sfcc-sdk.ts
Normal file
19
packages/sfcc/src/api/utils/sfcc-sdk.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Product, Search } from 'commerce-sdk'
|
||||
import { getConfigAuth } from './get-auth-token'
|
||||
|
||||
const getSearchClient = async () => {
|
||||
const configAuth = await getConfigAuth()
|
||||
return new Search.ShopperSearch(configAuth)
|
||||
}
|
||||
|
||||
const getshopperProductsClient = async () => {
|
||||
const configAuth = await getConfigAuth()
|
||||
return new Product.ShopperProducts(configAuth)
|
||||
}
|
||||
|
||||
export const sdk = {
|
||||
getshopperProductsClient,
|
||||
getSearchClient,
|
||||
}
|
||||
export type Sdk = typeof sdk
|
||||
export default sdk
|
3
packages/sfcc/src/auth/index.ts
Normal file
3
packages/sfcc/src/auth/index.ts
Normal 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'
|
16
packages/sfcc/src/auth/use-login.tsx
Normal file
16
packages/sfcc/src/auth/use-login.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useLogin, { UseLogin } from '@vercel/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 () {}
|
||||
},
|
||||
}
|
17
packages/sfcc/src/auth/use-logout.tsx
Normal file
17
packages/sfcc/src/auth/use-logout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useLogout, { UseLogout } from '@vercel/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 () => {},
|
||||
}
|
19
packages/sfcc/src/auth/use-signup.tsx
Normal file
19
packages/sfcc/src/auth/use-signup.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useCallback } from 'react'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup'
|
||||
|
||||
export default useSignup as UseSignup<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
},
|
||||
async fetcher() {
|
||||
return null
|
||||
},
|
||||
useHook:
|
||||
({ fetch }) =>
|
||||
() =>
|
||||
() => {},
|
||||
}
|
4
packages/sfcc/src/cart/index.ts
Normal file
4
packages/sfcc/src/cart/index.ts
Normal 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'
|
17
packages/sfcc/src/cart/use-add-item.tsx
Normal file
17
packages/sfcc/src/cart/use-add-item.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import useAddItem, { UseAddItem } from '@vercel/commerce/cart/use-add-item'
|
||||
import { MutationHook } from '@vercel/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 {}
|
||||
}
|
||||
},
|
||||
}
|
42
packages/sfcc/src/cart/use-cart.tsx
Normal file
42
packages/sfcc/src/cart/use-cart.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useMemo } from 'react'
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCart, { UseCart } from '@vercel/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,
|
||||
},
|
||||
}
|
||||
),
|
||||
[]
|
||||
)
|
||||
},
|
||||
}
|
20
packages/sfcc/src/cart/use-remove-item.tsx
Normal file
20
packages/sfcc/src/cart/use-remove-item.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useRemoveItem, {
|
||||
UseRemoveItem,
|
||||
} from '@vercel/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 {}
|
||||
}
|
||||
},
|
||||
}
|
20
packages/sfcc/src/cart/use-update-item.tsx
Normal file
20
packages/sfcc/src/cart/use-update-item.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useUpdateItem, {
|
||||
UseUpdateItem,
|
||||
} from '@vercel/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 {}
|
||||
}
|
||||
},
|
||||
}
|
16
packages/sfcc/src/checkout/use-checkout.tsx
Normal file
16
packages/sfcc/src/checkout/use-checkout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCheckout, {
|
||||
UseCheckout,
|
||||
} from '@vercel/commerce/checkout/use-checkout'
|
||||
|
||||
export default useCheckout as UseCheckout<typeof handler>
|
||||
|
||||
export const handler: SWRHook<any> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ useData }) =>
|
||||
async (input) => ({}),
|
||||
}
|
10
packages/sfcc/src/commerce.config.json
Normal file
10
packages/sfcc/src/commerce.config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"provider": "sfcc",
|
||||
"features": {
|
||||
"wishlist": false,
|
||||
"cart": false,
|
||||
"search": true,
|
||||
"customerAuth": false,
|
||||
"customCheckout": false
|
||||
}
|
||||
}
|
17
packages/sfcc/src/customer/address/use-add-item.tsx
Normal file
17
packages/sfcc/src/customer/address/use-add-item.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import useAddItem, {
|
||||
UseAddItem,
|
||||
} from '@vercel/commerce/customer/address/use-add-item'
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ fetch }) =>
|
||||
() =>
|
||||
async () => ({}),
|
||||
}
|
17
packages/sfcc/src/customer/card/use-add-item.tsx
Normal file
17
packages/sfcc/src/customer/card/use-add-item.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import useAddItem, {
|
||||
UseAddItem,
|
||||
} from '@vercel/commerce/customer/card/use-add-item'
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ fetch }) =>
|
||||
() =>
|
||||
async () => ({}),
|
||||
}
|
1
packages/sfcc/src/customer/index.ts
Normal file
1
packages/sfcc/src/customer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as useCustomer } from './use-customer'
|
17
packages/sfcc/src/customer/use-customer.tsx
Normal file
17
packages/sfcc/src/customer/use-customer.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCustomer, {
|
||||
UseCustomer,
|
||||
} from '@vercel/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 {}
|
||||
}
|
||||
},
|
||||
}
|
17
packages/sfcc/src/fetcher.ts
Normal file
17
packages/sfcc/src/fetcher.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Fetcher } from '@vercel/commerce/utils/types'
|
||||
|
||||
const clientFetcher: Fetcher = async ({ method, url, body }) => {
|
||||
const response = await fetch(url!, {
|
||||
method,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((response) => response.data)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export default clientFetcher
|
12
packages/sfcc/src/index.tsx
Normal file
12
packages/sfcc/src/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import {
|
||||
getCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@vercel/commerce'
|
||||
import { sfccProvider, SfccProvider } from './provider'
|
||||
|
||||
export { sfccProvider }
|
||||
export type { SfccProvider }
|
||||
|
||||
export const CommerceProvider = getCommerceProvider(sfccProvider)
|
||||
|
||||
export const useCommerce = () => useCoreCommerce<SfccProvider>()
|
12
packages/sfcc/src/next.config.cjs
Normal file
12
packages/sfcc/src/next.config.cjs
Normal file
@@ -0,0 +1,12 @@
|
||||
const commerce = require('./commerce.config.json')
|
||||
|
||||
module.exports = {
|
||||
commerce,
|
||||
images: {
|
||||
domains: [
|
||||
'localhost',
|
||||
'edge.disstg.commercecloud.salesforce.com',
|
||||
'zzte-053.sandbox.us02.dx.commercecloud.salesforce.com',
|
||||
],
|
||||
},
|
||||
}
|
2
packages/sfcc/src/product/index.ts
Normal file
2
packages/sfcc/src/product/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as usePrice } from './use-price'
|
||||
export { default as useSearch } from './use-search'
|
2
packages/sfcc/src/product/use-price.tsx
Normal file
2
packages/sfcc/src/product/use-price.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from '@vercel/commerce/product/use-price'
|
||||
export { default } from '@vercel/commerce/product/use-price'
|
42
packages/sfcc/src/product/use-search.tsx
Normal file
42
packages/sfcc/src/product/use-search.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useSearch, { UseSearch } from '@vercel/commerce/product/use-search'
|
||||
import { SearchProductsHook } from '@vercel/commerce/types/product'
|
||||
export default useSearch as UseSearch<typeof handler>
|
||||
|
||||
export const handler: SWRHook<SearchProductsHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/catalog/products',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
||||
console.log('search', search, categoryId, options)
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', String(search))
|
||||
if (categoryId) url.searchParams.set('categoryId', String(categoryId))
|
||||
if (brandId) url.searchParams.set('brandId', String(brandId))
|
||||
if (sort) url.searchParams.set('sort', String(sort))
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook:
|
||||
({ useData }) =>
|
||||
(input = {}) => {
|
||||
return useData({
|
||||
input: [
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
22
packages/sfcc/src/provider.ts
Normal file
22
packages/sfcc/src/provider.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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 sfccProvider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'session',
|
||||
fetcher,
|
||||
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||
customer: { useCustomer },
|
||||
products: { useSearch },
|
||||
auth: { useLogin, useLogout, useSignup },
|
||||
}
|
||||
|
||||
export type SfccProvider = typeof sfccProvider
|
13
packages/sfcc/src/wishlist/use-add-item.tsx
Normal file
13
packages/sfcc/src/wishlist/use-add-item.tsx
Normal 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
|
17
packages/sfcc/src/wishlist/use-remove-item.tsx
Normal file
17
packages/sfcc/src/wishlist/use-remove-item.tsx
Normal 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
|
43
packages/sfcc/src/wishlist/use-wishlist.tsx
Normal file
43
packages/sfcc/src/wishlist/use-wishlist.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { HookFetcher } from '@vercel/commerce/utils/types'
|
||||
import type { Product } from '@vercel/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)
|
Reference in New Issue
Block a user