diff --git a/commerce.config.json b/commerce.config.json index 06b985504..5d8e7cba5 100644 --- a/commerce.config.json +++ b/commerce.config.json @@ -1,6 +1,9 @@ { "features": { - "wishlist": false, + "cart": false, + "search": false, + "wishlist": true, + "customerAuth": true, "customCheckout": false } } diff --git a/framework/commerce/config.js b/framework/commerce/config.js index 1ba522a1c..de165f66f 100644 --- a/framework/commerce/config.js +++ b/framework/commerce/config.js @@ -14,6 +14,7 @@ const PROVIDERS = [ 'swell', 'vendure', 'local', + 'elasticpath' ] function getProviderName() { diff --git a/framework/elasticpath/.env.template b/framework/elasticpath/.env.template new file mode 100644 index 000000000..4e949519e --- /dev/null +++ b/framework/elasticpath/.env.template @@ -0,0 +1,6 @@ +# Available providers: bigcommerce, shopify, swell +COMMERCE_PROVIDER=elasticpath +NEXT_PUBLIC_ELASTICPATH_BASE= +NEXT_PUBLIC_ELASTICPATH_STOREID= +NEXT_PUBLIC_ELASTICPATH_CLIENTID= +NEXT_PUBLIC_ELASTICPATH_SECRET= diff --git a/framework/elasticpath/README.md b/framework/elasticpath/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/framework/elasticpath/api/endpoints/login/index.ts b/framework/elasticpath/api/endpoints/login/index.ts new file mode 100644 index 000000000..54a1d8683 --- /dev/null +++ b/framework/elasticpath/api/endpoints/login/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import loginEndpoint from '@commerce/api/endpoints/login' +import type { LoginSchema } from '../../../types/login' +import type { ElasticpathAPI } from '../..' +import login from './login' + +export type LoginAPI = GetAPISchema + +export type LoginEndpoint = LoginAPI['endpoint'] + +export const handlers: LoginEndpoint['handlers'] = { login } + +const loginApi = createEndpoint({ + handler: loginEndpoint, + handlers, +}) + +export default loginApi diff --git a/framework/elasticpath/api/endpoints/login/login.ts b/framework/elasticpath/api/endpoints/login/login.ts new file mode 100644 index 000000000..f55c3b54f --- /dev/null +++ b/framework/elasticpath/api/endpoints/login/login.ts @@ -0,0 +1,49 @@ +import { FetcherError } from '@commerce/utils/errors' +import type { LoginEndpoint } from '.' + +const invalidCredentials = /invalid credentials/i + +const login: LoginEndpoint['handlers']['login'] = async ({ + res, + body: { email, password }, + config, + commerce, +}) => { + // TODO: Add proper validations with something like Ajv + if (!(email && password)) { + return res.status(400).json({ + data: null, + errors: [{ message: 'Invalid request' }], + }) + } + // TODO: validate the password and email + // Passwords must be at least 7 characters and contain both alphabetic + // and numeric characters. + + try { + await commerce.login({ variables: { email, password }, config, res }) + } catch (error) { + // Check if the email and password didn't match an existing account + if ( + error instanceof FetcherError && + invalidCredentials.test(error.message) + ) { + return res.status(401).json({ + data: null, + errors: [ + { + message: + 'Cannot find an account that matches the provided credentials', + code: 'invalid_credentials', + }, + ], + }) + } + + throw error + } + + res.status(200).json({ data: null }) +} + +export default login diff --git a/framework/elasticpath/api/index.ts b/framework/elasticpath/api/index.ts new file mode 100644 index 000000000..8680d90ab --- /dev/null +++ b/framework/elasticpath/api/index.ts @@ -0,0 +1,59 @@ +import type { RequestInit } from '@vercel/fetch' +import { + CommerceAPI, + CommerceAPIConfig, + getCommerceApi as commerceApi, +} from '@commerce/api' +import createFetcher from './utils/fetch-local' + +import type { LoginAPI } from './endpoints/login' +import login from './operations/login' + +const API_URL = process.env.NEXT_PUBLIC_ELASTICPATH_BASE +const STOREID = process.env.NEXT_PUBLIC_ELASTICPATH_STOREID +const SECRET = process.env.NEXT_PUBLIC_ELASTICPATH_SECRET +const CLIENTID = process.env.NEXT_PUBLIC_ELASTICPATH_CLIENTID + +if (!API_URL) { + throw new Error( + `The environment variable BIGCOMMERCE_STOREFRONT_API_URL is missing and it's required to access your store` + ) +} + + +const ONE_DAY = 60 * 60 * 24 + +const config: any = { + commerceUrl: API_URL, + customerCookie: 'SHOP_TOKEN', + cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId', + cartCookieMaxAge: ONE_DAY * 30, + applyLocale: true, + + // REST API only + storeApiUrl: API_URL, + fetch: createFetcher(() => getCommerceApi().getConfig()), +} + +const operations = { + login +} + +export interface ElasticpathConfig extends CommerceAPIConfig { + fetch: any +} + +export const provider = { config, operations } + +export type Provider = typeof provider + +export type APIs = + | LoginAPI + +export type ElasticpathAPI

= CommerceAPI

+ +export function getCommerceApi

( + customProvider: P = provider as any +): ElasticpathAPI

{ + return commerceApi(customProvider) +} diff --git a/framework/elasticpath/api/operations/login.ts b/framework/elasticpath/api/operations/login.ts new file mode 100644 index 000000000..76cbb177d --- /dev/null +++ b/framework/elasticpath/api/operations/login.ts @@ -0,0 +1,46 @@ +import type { ServerResponse } from 'http' +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { LoginOperation } from '../../types/login' +import { Provider, ElasticpathConfig } from '..' + +export default function loginOperation({ + commerce, +}: OperationContext) { + async function login(opts: { + variables: T['variables'] + config?: Partial + res: ServerResponse + }): Promise + + async function login( + opts: { + variables: T['variables'] + config?: Partial + res: ServerResponse + } & OperationOptions + ): Promise + + async function login({ + variables, + res: response, + config: cfg, + }: { + query?: string + variables: T['variables'] + res: ServerResponse + config?: Partial + }): Promise { + const config = commerce.getConfig(cfg) + + const { data } = await config.fetch('account', 'login', [variables]) + + return { + result: data, + } + } + + return login +} diff --git a/framework/elasticpath/api/utils/fetch-local.ts b/framework/elasticpath/api/utils/fetch-local.ts new file mode 100644 index 000000000..def50ec38 --- /dev/null +++ b/framework/elasticpath/api/utils/fetch-local.ts @@ -0,0 +1,34 @@ +import { FetcherError } from '@commerce/utils/errors' +import type { GraphQLFetcher } from '@commerce/api' +import type { ElasticpathConfig } from '../index' +import fetch from './fetch' + +const fetchGraphqlApi: (getConfig: () => ElasticpathConfig) => 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 diff --git a/framework/elasticpath/api/utils/fetch.ts b/framework/elasticpath/api/utils/fetch.ts new file mode 100644 index 000000000..9d9fff3ed --- /dev/null +++ b/framework/elasticpath/api/utils/fetch.ts @@ -0,0 +1,3 @@ +import zeitFetch from '@vercel/fetch' + +export default zeitFetch() diff --git a/framework/elasticpath/auth/index.ts b/framework/elasticpath/auth/index.ts new file mode 100644 index 000000000..36e757a89 --- /dev/null +++ b/framework/elasticpath/auth/index.ts @@ -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' diff --git a/framework/elasticpath/auth/use-login.tsx b/framework/elasticpath/auth/use-login.tsx new file mode 100644 index 000000000..28351dc7f --- /dev/null +++ b/framework/elasticpath/auth/use-login.tsx @@ -0,0 +1,16 @@ +import { MutationHook } from '@commerce/utils/types' +import useLogin, { UseLogin } from '@commerce/auth/use-login' + +export default useLogin as UseLogin + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher() { + return null + }, + useHook: () => () => { + return async function () {} + }, +} diff --git a/framework/elasticpath/auth/use-logout.tsx b/framework/elasticpath/auth/use-logout.tsx new file mode 100644 index 000000000..9b3fc3e44 --- /dev/null +++ b/framework/elasticpath/auth/use-logout.tsx @@ -0,0 +1,17 @@ +import { MutationHook } from '@commerce/utils/types' +import useLogout, { UseLogout } from '@commerce/auth/use-logout' + +export default useLogout as UseLogout + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher() { + return null + }, + useHook: + ({ fetch }) => + () => + async () => {}, +} diff --git a/framework/elasticpath/auth/use-signup.tsx b/framework/elasticpath/auth/use-signup.tsx new file mode 100644 index 000000000..e9ad13458 --- /dev/null +++ b/framework/elasticpath/auth/use-signup.tsx @@ -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 + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher() { + return null + }, + useHook: + ({ fetch }) => + () => + () => {}, +} diff --git a/framework/elasticpath/cart/index.ts b/framework/elasticpath/cart/index.ts new file mode 100644 index 000000000..3b8ba990e --- /dev/null +++ b/framework/elasticpath/cart/index.ts @@ -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' diff --git a/framework/elasticpath/cart/use-add-item.tsx b/framework/elasticpath/cart/use-add-item.tsx new file mode 100644 index 000000000..7f3d1061f --- /dev/null +++ b/framework/elasticpath/cart/use-add-item.tsx @@ -0,0 +1,17 @@ +import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item' +import { MutationHook } from '@commerce/utils/types' + +export default useAddItem as UseAddItem +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: + ({ fetch }) => + () => { + return async function addItem() { + return {} + } + }, +} diff --git a/framework/elasticpath/cart/use-cart.tsx b/framework/elasticpath/cart/use-cart.tsx new file mode 100644 index 000000000..b3e509a21 --- /dev/null +++ b/framework/elasticpath/cart/use-cart.tsx @@ -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 + +export const handler: SWRHook = { + 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, + }, + } + ), + [] + ) + }, +} diff --git a/framework/elasticpath/cart/use-remove-item.tsx b/framework/elasticpath/cart/use-remove-item.tsx new file mode 100644 index 000000000..b4ed583b8 --- /dev/null +++ b/framework/elasticpath/cart/use-remove-item.tsx @@ -0,0 +1,18 @@ +import { MutationHook } from '@commerce/utils/types' +import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item' + +export default useRemoveItem as UseRemoveItem + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: + ({ fetch }) => + () => { + return async function removeItem(input) { + return {} + } + }, +} diff --git a/framework/elasticpath/cart/use-update-item.tsx b/framework/elasticpath/cart/use-update-item.tsx new file mode 100644 index 000000000..06d703f70 --- /dev/null +++ b/framework/elasticpath/cart/use-update-item.tsx @@ -0,0 +1,18 @@ +import { MutationHook } from '@commerce/utils/types' +import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item' + +export default useUpdateItem as UseUpdateItem + +export const handler: MutationHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: + ({ fetch }) => + () => { + return async function addItem() { + return {} + } + }, +} diff --git a/framework/elasticpath/commerce.config.json b/framework/elasticpath/commerce.config.json new file mode 100644 index 000000000..6fb526aaf --- /dev/null +++ b/framework/elasticpath/commerce.config.json @@ -0,0 +1,9 @@ +{ + "features": { + "cart": true, + "search": true, + "wishlist": true, + "customerAuth": true, + "customCheckout": true + } +} diff --git a/framework/elasticpath/customer/index.ts b/framework/elasticpath/customer/index.ts new file mode 100644 index 000000000..6c903ecc5 --- /dev/null +++ b/framework/elasticpath/customer/index.ts @@ -0,0 +1 @@ +export { default as useCustomer } from './use-customer' diff --git a/framework/elasticpath/customer/use-customer.tsx b/framework/elasticpath/customer/use-customer.tsx new file mode 100644 index 000000000..41757cd0d --- /dev/null +++ b/framework/elasticpath/customer/use-customer.tsx @@ -0,0 +1,15 @@ +import { SWRHook } from '@commerce/utils/types' +import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' + +export default useCustomer as UseCustomer +export const handler: SWRHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: () => () => { + return async function addItem() { + return {} + } + }, +} diff --git a/framework/elasticpath/data.json b/framework/elasticpath/data.json new file mode 100644 index 000000000..18c8ee718 --- /dev/null +++ b/framework/elasticpath/data.json @@ -0,0 +1,235 @@ +{ + "products": [ + { + "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjA=", + "name": "New Short Sleeve T-Shirt", + "vendor": "Next.js", + "path": "/new-short-sleeve-t-shirt", + "slug": "new-short-sleeve-t-shirt", + "price": { "value": 25, "currencyCode": "USD" }, + "descriptionHtml": "

Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.

", + "images": [ + { + "url": "/assets/drop-shirt-0.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/drop-shirt-1.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/drop-shirt-2.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + } + ], + "variants": [ + { + "id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjAss=", + "options": [ + { + "__typename": "MultipleChoiceOption", + "id": "asd", + "displayName": "Size", + "values": [ + { + "label": "XL" + } + ] + } + ] + } + ], + "options": [ + { + "id": "option-color", + "displayName": "Color", + "values": [ + { + "label": "color", + "hexColors": ["#222"] + } + ] + }, + { + "id": "option-size", + "displayName": "Size", + "values": [ + { + "label": "S" + }, + { + "label": "M" + }, + { + "label": "L" + } + ] + } + ] + }, + { + "id": "Z2lkOi8vc2hvcGlmeS9Qcm9ksdWN0LzU0NDczMjUwMjQ0MjA=", + "name": "Lightweight Jacket", + "vendor": "Next.js", + "path": "/lightweight-jacket", + "slug": "lightweight-jacket", + "price": { "value": 249.99, "currencyCode": "USD" }, + "descriptionHtml": "

Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.

", + "images": [ + { + "url": "/assets/lightweight-jacket-0.png", + "altText": "Lightweight Jacket", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/lightweight-jacket-1.png", + "altText": "Lightweight Jacket", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/lightweight-jacket-2.png", + "altText": "Lightweight Jacket", + "width": 1000, + "height": 1000 + } + ], + "variants": [ + { + "id": "Z2lkOid8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjAss=", + "options": [ + { + "__typename": "MultipleChoiceOption", + "id": "asd", + "displayName": "Size", + "values": [ + { + "label": "XL" + } + ] + } + ] + } + ], + "options": [ + { + "id": "option-color", + "displayName": "Color", + "values": [ + { + "label": "color", + "hexColors": ["#222"] + } + ] + }, + { + "id": "option-size", + "displayName": "Size", + "values": [ + { + "label": "S" + }, + { + "label": "M" + }, + { + "label": "L" + } + ] + } + ] + }, + { + "id": "Z2lkOis8vc2hvcGlmsddeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjA=", + "name": "Shirt", + "vendor": "Next.js", + "path": "/shirt", + "slug": "shirt", + "price": { "value": 25, "currencyCode": "USD" }, + "descriptionHtml": "

Show off your love for Next.js and Vercel with this unique, limited edition t-shirt. This design is part of a limited run, numbered drop at the June 2021 Next.js Conf. It features a unique, handcrafted triangle design. Get it while supplies last – only 200 of these shirts will be made! All proceeds will be donated to charity.

", + "images": [ + { + "url": "/assets/t-shirt-0.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/t-shirt-1.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/t-shirt-2.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/t-shirt-3.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + }, + { + "url": "/assets/t-shirt-4.png", + "altText": "Shirt", + "width": 1000, + "height": 1000 + } + ], + "variants": [ + { + "id": "Z2lkOi8vc2hvcGlmeS9Qcms9kdWN0LzU0NDczMjUwMjQ0MjAss=", + "options": [ + { + "__typename": "MultipleChoiceOption", + "id": "asd", + "displayName": "Size", + "values": [ + { + "label": "XL" + } + ] + } + ] + } + ], + "options": [ + { + "id": "option-color", + "displayName": "Color", + "values": [ + { + "label": "color", + "hexColors": ["#222"] + } + ] + }, + { + "id": "option-size", + "displayName": "Size", + "values": [ + { + "label": "S" + }, + { + "label": "M" + }, + { + "label": "L" + } + ] + } + ] + } + ] +} diff --git a/framework/elasticpath/fetcher.ts b/framework/elasticpath/fetcher.ts new file mode 100644 index 000000000..f22d04e38 --- /dev/null +++ b/framework/elasticpath/fetcher.ts @@ -0,0 +1,50 @@ +import { Fetcher } from '@commerce/utils/types' +import { FetcherError } from '@commerce/utils/errors' + +async function getText(res: Response) { + try { + return (await res.text()) || res.statusText + } catch (error) { + return res.statusText + } +} + +async function getError(res: Response) { + if (res.headers.get('Content-Type')?.includes('application/json')) { + const data = await res.json() + return new FetcherError({ errors: data.errors, status: res.status }) + } + return new FetcherError({ message: await getText(res), status: res.status }) +} + +export const fetcher: Fetcher = async ({ + url, + method = 'POST', + variables, + query, + body: bodyObj, +}) => { + const shopApiUrl = + process.env.NEXT_PUBLIC_ELASTICPATH_BASE + if (!shopApiUrl) { + throw new Error( + 'The Vendure Shop API url has not been provided. Please define NEXT_PUBLIC_VENDURE_SHOP_API_URL in .env.local' + ) + } + const hasBody = Boolean(variables || query) + const body = hasBody ? JSON.stringify({ query, variables }) : undefined + const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined + const res = await fetch(shopApiUrl, { + method, + body, + headers, + credentials: 'include', + }) + + if (res.ok) { + const { data } = await res.json() + return data + } + + throw await getError(res) +} diff --git a/framework/elasticpath/index.tsx b/framework/elasticpath/index.tsx new file mode 100644 index 000000000..8dd625332 --- /dev/null +++ b/framework/elasticpath/index.tsx @@ -0,0 +1,36 @@ +import type { ReactNode } from 'react' +import { + CommerceConfig, + CommerceProvider as CoreCommerceProvider, + useCommerce as useCoreCommerce, +} from '@commerce' +import { elasticpathProvider } from './provider' +import type { ElasticpathProvider } from './provider' + +export { elasticpathProvider } +export type { ElasticpathProvider } + +export const elasticpathConfig: CommerceConfig = { + locale: 'en-us', + cartCookie: 'bc_cartId', +} + +export type ElasticpathConfig = Partial + +export type ElasticpathProps = { + children?: ReactNode + locale: string +} & ElasticpathConfig + +export function CommerceProvider({ children, ...config }: ElasticpathProps) { + return ( + + {children} + + ) +} + +export const useCommerce = () => useCoreCommerce() \ No newline at end of file diff --git a/framework/elasticpath/next.config.js b/framework/elasticpath/next.config.js new file mode 100644 index 000000000..e69de29bb diff --git a/framework/elasticpath/product/index.ts b/framework/elasticpath/product/index.ts new file mode 100644 index 000000000..426a3edcd --- /dev/null +++ b/framework/elasticpath/product/index.ts @@ -0,0 +1,2 @@ +export { default as usePrice } from './use-price' +export { default as useSearch } from './use-search' diff --git a/framework/elasticpath/product/use-price.tsx b/framework/elasticpath/product/use-price.tsx new file mode 100644 index 000000000..0174faf5e --- /dev/null +++ b/framework/elasticpath/product/use-price.tsx @@ -0,0 +1,2 @@ +export * from '@commerce/product/use-price' +export { default } from '@commerce/product/use-price' diff --git a/framework/elasticpath/product/use-search.tsx b/framework/elasticpath/product/use-search.tsx new file mode 100644 index 000000000..30e699537 --- /dev/null +++ b/framework/elasticpath/product/use-search.tsx @@ -0,0 +1,17 @@ +import { SWRHook } from '@commerce/utils/types' +import useSearch, { UseSearch } from '@commerce/product/use-search' +export default useSearch as UseSearch + +export const handler: SWRHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input, options, fetch }) {}, + useHook: () => () => { + return { + data: { + products: [], + }, + } + }, +} diff --git a/framework/elasticpath/provider.ts b/framework/elasticpath/provider.ts new file mode 100644 index 000000000..8be96445d --- /dev/null +++ b/framework/elasticpath/provider.ts @@ -0,0 +1,34 @@ +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 useWishlist } from './wishlist/use-wishlist' +import { handler as useWishlistAddItem } from './wishlist/use-add-item' +import { handler as useWishlistRemoveItem } from './wishlist/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' + +import {fetcher} from './fetcher' + +export const elasticpathProvider = { + locale: 'en-us', + cartCookie: 'ep_cartId', + fetcher, + cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, + wishlist: { + useWishlist, + useAddItem: useWishlistAddItem, + useRemoveItem: useWishlistRemoveItem, + }, + customer: { useCustomer }, + products: { useSearch }, + auth: { useLogin, useLogout, useSignup }, +} + +export type ElasticpathProvider = typeof elasticpathProvider \ No newline at end of file diff --git a/framework/elasticpath/types/login.ts b/framework/elasticpath/types/login.ts new file mode 100644 index 000000000..a69bffc98 --- /dev/null +++ b/framework/elasticpath/types/login.ts @@ -0,0 +1,10 @@ +import * as Core from '@commerce/types/login' + +export * from '@commerce/types/login' + +export type LoginOperation = Core.LoginOperation & { + variables: { + email: string + password: string + } +} diff --git a/framework/elasticpath/utils/handle-fetch-response.ts b/framework/elasticpath/utils/handle-fetch-response.ts new file mode 100644 index 000000000..ac8150138 --- /dev/null +++ b/framework/elasticpath/utils/handle-fetch-response.ts @@ -0,0 +1,19 @@ +import { CommerceError } from '@commerce/utils/errors' + +type ElasticpathFetchResponse = { + error: { + message: string + code?: string + } +} + +const handleFetchResponse = async (res: ElasticpathFetchResponse) => { + if (res) { + if (res.error) { + throw new CommerceError(res.error) + } + return res + } +} + +export default handleFetchResponse diff --git a/framework/elasticpath/wishlist/use-add-item.tsx b/framework/elasticpath/wishlist/use-add-item.tsx new file mode 100644 index 000000000..b7ba918f1 --- /dev/null +++ b/framework/elasticpath/wishlist/use-add-item.tsx @@ -0,0 +1,36 @@ +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import { CommerceError } from '@commerce/utils/errors' +import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item' +import useCustomer from '../customer/use-customer' +import useWishlist from './use-wishlist' + +export default useAddItem as UseAddItem + +export const handler: MutationHook = { + fetchOptions: { + url: '/api/wishlist', + method: 'POST', + }, + useHook: ({ fetch }) => () => { + const { data: customer } = useCustomer() + const { revalidate } = useWishlist() + + return useCallback( + async function addItem(item) { + if (!customer) { + // A signed customer is required in order to have a wishlist + throw new CommerceError({ + message: 'Signed customer not found', + }) + } + + // TODO: add validations before doing the fetch + const data = await fetch({ input: { item } }) + await revalidate() + return data + }, + [fetch, revalidate, customer] + ) + }, +} diff --git a/framework/elasticpath/wishlist/use-remove-item.tsx b/framework/elasticpath/wishlist/use-remove-item.tsx new file mode 100644 index 000000000..87c3c8a0b --- /dev/null +++ b/framework/elasticpath/wishlist/use-remove-item.tsx @@ -0,0 +1,37 @@ +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import { CommerceError } from '@commerce/utils/errors' +import useRemoveItem, { + UseRemoveItem, +} from '@commerce/wishlist/use-remove-item' +import useCustomer from '../customer/use-customer' +import useWishlist from './use-wishlist' + +export default useRemoveItem as UseRemoveItem + +export const handler: MutationHook = { + fetchOptions: { + url: '/api/wishlist', + method: 'DELETE', + }, + useHook: ({ fetch }) => ({ wishlist } = {}) => { + const { data: customer } = useCustomer() + const { revalidate } = useWishlist(wishlist) + + return useCallback( + async function removeItem(input) { + if (!customer) { + // A signed customer is required in order to have a wishlist + throw new CommerceError({ + message: 'Signed customer not found', + }) + } + + const data = await fetch({ input: { itemId: String(input.id) } }) + await revalidate() + return data + }, + [fetch, revalidate, customer] + ) + }, +} diff --git a/framework/elasticpath/wishlist/use-wishlist.tsx b/framework/elasticpath/wishlist/use-wishlist.tsx new file mode 100644 index 000000000..cdffb8d95 --- /dev/null +++ b/framework/elasticpath/wishlist/use-wishlist.tsx @@ -0,0 +1,51 @@ +import { useMemo } from 'react' +import { SWRHook } from '@commerce/utils/types' +import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist' +import useCustomer from '../customer/use-customer' + +export default useWishlist as UseWishlist + +export const handler: SWRHook = { + fetchOptions: { + query: '', + }, + async fetcher({ input: { customerId, includeProducts }, options, fetch }) { + if (!customerId) return null + + // Use a dummy base as we only care about the relative path + const url = new URL(options.url!, 'http://a') + + if (includeProducts) url.searchParams.set('products', '1') + + return fetch({ + url: url.pathname + url.search, + method: options.method, + }) + }, + useHook: ({ useData }) => (input) => { + const { data: customer } = useCustomer() + const response = useData({ + input: [ + ['customerId', customer?.entityId], + ['includeProducts', input?.includeProducts], + ], + swrOptions: { + revalidateOnFocus: false, + ...input?.swrOptions, + }, + }) + + return useMemo( + () => + Object.create(response, { + isEmpty: { + get() { + return (response.data?.items?.length || 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, +} diff --git a/tsconfig.json b/tsconfig.json index 0b71cd09b..120d625c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,8 +22,8 @@ "@components/*": ["components/*"], "@commerce": ["framework/commerce"], "@commerce/*": ["framework/commerce/*"], - "@framework": ["framework/local"], - "@framework/*": ["framework/local/*"] + "@framework": ["framework/elasticpath"], + "@framework/*": ["framework/elasticpath/*"] } }, "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],